Skip to content

Commit 8184213

Browse files
committed
ci: add security scanning suite and governance
Consolidates the security CI work into one reviewable change. Adds, as separate workflow files under .github/workflows/: - secret-scan.yml gitleaks (pinned + checksum-verified), full history - workflow-security.yml actionlint + zizmor, audits the workflows themselves - dependency-review.yml PR dependency gate + advisory pip-audit - container-scan.yml hadolint (blocking) + Trivy image scan (advisory) - codeql.yml CodeQL for Python and JS, main + weekly Plus .github/dependabot.yml (pip/npm/actions/docker), .github/CODEOWNERS, and docs/security-ci.md explaining each check and the one-time settings. All additive: no existing files are modified. Actions are pinned to commit SHAs, tokens default-deny (permissions: {}), advisory scans never block, and SARIF upload is gated to push so fork PRs do not fail on a read-only token. Composes with the correctness CI in #1015.
1 parent ed6cc88 commit 8184213

8 files changed

Lines changed: 531 additions & 0 deletions

File tree

.github/CODEOWNERS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Code owners.
2+
#
3+
# Every file is owned by the maintainer, so that when branch protection has
4+
# "Require review from Code Owners" turned on, no pull request can be merged
5+
# without the maintainer's review. This is the human gate that backs up the
6+
# automated security checks. See docs/security-ci.md for how to turn it on.
7+
8+
* @pewdiepie-archdaemon

.github/dependabot.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Dependabot keeps dependencies and pinned action versions current.
2+
#
3+
# Why this matters for security: every workflow in this repo pins its GitHub
4+
# Actions to an exact commit (a SHA), which is safe but freezes them in time.
5+
# Dependabot opens a small, reviewable pull request whenever a newer version
6+
# exists -- for Python packages, npm packages, the Docker base image, and the
7+
# pinned Actions themselves -- so staying patched does not require manual work.
8+
# Updates are grouped so a week's bumps arrive as one PR per ecosystem, not a
9+
# flood of separate ones.
10+
11+
version: 2
12+
updates:
13+
# Python dependencies (requirements.txt + requirements-optional.txt).
14+
- package-ecosystem: pip
15+
directory: "/"
16+
schedule:
17+
interval: weekly
18+
open-pull-requests-limit: 5
19+
groups:
20+
python:
21+
patterns: ["*"]
22+
23+
# Frontend / tooling npm packages (package.json).
24+
- package-ecosystem: npm
25+
directory: "/"
26+
schedule:
27+
interval: weekly
28+
open-pull-requests-limit: 5
29+
groups:
30+
npm:
31+
patterns: ["*"]
32+
33+
# The pinned action SHAs used across .github/workflows.
34+
- package-ecosystem: github-actions
35+
directory: "/"
36+
schedule:
37+
interval: weekly
38+
open-pull-requests-limit: 5
39+
groups:
40+
actions:
41+
patterns: ["*"]
42+
43+
# The Docker base image in the Dockerfile.
44+
- package-ecosystem: docker
45+
directory: "/"
46+
schedule:
47+
interval: weekly
48+
open-pull-requests-limit: 5

