From 77ddaf7b66ffd7187b4ba8de1985f76a7bca58f4 Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 14:16:36 +0200 Subject: [PATCH 1/2] ci(security): run the API secret scan unauthenticated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the job that failed on main right after #230 merged: `403 Resource not accessible by integration` from `GET /user`. TruffleHog's GitHub source calls `/user` to identify whose token it holds, and an installation token cannot read that endpoint. There is no flag to skip the call, so `GITHUB_TOKEN` is unusable for this source. This repository is public, so the scan needs no token — measured unauthenticated: 8651 chunks in 8 seconds, the same coverage the token-authenticated run produced, and 0 findings either way. Caught by running it for real rather than by review: on pull requests this job is skipped by design, so the first execution was the push to main. Refs #231 Signed-off-by: Grigoriy Gogin --- .github/workflows/trufflehog.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/trufflehog.yml b/.github/workflows/trufflehog.yml index a4df768..14c0e71 100644 --- a/.github/workflows/trufflehog.yml +++ b/.github/workflows/trufflehog.yml @@ -141,13 +141,14 @@ 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. `GITHUB_TOKEN` cannot be used here: the GitHub source + # calls `GET /user` to identify the token owner, and an installation token gets + # `403 Resource not accessible by integration` — which is exactly how this job failed on + # its first real run on main. There is no flag to skip that call. This repository is + # public, so no token is needed — measured unauthenticated: 8651 chunks in 8 seconds. run: | set -euo pipefail docker run --rm \ - -e GITHUB_TOKEN \ trufflesecurity/trufflehog:3.96.0@sha256:aa821cf4ace8861c7d096d83818cdf7bb9719028a52d37a52eaad44086a52577 \ github \ --repo "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \ From 3ff478c53cbad964ddaa3f1d844df090139dbc0e Mon Sep 17 00:00:00 2001 From: Grigoriy Gogin Date: Wed, 29 Jul 2026 14:30:16 +0200 Subject: [PATCH 2/2] ci(security): guard the JSONL parse, trim rationale out of comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carries @cyberantonz's second review pass from constructorfabric/insight#2016 over to this repository, where the same code runs. The summary steps parsed every line with a bare `json.loads`, so one truncated line would kill the report on a job that exists to report. Parsing is now guarded: an unparsable line is counted, and a record without `DetectorName` is not a finding and no longer inflates the count. Both counters print when non-zero, so nothing is dropped silently. Verified on a file with two real findings, one truncated line and one valid non-finding record: exit 0, "1 unparsable line(s), 1 non-finding record(s) skipped", 2 findings reported. Comments lost the rationale and the measurements that would rot — why MEDIUM/LOW was reverted, chunk counts, finding counts. What stayed is compressed to the invariant a maintainer must not break: keep this scan unauthenticated, do not switch to `--ignorefile`. Refs #231 Signed-off-by: Grigoriy Gogin --- .github/workflows/docker.yml | 6 ++--- .github/workflows/trivy.yml | 13 ++++----- .github/workflows/trufflehog.yml | 45 ++++++++++++++++++++++++-------- 3 files changed, 41 insertions(+), 23 deletions(-) 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 14c0e71..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,11 +153,9 @@ jobs: timeout-minutes: 20 steps: - name: TruffleHog scan (GitHub API — comments, wikis, unreachable objects) - # Deliberately unauthenticated. `GITHUB_TOKEN` cannot be used here: the GitHub source - # calls `GET /user` to identify the token owner, and an installation token gets - # `403 Resource not accessible by integration` — which is exactly how this job failed on - # its first real run on main. There is no flag to skip that call. This repository is - # public, so no token is needed — measured unauthenticated: 8651 chunks in 8 seconds. + # 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 \ @@ -166,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 {} @@ -185,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")