diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8e90a21..9042830 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -229,10 +229,8 @@ jobs: # 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. # - # 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. + # CRITICAL/HIGH only: everything here comes from the base layer, and the remedy is the + # same at any severity — refresh the base image. env: TRIVY_IMAGE: aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f IMAGE_REF: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.merge.outputs.build_tag }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index d8bae68..94d43e9 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -63,11 +63,9 @@ jobs: # 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 - # 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. + # `-w /src` is required for that: Trivy resolves the default ignore file relative to the + # working directory. Do not switch to `--ignorefile` — it exits FATAL when the file is + # absent, breaking every run until someone adds a waiver. run: | docker run --rm \ -v "${{ github.workspace }}:/src:ro" \ @@ -100,9 +98,8 @@ jobs: # 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 - # the supply-chain surface — on this repository it hides 8 of 37 findings. + # `--include-dev-deps` because a compromised build-time dependency executes on the runner + # and can alter the bundle; Trivy's pnpm default hides them. run: | set -euo pipefail docker run --rm \ diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index a4df768..b07290f 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -92,13 +92,24 @@ jobs: [ -f trufflehog-findings.jsonl ] || exit 0 python3 - trufflehog-findings.jsonl "${GITHUB_STEP_SUMMARY:-/dev/null}" <<'PY' import json, sys, collections - rows = [] + rows, unparsable, skipped = [], 0, 0 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) + try: + d = json.loads(line) + except json.JSONDecodeError: + # A truncated or non-JSON line must not take the whole summary down with + # it; it is counted and reported instead. + unparsable += 1 + continue + if "DetectorName" not in d: + # Only findings carry a detector. Anything else on stdout is not a finding + # and must not inflate the count. + skipped += 1 + continue git = (d.get("SourceMetadata") or {}).get("Data", {}).get("Git", {}) rows.append({ "detector": d.get("DetectorName", "?"), @@ -110,6 +121,8 @@ jobs: out = open(sys.argv[2], "a", encoding="utf-8") w = out.write w("## TruffleHog secret scan\n\n") + if unparsable or skipped: + w(f"_{unparsable} unparsable line(s), {skipped} non-finding record(s) skipped._\n\n") if not rows: w("No secrets detected in the scanned history (all result kinds enabled).\n") sys.exit(0) @@ -129,8 +142,7 @@ jobs: # 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. + # comments, where they arrive more often than in code. # # Off pull requests because it depends on API availability and rate limits, and a transient # outage should not colour a pull request at all. @@ -141,13 +153,12 @@ jobs: 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 }} + # Deliberately unauthenticated, and it must stay that way: TruffleHog's GitHub source + # calls `GET /user`, which an installation token cannot read — passing `GITHUB_TOKEN` + # fails the job with 403. The repository is public, so no token is needed. run: | set -euo pipefail docker run --rm \ - -e GITHUB_TOKEN \ trufflesecurity/trufflehog:3.96.0@sha256:aa821cf4ace8861c7d096d83818cdf7bb9719028a52d37a52eaad44086a52577 \ github \ --repo "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ @@ -165,13 +176,24 @@ jobs: [ -f trufflehog-api-findings.jsonl ] || exit 0 python3 - trufflehog-api-findings.jsonl "${GITHUB_STEP_SUMMARY:-/dev/null}" <<'PY' import json, sys, collections - rows = [] + rows, unparsable, skipped = [], 0, 0 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) + try: + d = json.loads(line) + except json.JSONDecodeError: + # A truncated or non-JSON line must not take the whole summary down with + # it; it is counted and reported instead. + unparsable += 1 + continue + if "DetectorName" not in d: + # Only findings carry a detector. Anything else on stdout is not a finding + # and must not inflate the count. + skipped += 1 + continue 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 {} @@ -184,6 +206,8 @@ jobs: out = open(sys.argv[2], "a", encoding="utf-8") w = out.write w("## TruffleHog — GitHub API source (report-only)\n\n") + if unparsable or skipped: + w(f"_{unparsable} unparsable line(s), {skipped} non-finding record(s) skipped._\n\n") if not rows: w("No secrets detected in commits, pull request and issue comments, or wikis " "(all result kinds enabled).\n")