From cd7214d3efabd8943b1359190d3e21474f5b980c Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 09:36:37 +0200 Subject: [PATCH 1/4] ci(security): honor repo-root .trivyignore, guard SARIF upload, scan once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports three review findings from constructorfabric/insight#2002 to this repo, so the two scanning setups do not drift. The repo-root `.trivyignore` documented on the gate job was never read. Trivy resolves the default ignore file relative to the working directory, and the containers had no `-w`, so waivers were looked for in the image's own cwd. Verified: with a waiver present the gate now honors it where it previously did not, and with no `.trivyignore` at all — the current state of this repo — the gate is unaffected. `--ignorefile` is deliberately not used: Trivy exits FATAL when the named file is absent, which would break every run until someone adds a waiver. SARIF upload steps ran under `always()` against files a failed scan never wrote, producing a second error pointing at Code Scanning instead of the scan that actually broke. A `hashFiles` guard drops that. Both report passes scanned the same target twice to produce two formats. The scan now runs once to JSON and `trivy convert` derives the rest — SARIF for the repository pass, and both the log table and SARIF for the image scan. Output is identical: 31 results for the repository pass and 37 for the image, matching what main currently reports. Signed-off-by: Grigoriy Gogin --- .github/workflows/docker.yml | 30 +++++++++++++++--------------- .github/workflows/trivy.yml | 30 ++++++++++++++++++------------ 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8ce5d1f..bbc0491 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -220,25 +220,19 @@ jobs: IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.merge.outputs.build_tag }} TRIVY_USERNAME: ${{ github.actor }} TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # One pull and one analysis: `trivy convert` renders both the log table and the SARIF + # from a single JSON report instead of scanning twice. `-w /work` so a repo-root + # `.trivyignore` is resolved — Trivy reads the default ignore file relative to the + # working directory, and without this the file is looked for in the image's own cwd + # and silently ignored. `--ignorefile` is not used instead: Trivy exits FATAL when the + # named file is absent, which would break every run until someone adds a waiver. run: | set -euo pipefail echo "Scanning ${IMAGE_REF}" - # Two passes over the same warm DB cache: a readable table in the log, SARIF for - # Code Scanning. - docker run --rm \ - -v /tmp/trivy-cache:/root/.cache \ - -e TRIVY_USERNAME -e TRIVY_PASSWORD \ - "$TRIVY_IMAGE" image \ - --severity CRITICAL,HIGH \ - --pkg-types os,library \ - --ignore-unfixed \ - --exit-code 0 \ - --no-progress \ - --format table \ - "$IMAGE_REF" docker run --rm \ -v "${{ github.workspace }}:/work" \ -v /tmp/trivy-cache:/root/.cache \ + -w /work \ -e TRIVY_USERNAME -e TRIVY_PASSWORD \ "$TRIVY_IMAGE" image \ --severity CRITICAL,HIGH \ @@ -246,11 +240,17 @@ jobs: --ignore-unfixed \ --exit-code 0 \ --no-progress \ - --format sarif --output /work/trivy-image.sarif \ + --format json --output /work/trivy-image.json \ "$IMAGE_REF" + docker run --rm -v "${{ github.workspace }}:/work" \ + "$TRIVY_IMAGE" convert --format table /work/trivy-image.json + docker run --rm -v "${{ github.workspace }}:/work" \ + "$TRIVY_IMAGE" convert --format sarif --output /work/trivy-image.sarif /work/trivy-image.json - name: Upload SARIF to GitHub Code Scanning - if: always() + # The guard keeps a failed registry pull from reporting twice — once for the pull, once + # for a SARIF file that was never written. + if: ${{ always() && hashFiles('trivy-image.sarif') != '' }} uses: github/codeql-action/upload-sarif@fb0994ef1c058010acf1efccff928b0a83b1ed54 # v4.32.6 with: sarif_file: trivy-image.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 5e42337..8249826 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -59,9 +59,16 @@ jobs: # be resolved by the PR author, so it belongs in the report pass, not in a merge block. # Waivers go in `.trivyignore` at the repo root, date-scoped: # CVE-2026-12345 exp:2026-12-31 # tracked in , fix queued + # + # `-w /src` is load-bearing for that: Trivy resolves the default ignore file relative to + # the working directory, so without it a repo-root `.trivyignore` is read from the + # image's own cwd and silently ignored. `--ignorefile` is not used instead because Trivy + # exits FATAL when the named file does not exist, which would break every run until + # someone adds a waiver. run: | docker run --rm \ -v "${{ github.workspace }}:/src:ro" \ + -w /src \ "$TRIVY_IMAGE" fs \ --scanners vuln,secret,misconfig \ --severity CRITICAL \ @@ -83,30 +90,27 @@ jobs: persist-credentials: false - name: Trivy fs scan — HIGH/MEDIUM/LOW (never blocks) - # Two output formats from two runs over the same cached DB: SARIF for Code Scanning, - # JSON for the summary table below. `--exit-code 0` on both — this pass only reports. + # One scan, two formats: JSON feeds the summary below, and `trivy convert` derives the + # SARIF from that same JSON instead of rescanning. `-w /src` for the ignore-file reason + # documented on the gate job. `--exit-code 0` — this pass only reports. run: | set -euo pipefail docker run --rm \ -v "${{ github.workspace }}:/src" \ -v /tmp/trivy-cache:/root/.cache \ + -w /src \ "$TRIVY_IMAGE" fs \ --scanners vuln,misconfig \ --severity HIGH,MEDIUM,LOW \ --exit-code 0 \ --no-progress \ - --format sarif --output /src/trivy-fs.sarif \ + --format json --output /src/trivy-fs.json \ /src docker run --rm \ -v "${{ github.workspace }}:/src" \ - -v /tmp/trivy-cache:/root/.cache \ - "$TRIVY_IMAGE" fs \ - --scanners vuln,misconfig \ - --severity HIGH,MEDIUM,LOW \ - --exit-code 0 \ - --no-progress \ - --format json --output /src/trivy-fs.json \ - /src + "$TRIVY_IMAGE" convert \ + --format sarif --output /src/trivy-fs.sarif \ + /src/trivy-fs.json - name: Summarize findings in the job summary if: always() @@ -162,7 +166,9 @@ jobs: - name: Upload SARIF to GitHub Code Scanning # Skip on fork PRs: read-only token cannot upload, which would red-X this report-only job. - if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + # The hashFiles guard keeps a failed scan from producing a second, misleading error that + # points at Code Scanning rather than at the scan that actually broke. + if: ${{ always() && hashFiles('trivy-fs.sarif') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: github/codeql-action/upload-sarif@fb0994ef1c058010acf1efccff928b0a83b1ed54 # v4.32.6 with: sarif_file: trivy-fs.sarif From b156a3bdce5c69223b6082b64cb0b388ed6698ae Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 10:39:00 +0200 Subject: [PATCH 2/4] ci(security): report every severity, dev deps, comments and orphaned objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widens what the scans report. Nothing here changes what blocks a merge, and no finding is fixed — this is about the report being complete. Four gaps, each measured on this repository: - The repository report pass filtered to HIGH/MEDIUM/LOW while the gate, which covers CRITICAL, only prints a table. A CRITICAL therefore failed the check and never became an alert. CRITICAL is now in the report severities too; reporting and blocking are separate concerns. - Dev dependencies were excluded by Trivy's pnpm default. They do not ship, but a compromised one executes on the runner and can alter the bundle. Including them takes the repository pass from 29 to 37 vulnerabilities. - The image scan filtered to CRITICAL/HIGH, hiding 70 of 107 findings in the image that actually ships. - Secrets were only scanned through git refs. A new report-only job uses the GitHub API source, which additionally sees pull request and issue comments, wikis, and objects orphaned by a force push or a deleted branch — all unreachable from any ref and therefore invisible to the git scan. Measured: ~8600 chunks against the git scan's ~6900, ten seconds. Off pull requests and non-blocking, since it depends on API availability. semgrep.yml gains a push trigger for main. Without it Code Scanning holds no SAST data for the default branch between a merge and the 03:27 nightly run, so a merge that introduces a finding stays invisible until the next morning. Deliberately not added: extra Semgrep rule packs (p/javascript, p/typescript, p/react, p/owasp-top-ten). Measured on this repository they raise the rule count from 525 to 564 and the finding count from 19 to 20 — `--config auto` already selects what matters here. Signed-off-by: Grigoriy Gogin --- .github/workflows/docker.yml | 7 ++- .github/workflows/semgrep.yml | 5 +++ .github/workflows/trivy.yml | 40 ++++++++++------- .github/workflows/trufflehog.yml | 76 ++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bbc0491..ab2f733 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -209,12 +209,15 @@ jobs: with: persist-credentials: false - - name: Trivy image scan (CRITICAL/HIGH) + - name: Trivy image scan (all severities) # Trivy pulls straight from the registry (TRIVY_USERNAME/TRIVY_PASSWORD) rather than # via a mounted docker socket, so no daemon and no local `docker pull` is needed. # `--ignore-unfixed` keeps the output to what a base-image bump can actually fix. # A multi-arch manifest resolves to the runner's platform (linux/amd64); the arm64 # leg shares the same base image and package set, so one pass is representative. + # + # Every severity, not just CRITICAL/HIGH: this job is the only inventory of what actually + # ships, and on the current image the narrower filter hides 70 of 107 findings. env: TRIVY_IMAGE: aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.merge.outputs.build_tag }} @@ -235,7 +238,7 @@ jobs: -w /work \ -e TRIVY_USERNAME -e TRIVY_PASSWORD \ "$TRIVY_IMAGE" image \ - --severity CRITICAL,HIGH \ + --severity CRITICAL,HIGH,MEDIUM,LOW \ --pkg-types os,library \ --ignore-unfixed \ --exit-code 0 \ diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 06ca330..b149e3d 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -18,6 +18,11 @@ name: Semgrep SAST on: pull_request: branches: [main] + push: + # A main-branch baseline the moment something lands, rather than only at 03:27 the next + # morning: without this, `branch:main` in Code Scanning carries no SAST data between a merge + # and the nightly run, and a merge that introduces a finding is invisible until then. + branches: [main] schedule: # Nightly full-tree baseline (independent of what any PR touched), 03:27 UTC. - cron: "27 3 * * *" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 8249826..76b368b 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -6,9 +6,9 @@ name: Trivy SCA # gate — CRITICAL only, blocks. Stop-the-line: a CRITICAL CVE or an embedded secret in a # dependency manifest must be fixed or waived in `.trivyignore` before merge. # Baseline today: 0 CRITICAL, so the gate starts green. -# report — HIGH/MEDIUM/LOW, never blocks. Findings land in "Security -> Code scanning" -# (SARIF) plus a job-summary table. Baseline today: 10 HIGH / 16 MEDIUM / 3 LOW, -# almost all from build-time tooling declared under `dependencies`. +# report — every severity, never blocks. Findings land in "Security -> Code scanning" +# (SARIF) plus a job-summary table. Baseline today: 37 vulnerabilities and 2 +# misconfigurations, most of them build-time tooling declared under `dependencies`. # # Scope: this scans the repository (`trivy fs`) — lock file, Dockerfile, configs. It does NOT # see the image that ships: base-image and OS-package CVEs are covered by the `trivy-image` @@ -19,8 +19,9 @@ name: Trivy SCA # the surrounding match. Secrets stay in the blocking gate, where TruffleHog is the primary # owner anyway (trufflehog.yml). # -# Dev dependencies are excluded by Trivy's pnpm default — they are not in the shipped bundle. -# Add `--include-dev-deps` if we ever want build-time supply-chain coverage here too. +# Dev dependencies are included (`--include-dev-deps`). They are not in the shipped bundle, but a +# compromised one executes on the runner during the build, so leaving them out understates the +# supply-chain surface rather than simplifying it. on: pull_request: @@ -78,7 +79,7 @@ jobs: /src report: - name: report (HIGH/MEDIUM/LOW) + name: report (all severities) runs-on: ubuntu-latest timeout-minutes: 15 permissions: @@ -89,10 +90,18 @@ jobs: with: persist-credentials: false - - name: Trivy fs scan — HIGH/MEDIUM/LOW (never blocks) + - name: Trivy fs scan — all severities, dev deps included (never blocks) # One scan, two formats: JSON feeds the summary below, and `trivy convert` derives the # SARIF from that same JSON instead of rescanning. `-w /src` for the ignore-file reason # documented on the gate job. `--exit-code 0` — this pass only reports. + # + # CRITICAL is in the severity list even though the gate already blocks on it: the gate + # only prints a table, so without this a CRITICAL would fail the check and never appear + # as an alert. Reporting and blocking are separate concerns. + # + # `--include-dev-deps` because a compromised build-time dependency executes on the + # runner and can alter the bundle. Trivy's pnpm default hides them, which understates + # the supply-chain surface — on this repository it hides 8 of 37 findings. run: | set -euo pipefail docker run --rm \ @@ -101,7 +110,8 @@ jobs: -w /src \ "$TRIVY_IMAGE" fs \ --scanners vuln,misconfig \ - --severity HIGH,MEDIUM,LOW \ + --severity CRITICAL,HIGH,MEDIUM,LOW \ + --include-dev-deps \ --exit-code 0 \ --no-progress \ --format json --output /src/trivy-fs.json \ @@ -136,24 +146,24 @@ jobs: vulns += r.get("Vulnerabilities") or [] miscfg += [(r.get("Target"), m) for m in (r.get("Misconfigurations") or [])] w("## Trivy SCA (report-only)\n\n") - w(f"**{len(vulns)}** vulnerabilit(ies) + **{len(miscfg)}** misconfiguration(s) at " - f"HIGH/MEDIUM/LOW. CRITICAL is handled by the blocking `gate` job. " - f"Full details in [Security -> Code scanning]({cs_url}).\n\n") + w(f"**{len(vulns)}** vulnerabilit(ies) + **{len(miscfg)}** misconfiguration(s), every " + f"severity, production and development dependencies. The blocking `gate` job covers " + f"CRITICAL separately. Full details in [Security -> Code scanning]({cs_url}).\n\n") if vulns: sev = collections.Counter(v["Severity"] for v in vulns) w("| Severity | Count |\n|---|---:|\n") - for s in ("HIGH", "MEDIUM", "LOW"): + for s in ("CRITICAL", "HIGH", "MEDIUM", "LOW"): if sev.get(s): w(f"| {s} | {sev[s]} |\n") # One row per (package, version): that is what an upgrade actually fixes. bundles = collections.defaultdict(list) for v in vulns: bundles[(v["PkgName"], v.get("InstalledVersion", "?"))].append(v) - rank = {"HIGH": 0, "MEDIUM": 1, "LOW": 2} + rank = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3} w("\n
By package\n\n| Package | Max | CVEs | Fixed in |\n|---|---|---:|---|\n") for (pkg, ver), items in sorted( - bundles.items(), key=lambda kv: (min(rank.get(i["Severity"], 3) for i in kv[1]), kv[0])): - worst = min(items, key=lambda i: rank.get(i["Severity"], 3))["Severity"] + bundles.items(), key=lambda kv: (min(rank.get(i["Severity"], 4) for i in kv[1]), kv[0])): + worst = min(items, key=lambda i: rank.get(i["Severity"], 4))["Severity"] fixes = sorted({i.get("FixedVersion") or "-" for i in items}) w(f"| `{pkg}@{ver}` | {worst} | {len(items)} | {', '.join(fixes)} |\n") w("\n
\n") diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index 54e87f8..f28035f 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -124,3 +124,79 @@ jobs: out.close() sys.exit(1) PY + + # The git scan above only reaches commits that some ref still points at. Two classes of leak + # sit outside it, and only the GitHub API can see them: objects orphaned by a force push or a + # deleted branch, which remain fetchable by SHA, and secrets pasted into pull request or issue + # comments, where they arrive more often than in code. On this repository the API source reads + # ~8600 chunks against the git scan's ~6900, and takes about ten seconds. + # + # Report-only, and off pull requests: it depends on API availability and rate limits, so a + # transient failure must not block a merge. The blocking gate stays the git-based diff scan. + secrets-api: + name: secrets (GitHub API) + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: TruffleHog scan (GitHub API — comments, wikis, unreachable objects) + env: + # The auto-injected token is enough for a public repository's commits and comments. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + docker run --rm \ + -e GITHUB_TOKEN \ + trufflesecurity/trufflehog:3.96.0@sha256:aa821cf4ace8861c7d096d83818cdf7bb9719028a52d37a52eaad44086a52577 \ + github \ + --repo "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ + --issue-comments \ + --pr-comments \ + --include-wikis \ + --json \ + --no-update \ + --results=verified,unknown,unverified,filtered_unverified \ + > trufflehog-api-findings.jsonl + + - name: Summarize (redacted) — report only + if: always() + run: | + [ -f trufflehog-api-findings.jsonl ] || exit 0 + python3 - trufflehog-api-findings.jsonl "${GITHUB_STEP_SUMMARY:-/dev/null}" <<'PY' + import json, sys, collections + rows = [] + with open(sys.argv[1], encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if not line: + continue + d = json.loads(line) + meta = (d.get("SourceMetadata") or {}).get("Data", {}) + # The GitHub source reports under Github for commits and comments alike. + loc = meta.get("Github") or meta.get("Git") or {} + rows.append({ + "detector": d.get("DetectorName", "?"), + "verified": bool(d.get("Verified")), + # A comment hit carries a link instead of a path. + "where": loc.get("file") or loc.get("link") or "?", + }) + out = open(sys.argv[2], "a", encoding="utf-8") + w = out.write + w("## TruffleHog — GitHub API source (report-only)\n\n") + if not rows: + w("No secrets detected in commits, pull request and issue comments, or wikis " + "(all result kinds enabled).\n") + sys.exit(0) + ver = sum(1 for r in rows if r["verified"]) + w(f"**{len(rows)}** finding(s) — {ver} live-verified, {len(rows) - ver} unverified. " + "Values are redacted; open the linked location to read them.\n\n") + w("| Detector | Verified | Where |\n|---|---|---|\n") + for r in sorted(rows, key=lambda r: (not r["verified"], r["detector"])): + w(f"| `{r['detector']}` | {'yes' if r['verified'] else 'no'} | `{r['where']}` |\n") + by_det = collections.Counter(r["detector"] for r in rows) + w(f"\nBy detector: {', '.join(f'{k} x{v}' for k, v in by_det.most_common())}\n") + w("\nA hit here that the git scan did not report means the value lives outside the " + "current refs — an orphaned commit or a comment. **Rotate it; deleting the comment " + "does not.**\n") + out.close() + PY From 7f7382846b58be99e1ee8aaf831e68adaf103079 Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 12:09:59 +0200 Subject: [PATCH 3/4] =?UTF-8?q?ci(security):=20report=20only=20=E2=80=94?= =?UTF-8?q?=20no=20security=20check=20blocks=20a=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scan here becomes observational. Two checks blocked until now: the Trivy CRITICAL pass and the TruffleHog history scan. Both keep running, keep reporting, and stop deciding whether a branch can merge. - Trivy: `--exit-code 1` becomes `--exit-code 0`. The pass is renamed from `gate (CRITICAL)` to `critical (report-only)` — a check named "gate" that gates nothing is worse than no check at all. - TruffleHog: the summary step no longer exits non-zero on a finding. It still renders the redacted table and still says to rotate. Comments follow the behaviour: nothing in either file claims to block any more, and where a waiver mechanism only matters under enforcement, the comment says so. Baselines are unchanged by this: 0 CRITICAL and 0 secrets. Nothing was passing because of these gates, so nothing regresses by removing them — what changes is that a future finding will be reported rather than enforced. Turning enforcement back on is prepared separately and deliberately not merged. Refs #231 Signed-off-by: Grigoriy Gogin --- .github/workflows/trivy.yml | 36 ++++++++++++++++---------------- .github/workflows/trufflehog.yml | 23 ++++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 76b368b..d8bae68 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -3,9 +3,11 @@ name: Trivy SCA # Software-composition analysis over the source tree (constructorfabric/insight#1478). # Two passes, matching the org's GitLab trivy-scan component: # -# gate — CRITICAL only, blocks. Stop-the-line: a CRITICAL CVE or an embedded secret in a -# dependency manifest must be fixed or waived in `.trivyignore` before merge. -# Baseline today: 0 CRITICAL, so the gate starts green. +# critical — CRITICAL only, and the one pass that also runs the secret scanner. Report-only +# (`--exit-code 0`), like everything else here: a CRITICAL surfaces in the log without +# stopping a merge. Kept as a separate pass because it is the only place secrets are +# scanned, and those must never reach a SARIF upload on a public repository. +# Baseline today: 0 CRITICAL. # report — every severity, never blocks. Findings land in "Security -> Code scanning" # (SARIF) plus a job-summary table. Baseline today: 37 vulnerabilities and 2 # misconfigurations, most of them build-time tooling declared under `dependencies`. @@ -16,8 +18,8 @@ name: Trivy SCA # # The report pass deliberately drops the secret scanner (`--scanners vuln,misconfig`): Code # Scanning alerts on a public repository are world-readable, and Trivy's secret findings quote -# the surrounding match. Secrets stay in the blocking gate, where TruffleHog is the primary -# owner anyway (trufflehog.yml). +# the surrounding match. Secrets stay in the `critical` pass, whose output goes to the log only, +# and TruffleHog owns the domain anyway (trufflehog.yml). # # Dev dependencies are included (`--include-dev-deps`). They are not in the shipped bundle, but a # compromised one executes on the runner during the build, so leaving them out understates the @@ -47,7 +49,7 @@ env: jobs: gate: - name: gate (CRITICAL) + name: critical (report-only) runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -55,10 +57,10 @@ jobs: with: persist-credentials: false - - name: Trivy fs scan — CRITICAL only (blocking) - # `--ignore-unfixed` keeps the gate actionable: a CRITICAL with no released fix cannot - # be resolved by the PR author, so it belongs in the report pass, not in a merge block. - # Waivers go in `.trivyignore` at the repo root, date-scoped: + - name: Trivy fs scan — CRITICAL only (report-only) + # `--ignore-unfixed` keeps the output actionable: a CRITICAL with no released fix cannot + # be resolved by the PR author. Waivers go in `.trivyignore` at the repo root, + # date-scoped — they matter again the moment this pass becomes blocking: # CVE-2026-12345 exp:2026-12-31 # tracked in , fix queued # # `-w /src` is load-bearing for that: Trivy resolves the default ignore file relative to @@ -74,7 +76,7 @@ jobs: --scanners vuln,secret,misconfig \ --severity CRITICAL \ --ignore-unfixed \ - --exit-code 1 \ + --exit-code 0 \ --no-progress \ /src @@ -90,14 +92,13 @@ jobs: with: persist-credentials: false - - name: Trivy fs scan — all severities, dev deps included (never blocks) + - name: Trivy fs scan — all severities, dev deps included # One scan, two formats: JSON feeds the summary below, and `trivy convert` derives the # SARIF from that same JSON instead of rescanning. `-w /src` for the ignore-file reason - # documented on the gate job. `--exit-code 0` — this pass only reports. + # documented on the `critical` pass. `--exit-code 0` — nothing here blocks. # - # CRITICAL is in the severity list even though the gate already blocks on it: the gate - # only prints a table, so without this a CRITICAL would fail the check and never appear - # as an alert. Reporting and blocking are separate concerns. + # CRITICAL is in the severity list even though the `critical` pass covers it: that pass + # only prints a table, so without this a CRITICAL would never appear as an alert. # # `--include-dev-deps` because a compromised build-time dependency executes on the # runner and can alter the bundle. Trivy's pnpm default hides them, which understates @@ -147,8 +148,7 @@ jobs: miscfg += [(r.get("Target"), m) for m in (r.get("Misconfigurations") or [])] w("## Trivy SCA (report-only)\n\n") w(f"**{len(vulns)}** vulnerabilit(ies) + **{len(miscfg)}** misconfiguration(s), every " - f"severity, production and development dependencies. The blocking `gate` job covers " - f"CRITICAL separately. Full details in [Security -> Code scanning]({cs_url}).\n\n") + f"severity, production and development dependencies. Nothing here blocks a merge. Full details in [Security -> Code scanning]({cs_url}).\n\n") if vulns: sev = collections.Counter(v["Severity"] for v in vulns) w("| Severity | Count |\n|---|---:|\n") diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index f28035f..a4df768 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -1,20 +1,22 @@ name: TruffleHog Secrets -# Blocking secret-detection gate (constructorfabric/insight#1478). +# Secret detection over the full git history (constructorfabric/insight-front#231). # -# Unlike the Semgrep and Trivy gates this one is BLOCKING from day one: the baseline is -# provably empty (full-history scan over every ref: 0 findings, 0 detector hits), so there -# is nothing to triage and no reason to run in observation mode. Zero-leak policy — a -# committed credential must be revoked and rotated, and rewriting history is not enough. +# Report-only, like every other security check here: findings render in the job summary and the +# run stays green. The baseline is empty — a full-history scan over all 62 refs returns 0 +# findings and 0 detector hits — so there is nothing to triage, and making this blocking is a +# one-line change tracked separately. Until then the zero-leak policy is a convention, not an +# enforced one: a committed credential must be revoked and rotated, and rewriting history is +# not remediation. # # `--results` is the load-bearing flag. TruffleHog's default (`verified,unknown`) HIDES # findings whose live verification failed, so an already-revoked or inactive key in the # history produces a green run — for a public repository that is still a leak. All four -# result kinds are requested so the gate sees what the default would drop. +# result kinds are requested so the scan sees what the default would drop. # # Raw secret values are never printed to the log or the job summary, and no report artifact # is uploaded: Actions artifacts and logs on a public repository are world-readable, so -# echoing a hit would publish the very value the gate exists to protect. The summary carries +# echoing a hit would publish the very value this scan exists to surface. The summary carries # detector name, commit and path — enough to locate and rotate. # # Scope note: this scans commits reachable from refs. Objects left unreachable by a force @@ -82,7 +84,7 @@ jobs: --results=verified,unknown,unverified,filtered_unverified \ > trufflehog-findings.jsonl - - name: Summarize (redacted) and fail on any finding + - name: Summarize (redacted) if: always() # Null-guarded append (repo convention, cf. ci.yml). Prints detector / commit / path # only — never `Raw`, `RawV2` or the surrounding line. @@ -122,7 +124,6 @@ jobs: w("\n**Revoke and rotate every credential listed above.** Removing the commit is not " "remediation — assume the value is compromised the moment it was pushed.\n") out.close() - sys.exit(1) PY # The git scan above only reaches commits that some ref still points at. Two classes of leak @@ -131,8 +132,8 @@ jobs: # comments, where they arrive more often than in code. On this repository the API source reads # ~8600 chunks against the git scan's ~6900, and takes about ten seconds. # - # Report-only, and off pull requests: it depends on API availability and rate limits, so a - # transient failure must not block a merge. The blocking gate stays the git-based diff scan. + # Off pull requests because it depends on API availability and rate limits, and a transient + # outage should not colour a pull request at all. secrets-api: name: secrets (GitHub API) if: github.event_name != 'pull_request' From cb86614786e00b1d468106d77dc3d46985375d96 Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 12:42:10 +0200 Subject: [PATCH 4/4] ci(security): keep the image scan at CRITICAL/HIGH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review remark from @cyberantonz on constructorfabric/insight#2016, applied here too since the same widening had landed in this workflow. Measured before reverting: on a backend connector image the filter change takes one image from 99 findings to 315, and across that estate from ~660 to ~2100. On this repository's image it was 37 to 107. The volume buys nothing. Every finding in an image comes from a base layer, and the remedy is identical at every severity — refresh the base image. What the wider filter does change is that the CRITICALs, which are the ones driving that refresh, end up buried under three times as many MEDIUM and LOW entries. Kept where it does pay: the repository pass stays at every severity. There the volume is small (37 findings) and each one points at a dependency this repository declares and can bump on its own. Refs #231 Signed-off-by: Grigoriy Gogin --- .github/workflows/docker.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ab2f733..7107d46 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -209,15 +209,17 @@ jobs: with: persist-credentials: false - - name: Trivy image scan (all severities) + - name: Trivy image scan (CRITICAL/HIGH) # Trivy pulls straight from the registry (TRIVY_USERNAME/TRIVY_PASSWORD) rather than # via a mounted docker socket, so no daemon and no local `docker pull` is needed. # `--ignore-unfixed` keeps the output to what a base-image bump can actually fix. # A multi-arch manifest resolves to the runner's platform (linux/amd64); the arm64 # leg shares the same base image and package set, so one pass is representative. # - # Every severity, not just CRITICAL/HIGH: this job is the only inventory of what actually - # ships, and on the current image the narrower filter hides 70 of 107 findings. + # CRITICAL/HIGH only. Widening to MEDIUM/LOW was tried and reverted on review + # (constructorfabric/insight#2016): every finding here comes from the base layer and the + # remedy is the same either way — refresh the base image — while the wider filter triples + # the alert count and buries the CRITICALs that drive action. env: TRIVY_IMAGE: aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.merge.outputs.build_tag }} @@ -238,7 +240,7 @@ jobs: -w /work \ -e TRIVY_USERNAME -e TRIVY_PASSWORD \ "$TRIVY_IMAGE" image \ - --severity CRITICAL,HIGH,MEDIUM,LOW \ + --severity CRITICAL,HIGH \ --pkg-types os,library \ --ignore-unfixed \ --exit-code 0 \