-
Notifications
You must be signed in to change notification settings - Fork 114
feat(v2): harden the runtime image, schedule CodeQL, and project rewards and rebuilds #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,81 @@ | ||
| # --------------------------------------------------------------------------- | ||
| # #498 — build-context hygiene. | ||
| # | ||
| # Everything listed here is excluded from the context the Docker daemon | ||
| # receives. This is the control that makes "no secrets in the image" true by | ||
| # construction rather than by review: a file that is not in the context cannot | ||
| # be captured in a layer, and cannot survive in a layer cache either. | ||
| # | ||
| # Only `package*.json`, `package-lock.json`, `nest-cli.json`, `tsconfig*.json`, | ||
| # `prisma/`, and `src/` are actually needed to build. Everything else is | ||
| # dead weight in the context and a liability if it leaks. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # --- Version control & CI ------------------------------------------------- | ||
| # Git history is large and can contain credentials in past commits. | ||
| .git | ||
| .gitignore | ||
| .gitattributes | ||
| .github | ||
|
|
||
| # --- Dependencies & build output ----------------------------------------- | ||
| # Never ship a host node_modules or a stale dist into the image: both are | ||
| # rebuilt inside the builder stage against the pinned lockfile. | ||
| node_modules | ||
| coverage | ||
| dist | ||
| build | ||
| coverage | ||
| .nyc_output | ||
| *.tsbuildinfo | ||
|
|
||
| # --- Environment files ---------------------------------------------------- | ||
| # A real .env (or any variant) must never reach the daemon, a layer, or a | ||
| # cache entry. `.env.example` is the deliberate exception: it is checked-in | ||
| # documentation of the variable NAMES the image expects, contains no values, | ||
| # and is what an operator copies to .env on first run. | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
| # --- Local databases / dev state ----------------------------------------- | ||
| # Local SQLite files are developer state, never part of a runtime image. | ||
| database.sqlite | ||
| dev.db | ||
| *.sqlite | ||
| *.sqlite-journal | ||
| *.sqlite-wal | ||
| *.sqlite-shm | ||
| prisma/*.db | ||
|
|
||
| # --- Tests, fixtures & scratch output ------------------------------------- | ||
| test | ||
| coverage-reports | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| run-tests.js | ||
| test-ip-security.js | ||
| commit-fix.sh | ||
| git_automate.py | ||
| *.pid | ||
| pids | ||
|
|
||
| # --- Editor / OS cruft --------------------------------------------------- | ||
| .vscode | ||
| .idea | ||
| .DS_Store | ||
| .git | ||
| .gitignore | ||
| Thumbs.db | ||
| *.swp | ||
| *.swo | ||
|
|
||
| # --- Docker control files (not needed inside the image) ------------------ | ||
| Dockerfile | ||
| .dockerignore | ||
| docker-compose.yml | ||
| test | ||
| .vscode | ||
| npm-debug.log | ||
| docker-compose.*.yml | ||
|
|
||
| # --- Documentation -------------------------------------------------------- | ||
| # Large, and irrelevant at runtime. The contract-relevant content lives in | ||
| # docs/CONTAINER_IMAGE.md in the repository, not baked into the image. | ||
| *.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # --------------------------------------------------------------------------- | ||
| # CodeQL configuration for the TruthBounty API (issue #497). | ||
| # | ||
| # Referenced by both: | ||
| # .github/workflows/ci.yml (push/pull_request — owned by a | ||
| # parallel PR, not edited here) | ||
| # .github/workflows/codeql-schedule.yml (cron + workflow_dispatch) | ||
| # | ||
| # CodeQL validates this file against a strict schema. Keys are limited to: | ||
| # name, disable-default-queries, queries (+ include/exclude per query), | ||
| # paths, paths-ignore, packs, and disable-feature-flags. Adding an unknown key | ||
| # makes the action fail closed with a configuration error, so resist the urge | ||
| # to add "just one more option". | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| name: "TruthBounty API CodeQL config" | ||
|
|
||
| # Keep the default suite. This is the baseline of correctness/security queries | ||
| # that ship with the analyser; disabling it would silently drop coverage and | ||
| # there is no compensating control in this repository. | ||
| disable-default-queries: false | ||
|
|
||
| queries: | ||
| # Wider analysis than the default suite. `security-extended` adds the | ||
| # precise, lower-signal/lower-severity variants of security queries (e.g. | ||
| # the SSRF and injection variants that are usually false positives on an | ||
| # indexer that legitimately talks to an RPC provider). `security-and-quality` | ||
| # adds maintainability and reliability queries. Both are additive to the | ||
| # default suite, not a replacement for it. | ||
| # | ||
| # No `exclude` filters are declared here on purpose. A query exclusion is a | ||
| # blanket suppression by another name, and this repository's suppression | ||
| # policy (see docs/STATIC_ANALYSIS.md) requires a documented, expiring | ||
| # justification for any suppressed alert. Alert-level suppression belongs in | ||
| # the alert itself, where the justification and expiry can be reviewed. | ||
| - uses: security-extended | ||
| - uses: security-and-quality | ||
|
|
||
| # JavaScript/TypeScript is an interpreted language, so `paths` narrows the set | ||
| # of extracted source files rather than acting as a compiler include path. | ||
| # `src` is the entire shipped application: NestJS modules, services, | ||
| # controllers, entities, and the V2 projectors. | ||
| paths: | ||
| - src | ||
|
|
||
| # Build output and generated artifacts. These are excluded because analysing | ||
| # them produces noise with no security value: `dist` is a transpilation of | ||
| # files already covered by `src`, `src/generated` is the machine-written | ||
| # Prisma client, and the rest is test/coverage output that is never shipped. | ||
| # | ||
| # Note what is NOT here: no `**/*.spec.ts` and no source directory. Test files | ||
| # and application source are both analysed. Narrowing this list to dodge | ||
| # findings would be a coverage regression dressed up as hygiene. | ||
| paths-ignore: | ||
| - dist | ||
| - dist/** | ||
| - build | ||
| - build/** | ||
| - coverage | ||
| - coverage/** | ||
| - node_modules | ||
| - node_modules/** | ||
| # Machine-generated Prisma client — never hand-edited, never a place to fix. | ||
| - src/generated | ||
| - src/generated/** | ||
| # Source maps point back into `dist`, which is already ignored, and analyzing | ||
| # them double-counts every file. | ||
| - "**/*.js.map" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| name: CodeQL Scheduled Analysis | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Issue #497 — "Enforce CodeQL and Static Security Analysis". | ||
| # | ||
| # WHAT THIS CLOSES | ||
| # `.github/workflows/ci.yml` already runs CodeQL, but only on: | ||
| # push: branches: [main] | ||
| # pull_request: branches: [main] | ||
| # That means a CodeQL *query* or *scanner* update published by GitHub after | ||
| # the last PR is never applied to the default branch. A new | ||
| # `js/injection`-style query, or a new taint-mode model for a library this | ||
| # repo uses, silently produces zero alerts until the next unrelated PR lands. | ||
| # For a project that indexes a financial protocol, an unnoticed new finding on | ||
| # the default branch is exactly the failure mode you cannot detect by | ||
| # reviewing PRs. | ||
| # | ||
| # THIS WORKFLOW FILLS THAT GAP. It does not replace or duplicate the ci.yml | ||
| # job; both run, and they upload to the same SARIF stream. ci.yml remains | ||
| # owned by a parallel PR (#497 follow-on) which is responsible for pinning | ||
| # and least-privileging that job — see the "Known gaps" section of | ||
| # docs/STATIC_ANALYSIS.md. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| on: | ||
| schedule: | ||
| # Weekly, Monday 03:17 UTC. Deliberately off the hour: GitHub's shared | ||
| # scheduler queue is heavily oversubscribed on the hour, and a scheduled | ||
| # job that lands in that queue can be delayed by 10+ minutes or dropped. | ||
| - cron: '17 3 * * 1' | ||
| workflow_dispatch: | ||
| inputs: | ||
| target_ref: | ||
| description: 'Branch, tag, or SHA to analyze (defaults to the default branch)' | ||
| required: false | ||
| type: string | ||
|
|
||
| # Least privilege at the workflow level. `security-events: write` is the only | ||
| # permission CodeQL genuinely needs -- it is what allows the SARIF upload -- | ||
| # and `contents: read` is what allows checkout. Nothing here grants write | ||
| # access to the repository, packages, deployments, or issues. | ||
| permissions: | ||
| contents: read | ||
| security-events: write | ||
|
|
||
| # Never cancel an in-flight analysis. Two overlapping CodeQL runs uploading | ||
| # SARIF for the same ref race, and the loser's alerts can be dropped from the | ||
| # security view. Queueing is the correct behaviour for a scheduled job. | ||
| concurrency: | ||
| group: codeql-schedule-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| analyze: | ||
| name: Analyze JavaScript/TypeScript | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
|
|
||
| # Restated per-job, because a workflow-level block can be widened by a | ||
| # future edit and the job is what actually runs. | ||
| permissions: | ||
| contents: read | ||
| security-events: write | ||
|
|
||
| steps: | ||
| - name: Resolve target ref | ||
| id: target | ||
| env: | ||
| # Passed through the environment rather than interpolated directly | ||
| # into the shell, so a hostile ref string cannot become script. | ||
| INPUT_TARGET_REF: ${{ inputs.target_ref }} | ||
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | ||
| run: | | ||
| set -eu | ||
| ref="${INPUT_TARGET_REF:-${DEFAULT_BRANCH:-}}" | ||
| if [ -z "$ref" ]; then | ||
| echo "::error::No default branch available in the event payload; re-run with an explicit target_ref." | ||
| exit 1 | ||
| fi | ||
| echo "Analyzing ref: $ref" | ||
| echo "ref=$ref" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Checkout code | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ steps.target.outputs.ref }} | ||
|
Comment on lines
+83
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Upload results for the commit that was analyzed. If 🧰 Tools🪛 zizmor (1.30.0)[warning] 83-86: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) 🤖 Prompt for AI Agents |
||
|
|
||
| - name: Initialize CodeQL | ||
| uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 | ||
| with: | ||
| languages: javascript-typescript | ||
| config-file: ./.github/codeql/config.yml | ||
| # JavaScript/TypeScript is analysed from source; there is no | ||
| # compiler step to trace. `none` avoids invoking `npm ci` + | ||
| # `npm run build` inside the analysis job, which is both slow and a | ||
| # second, redundant copy of whatever `build-and-test` in ci.yml | ||
| # already gates on. It also means an unrelated build failure cannot | ||
| # mask or fake a static-analysis result. | ||
| build-mode: 'none' | ||
| # Analysis starts from a clean slate: a partial previous run's cache | ||
| # must never be able to suppress an alert. | ||
| clean: 'true' | ||
| # | ||
| # `fail-on-errors` is deliberately left at its default (true). That | ||
| # flag governs *analysis errors* — a malformed config file, an | ||
| # extractor crash, an upload failure — and those must fail the run | ||
| # loudly. A red scheduled job saying "the scanner broke" is a | ||
| # correct, actionable result. Alert counts are a separate question: | ||
| # this workflow does not turn new alerts into a build failure, | ||
| # because alert triage is a review process, not a compile step. | ||
|
|
||
| - name: Perform CodeQL Analysis | ||
| uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 | ||
| with: | ||
| category: '/language:javascript-typescript' | ||
| # Explicit rather than implicit, so that a future bump of the action | ||
| # cannot quietly flip the default and leave a green run with no | ||
| # SARIF in the security tab. | ||
| upload-sarif: 'true' | ||
| # Block until the results are committed to the security view. Without | ||
| # this the job can go green before the upload lands, and a | ||
| # "successful" scan whose alerts never appeared is worse than a | ||
| # visible failure. | ||
| wait-for-processing: true | ||
|
|
||
| - name: Summarise run | ||
| if: always() | ||
| env: | ||
| ANALYZED_REF: ${{ steps.target.outputs.ref }} | ||
| run: | | ||
| { | ||
| echo "### CodeQL scheduled analysis" | ||
| echo | ||
| echo "- **Ref analyzed:** \`${ANALYZED_REF}\`" | ||
| echo "- **Suites:** default + \`security-extended\` + \`security-and-quality\`" | ||
| echo "- **Config:** \`.github/codeql/config.yml\`" | ||
| echo "- **Trigger:** ${{ github.event_name }}" | ||
| echo | ||
| echo "Alert counts and per-alert detail are in the repository's" | ||
| echo "**Security → Code scanning** tab. Triage and suppression policy:" | ||
| echo "\`docs/STATIC_ANALYSIS.md\`." | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -22,13 +22,53 @@ FROM node:20-alpine AS runner | |||
| WORKDIR /app | ||||
| ENV NODE_ENV=production | ||||
|
|
||||
| # --------------------------------------------------------------------------- | ||||
| # #498 — runtime-stage hardening. Only this stage is modified by this change; | ||||
| # the builder stage above is owned by a separate PR (#516) so the two do not | ||||
| # conflict. See docs/CONTAINER_IMAGE.md for the full runtime contract. | ||||
| # --------------------------------------------------------------------------- | ||||
|
|
||||
| # Dedicated service account. A named user/group (rather than a bare numeric | ||||
| # UID) is used deliberately: it gives the account a real passwd entry, a | ||||
| # nologin shell, and an ownership bit that survives `docker exec` debugging. | ||||
| # UID/GID 1001 is chosen because it is in the unprivileged range and does not | ||||
| # collide with the `node` account baked into the base image. | ||||
| RUN addgroup -S -g 1001 truthbounty \ | ||||
| && adduser -S -u 1001 -G truthbounty -h /app -s /sbin/nologin truthbounty | ||||
|
|
||||
| # Only runtime artifacts cross the stage boundary: production dependencies, | ||||
| # the compiled bundle, and the generated Prisma client. No source, no config, | ||||
| # no test fixtures, no .env — .dockerignore guarantees none of those are even | ||||
| # present in the build context. | ||||
| COPY package*.json ./ | ||||
| COPY --from=builder /app/node_modules ./node_modules | ||||
| COPY --from=builder /app/dist ./dist | ||||
| COPY --from=builder /app/src/generated ./src/generated | ||||
|
|
||||
| # Hand the two writable-capable artifact trees to the service account. Applied | ||||
| # as a targeted RUN rather than `COPY --chown` so the stage stays mergeable | ||||
| # with the parallel builder-stage change and so the COPY lines remain exactly | ||||
| # as they were. | ||||
| RUN chown -R truthbounty:truthbounty /app/dist /app/src/generated | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Dockerfile (numbered, focused) ---'
cat -n Dockerfile | sed -n '1,90p'
printf '%s\n' '--- PR diff summary ---'
git diff --stat 5c4807b80d89cbce8e3413a83ad74126dda1edeb 3b029e9daf1e24d00bc2683772ba5102316db50d -- Dockerfile
printf '%s\n' '--- PR diff for Dockerfile ---'
git diff --no-ext-diff --unified=25 5c4807b80d89cbce8e3413a83ad74126dda1edeb 3b029e9daf1e24d00bc2683772ba5102316db50d -- Dockerfile
printf '%s\n' '--- direct references to the cited trees ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' '/app/(dist|src/generated)|src/generated|dist/' . || true
printf '%s\n' '--- Docker-related files ---'
git ls-files '*Docker*' '*docker*' '.dockerignore'Repository: DigiNodes/truthbounty-api Length of output: 19672 🤖 get_repo_knowledge executed:
Length of output: 942 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- container contract (focused) ---'
cat -n docs/CONTAINER_IMAGE.md | sed -n '1,190p'
printf '%s\n' '--- Dockerfile contract tests ---'
cat -n src/dockerfile.spec.ts | sed -n '1,180p'
printf '%s\n' '--- runtime entrypoint declarations ---'
cat -n package.json | sed -n '1,45p'Repository: DigiNodes/truthbounty-api Length of output: 16670 Security Misconfiguration Reachability: Internal Keep runtime application artifacts read-only to
Keep build artifacts root-owned- RUN chown -R truthbounty:truthbounty /app/dist /app/src/generated📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Learnings |
||||
|
|
||||
| # Drop root. Everything after this point runs unprivileged, including anything | ||||
| # a future layer might execute. | ||||
| USER truthbounty | ||||
|
|
||||
| # Expose port | ||||
| EXPOSE 3000 | ||||
|
|
||||
| # Liveness probe against the route the application actually serves: | ||||
| # `HealthController` is `@Controller('health')` + `@Public()` with | ||||
| # `@Get('live')` (src/health/health.controller.ts), and no global prefix is set | ||||
| # in src/main.ts — so the real path is GET /health/live. It is unauthenticated, | ||||
| # so no credential is baked into the image. Implemented with the interpreter | ||||
| # already present in the image rather than a wget/curl dependency. | ||||
| # Deliberately NOT /health/ready: that endpoint fails closed when Postgres, | ||||
| # the job queue, or the indexer is down, which would make Docker kill a | ||||
| # container that is alive but correctly refusing traffic. | ||||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ | ||||
| CMD node -e "require('http').get({host:'127.0.0.1',port:process.env.PORT||3000,path:'/health/live',timeout:4000},r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))" | ||||
|
|
||||
| # Start app | ||||
| CMD ["npm", "run", "start:prod"] | ||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Connect the CI scan before calling this a shared configuration.
The supplied
.github/workflows/ci.ymlinitializes CodeQL withoutconfig-file. Its PR and push scans therefore do not load these query suites or path settings. Add this configuration to that job, or state that only the scheduled scan uses it. GitHub documentsconfig-fileas the input that loads a custom configuration. (docs.github.com)🤖 Prompt for AI Agents