.github/workflows/codeql.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# CodeQL code scanning
2+
#
3+
# Purpose: GitHub's own static analysis engine reads the application source
4+
# (Python backend + the JavaScript frontend) and looks for real
5+
# vulnerabilities -- SQL/command injection, path traversal, auth mistakes,
6+
# unsafe deserialization. Findings appear in the repo's Security tab. This is
7+
# the deepest check in the suite and the most valuable for a high-profile
8+
# target.
9+
#
10+
# It runs on every push to main and on a weekly schedule (to catch newly
11+
# disclosed query patterns against unchanged code). It deliberately does NOT
12+
# run on pull requests: most PRs here come from forks, whose read-only token
13+
# cannot publish results, which would produce confusing failures. To scan pull
14+
# requests too, a maintainer can instead enable CodeQL "default setup" in
15+
# Settings -> Security -> Code scanning (one toggle, no file needed) -- see
16+
# docs/security-ci.md.
17+
18+
name: CodeQL
19+
20+
on:
21+
push:
22+
branches: [main]
23+
schedule:
24+
# Weekly, Monday 06:00 UTC.
25+
- cron: '0 6 * * 1'
26+
workflow_dispatch:
27+
28+
permissions: {}
29+
30+
concurrency:
31+
group: codeql-${{ github.workflow }}-${{ github.ref }}
32+
cancel-in-progress: true
33+
34+
jobs:
35+
analyze:
36+
name: Analyze (${{ matrix.language }})
37+
runs-on: ubuntu-latest
38+
permissions:
39+
contents: read
40+
security-events: write # publish results to the Security tab
41+
strategy:
42+
fail-fast: false
43+
matrix:
44+
# Both are interpreted, so CodeQL needs no build step (build-mode none).
45+
language: [python, javascript-typescript]
46+
steps:
47+
- name: Checkout repository
48+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49+
with:
50+
persist-credentials: false
51+
52+
- name: Initialize CodeQL
53+
uses: github/codeql-action/init@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
54+
with:
55+
languages: ${{ matrix.language }}
56+
build-mode: none
57+
58+
- name: Perform CodeQL analysis
59+
uses: github/codeql-action/analyze@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
60+
with:
61+
category: "/language:${{ matrix.language }}"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Container security
2+
#
3+
# Purpose: the Docker image is how most people run Odysseus, so it is part of
4+
# the attack surface. Two checks:
5+
#
6+
# - hadolint: lints the Dockerfile for mistakes and insecure patterns
7+
# (running as root longer than needed, unpinned base image, bad apt usage).
8+
# Blocking.
9+
# - Trivy: builds the image and scans it for known-vulnerable OS and Python
10+
# packages. Advisory only -- it reports findings to the repo's Security tab
11+
# without blocking a merge, because the image inevitably contains
12+
# already-known CVEs in upstream packages that are not this project's bug.
13+
#
14+
# Note: a separate open PR (#120) proposes a local `scripts/scan_image.py`.
15+
# This job is complementary -- it is a CI gate plus Security-tab integration,
16+
# not a script a contributor has to remember to run.
17+
18+
name: Container scan
19+
20+
on:
21+
pull_request:
22+
push:
23+
branches: [main]
24+
workflow_dispatch:
25+
26+
permissions: {}
27+
28+
concurrency:
29+
group: container-scan-${{ github.workflow }}-${{ github.ref }}
30+
cancel-in-progress: true
31+
32+
jobs:
33+
hadolint:
34+
name: hadolint (Dockerfile lint)
35+
runs-on: ubuntu-latest
36+
permissions:
37+
contents: read
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
41+
with:
42+
persist-credentials: false
43+
44+
- name: Lint Dockerfile
45+
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
46+
with:
47+
dockerfile: Dockerfile
48+
# DL3008: pinning apt package versions is impractical on a -slim base
49+
# image. Debian purges old package versions from its repos, so a
50+
# pinned version breaks future rebuilds. The base image itself is
51+
# what should be pinned (tracked by Dependabot's docker ecosystem).
52+
ignore: DL3008
53+
54+
trivy:
55+
name: Trivy (image scan, advisory)
56+
runs-on: ubuntu-latest
57+
# Advisory: a CVE in an upstream package must not block unrelated PRs.
58+
continue-on-error: true
59+
permissions:
60+
contents: read
61+
security-events: write # upload SARIF to the Security tab (push only)
62+
steps:
63+
- name: Checkout repository
64+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
65+
with:
66+
persist-credentials: false
67+
68+
- name: Set up Buildx
69+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
70+
71+
# Build without pushing so a broken Dockerfile is caught here, and the
72+
# exact image we ship is what gets scanned.
73+
- name: Build image
74+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
75+
with:
76+
context: .
77+
push: false
78+
load: true
79+
tags: odysseus:ci
80+
81+
- name: Scan image with Trivy
82+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
83+
with:
84+
image-ref: odysseus:ci
85+
format: sarif
86+
output: trivy-results.sarif
87+
ignore-unfixed: true
88+
env:
89+
# Pin the vuln DB source to GHCR to avoid rate-limited Docker Hub
90+
# mirrors that flake on shared runners.
91+
TRIVY_DB_REPOSITORY: ghcr.io/aquasecurity/trivy-db:2
92+
93+
# Upload only on push to main. Pull requests from forks get a read-only
94+
# token that cannot write to the Security tab, so skipping there avoids a
95+
# confusing failure; the main-branch scan keeps the tab populated.
96+
- name: Upload Trivy results
97+
if: github.event_name == 'push'
98+
uses: github/codeql-action/upload-sarif@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
99+
with:
100+
sarif_file: trivy-results.sarif
101+
category: trivy-image
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Supply-chain review
2+
#
3+
# Purpose: defend against "side-chain" / supply-chain attacks -- a pull request
4+
# that adds (or bumps) a dependency to a version with a known vulnerability or a
5+
# disallowed license. Two layers:
6+
#
7+
# - dependency-review: runs ONLY on pull requests. It compares the
8+
# dependencies before and after the PR and blocks the merge if the change
9+
# pulls in a package with a known security advisory. This is the gate.
10+
# - pip-audit: scans the project's current Python requirements against the
11+
# advisory database. Advisory only (it never blocks a merge), because it can
12+
# flag a pre-existing issue in an already-shipped dependency.
13+
14+
name: Dependency review
15+
16+
on:
17+
pull_request:
18+
push:
19+
branches: [main]
20+
workflow_dispatch:
21+
22+
# Default-deny token; jobs grant only read access.
23+
permissions: {}
24+
25+
concurrency:
26+
group: dependency-review-${{ github.workflow }}-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
jobs:
30+
dependency-review:
31+
name: dependency-review (PR gate)
32+
# Only meaningful on a pull request -- it needs a base..head diff to review.
33+
if: github.event_name == 'pull_request'
34+
runs-on: ubuntu-latest
35+
permissions:
36+
contents: read
37+
steps:
38+
- name: Checkout repository
39+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
40+
with:
41+
persist-credentials: false
42+
43+
- name: Review dependency changes
44+
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
45+
with:
46+
# Fail the PR on any newly introduced moderate-or-worse advisory.
47+
fail-on-severity: moderate
48+
49+
pip-audit:
50+
name: pip-audit (advisory)
51+
runs-on: ubuntu-latest
52+
# Advisory: report known-vulnerable Python deps without blocking the merge.
53+
continue-on-error: true
54+
permissions:
55+
contents: read
56+
steps:
57+
- name: Checkout repository
58+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
59+
with:
60+
persist-credentials: false
61+
62+
- name: Set up Python
63+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
64+
with:
65+
python-version: '3.12'
66+
67+
- name: Run pip-audit on requirements
68+
run: |
69+
set -euo pipefail
70+
pip install pip-audit==2.10.0
71+
pip-audit -r requirements.txt -r requirements-optional.txt --strict

.github/workflows/secret-scan.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Secret scanning
2+
#
3+
# Purpose: stop credentials (API keys, tokens, passwords, private keys) from
4+
# ever living in the Git history. Odysseus deliberately keeps real secrets in
5+
# files that are gitignored (.env, data/), but a slip in a future commit -- or a
6+
# malicious pull request that sneaks one in -- would otherwise go unnoticed.
7+
# This job reads the repository and the full commit history and fails if it
8+
# finds anything that looks like a secret.
9+
#
10+
# It runs the official gitleaks BINARY directly (pinned to an exact version and
11+
# verified against the project's published SHA-256 checksum) rather than the
12+
# gitleaks GitHub Action, because the Action asks for a paid license on
13+
# organization-owned repos. The binary is free and behaves identically.
14+
15+
name: Secret scan
16+
17+
on:
18+
pull_request:
19+
push:
20+
branches: [main]
21+
workflow_dispatch:
22+
23+
# Start with zero permissions; the single job opts back in to read-only.
24+
permissions: {}
25+
26+
concurrency:
27+
group: secret-scan-${{ github.workflow }}-${{ github.ref }}
28+
cancel-in-progress: true
29+
30+
jobs:
31+
gitleaks:
32+
name: gitleaks
33+
runs-on: ubuntu-latest
34+
permissions:
35+
contents: read
36+
steps:
37+
- name: Checkout repository
38+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39+
with:
40+
# Full history so a secret committed in an earlier commit (and later
41+
# deleted) is still caught -- deletion does not remove it from Git.
42+
fetch-depth: 0
43+
persist-credentials: false
44+
45+
# Pinned version + checksum so a tampered release binary cannot run here.
46+
# Bump VERSION/SHA256 together; the checksum comes from the matching
47+
# gitleaks_<version>_checksums.txt on the GitHub release.
48+
- name: Run gitleaks (pinned, checksum-verified)
49+
env:
50+
GITLEAKS_VERSION: 8.30.1
51+
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
52+
run: |
53+
set -euo pipefail
54+
TARBALL="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
55+
curl -fsSL -o "${TARBALL}" \
56+
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${TARBALL}"
57+
echo "${GITLEAKS_SHA256} ${TARBALL}" | sha256sum -c -
58+
tar -xzf "${TARBALL}" gitleaks
59+
# Scan the whole history. Findings print to the log and fail the job.
60+
./gitleaks git --no-banner --redact --verbose .

0 commit comments

Comments
 (0)