Skip to content

security: bootstrap sscsb supply-chain baseline (TruffleHog+Gitleaks+Trivy secrets, CodeQL+OpenGrep SAST) - #4

Open
p4gs wants to merge 3 commits into
mainfrom
security/sscsb-baseline
Open

p4gs wants to merge 3 commits into
mainfrom
security/sscsb-baseline

Conversation

@p4gs

@p4gs p4gs commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Update 2026-10-06: Gitleaks is kept alongside TruffleHog, by owner decision. TruffleHog only finds credentials for providers it has a detector for, and with --results=verified,unknown it reports a match only when the provider confirms it live or the verification attempt errors; it drops matches it could not verify, including those from detectors that have no verifier. Gitleaks matches on shape, so it also catches generic secrets such as passwords, internal tokens and keys for services TruffleHog has no detector for. Restored in ad284ba.

Bootstraps this repository with sscs-bootstrapper 0.4.0 and closes every gap the first sscsb verify reported.

sscsb verify: 4 failed / 3 degraded in the first report, before any remediation, down to 1 failed / 2 degraded at f51275b (2026-09-12). The one remaining failure and both degraded items were environment-real at that point and are explained at the bottom — none was a suppressed finding.

Re-run today on this PR's current head (4c9b666, 2026-10-06): 1 failed, 1 degraded. The drop is scorecard moving to PASS — but vacuously: this PR's own CodeQL/OpenGrep/OSV/Trivy uploads switch on code scanning for the repo, so the Scorecard alerts query (code-scanning/alerts?tool_name=Scorecard&state=open) now returns an empty list instead of failing, not because a Scorecard run actually found nothing. code-scanning/analyses confirms no Scorecard analysis has ever been published here — only CodeQL, Opengrep OSS, osv-scanner, and Trivy. The Remaining table below still describes the real state until a genuine Scorecard run lands on main.

Why this is a PR and not a push to main

main now carries a ruleset requiring signed commits, and the local sscsb pre-push hook blocked the direct push:

✗ f51275b: signer `10093271+p4gs@...` key is not marked hardware_backed=true
  in policy — hardware-backed signing is required on `main`

That block is correct and was not bypassed. .sscsb/policy/signers.toml honestly records hardware_backed = false for the 1Password-vaulted signing key, because it is a software vault, not a YubiKey — it has no -sk suffix and no attestation a verifier could check. The two ways past the hook were to lie in the policy file or to use --no-verify, and both were refused. See the open decision at the bottom.

What changed

Secrets — TruffleHog and Gitleaks, together

The first commit on this branch set gitleaks = false in .sscsb/config.toml and removed the Gitleaks job from the generated secrets-scan.yml by hand (sscsb init doesn't propagate that flag into the workflow it writes, so leaving the job in would have let config and CI disagree). The reasoning then was that TruffleHog's provider-verified findings made an unverifiable regex scanner redundant, plus gitleaks-action is license-walled for org-owned repos and no GITLEAKS_LICENSE exists here, so the job would also have been a permanently-red check.

That policy was reversed by the owner on 2026-10-06 (ad284ba): TruffleHog only finds credentials for providers it has a detector for, and with --results=verified,unknown it reports a match only when the provider confirms it live or the verification attempt errors; it drops matches it could not verify, including those from detectors that have no verifier. Gitleaks matches on shape, so it also catches generic secrets such as passwords, internal tokens and keys for services TruffleHog has no detector for — including something like api_key = "<random>". So TruffleHog and Gitleaks both run; each covers the other's gaps instead of one subsuming the other.

The license wall is still real, so Gitleaks doesn't come back as gitleaks-action — it runs via the MIT-licensed gitleaks binary, pinned to v8.30.1 with its SHA-256 pinned in the workflow. Before execution the job checks that the pinned digest matches the release's published checksums file and that the downloaded tarball matches the pinned digest, then scopes the scan to the commits under review with the same --no-merges --first-parent flags the licensed action used on push/PR, per the workflow's own comment — the only real difference is the range's lower bound. When no base can be resolved, the job scans only the head commit, which is narrower than the action. .gitleaks.toml (default ruleset) was added, and .sscsb/config.toml flips gitleaks back to true — which also re-enables Gitleaks in the local sscsb pre-commit secret scan alongside TruffleHog, so both the hook and the CI job read the added .gitleaks.toml. SECURITY.md's secret-detection paragraph now describes both scanners.

For completeness: other scanners such as Trivy's secret scanner also run across the repo — the Trivy filesystem scan in vuln-scan.yml includes secret in its scanner list (scanners: vuln,secret,misconfig), alongside trufflehog and gitleaks in secrets-scan.yml.

SAST — CodeQL and OpenGrep, with CodeQL carrying the app

codeql.yml shipped analysing actions only — a green check over unread source. index.html holds ~790 lines of inline <script> that fetches README.md at runtime and renders it through innerHTML sinks. That is the whole application.

Measured on 2026-09-12, against index.html:

Engine Rules Files scanned Findings
Semgrep 1.176.0 316 0 0
OpenGrep 1.25.0 74 0 0

Both report "Nothing to scan" — their JS rules bind to .js/.ts extensions and do not read inline script out of HTML (--scan-unknown-extensions does not change it). CodeQL's javascript-typescript extractor does, so it is added to the matrix and is the only configured engine that reads this application at all.

To confirm the zero is real and not vacuous, the inline JS was extracted to a true .js file and both engines re-run (309 + 68 rules): 0 findings. The code is clean on its merits — measured, not assumed.

semgrep.yml — hardened, not deleted

It is a live control (this repository has SEMGREP_APP_TOKEN set as a repo secret), so it was fixed at code level rather than removed:

  • actions/checkout@v4 → pinned to a full commit SHA (mutable tag = the tj-actions/changed-files CVE-2025-30066 shape)
  • persist-credentials: false so GITHUB_TOKEN does not sit on disk for later steps
  • step-security/harden-runner added as the first step
  • container: semgrep/semgrep dropped — harden-runner installs a host agent and cannot run in a container job, so no step ordering could have satisfied it. Semgrep now installs from PyPI at an exact pinned version, which also makes the analyser version auditable from the file.

SECURITY.md — added

security-insights.yml pointed at a SECURITY.md that did not exist — two dead links and no disclosure channel on a public repo. Added with scope, response targets, safe harbour, and explicit out-of-scope items. GitHub private vulnerability reporting is now enabled, so the advisory link is live rather than aspirational.

Repository settings applied

  • Ruleset sscsb-main-protection on main: blocks force-pushes, blocks deletion, requires signed commits
  • Private vulnerability reporting: enabled
  • protected_branches corrected to ["main"] — master has never existed here, and an unsatisfiable line trains readers to skim past real ones

Remaining — all defensible, none suppressed

Item Why it is open
branch-protection (FAIL) 3 of 5 sub-checks pass. The missing two — required PRs and required status checks — are one coupled decision that flips this repo from direct-push to PR-flow, contradicting the maintainer's direct-push workflow and the auto-deploy-from-main model. Status checks also cannot be required before they have run once. Owner decision.
scorecard (DEGRADED as of f51275b/2026-09-12; reports PASS at the current head, vacuously) No Scorecard analysis has ever been published for this repo. sscsb verify shows PASS now only because this PR's other scanners switched on code scanning and the Scorecard alerts query comes back empty, not because a Scorecard run found nothing. Self-resolving once this lands on main and a real run publishes.
signing-model (DEGRADED) Most unset lanes are account-level attestations only the owner can truthfully make (vigilant mode, phishing-resistant MFA, Codespaces GPG, the Claude GitHub App install). One gap is in the repo, not an attestation: the cloud-claude lane wants a repo-level .claude/settings.json with a non-empty attribution block (sscsb signing setup cloud-claude), which this PR does not add — left as a follow-up since no cloud-Claude commits land on this repo yet. The agent-claude-code lane is deliberately not configured — it would contradict the standing one-signer directive.

Open decision for the owner

require_hardware_backed. With it true and the key honestly false, pushing to main is structurally impossible for the repo's only approved signer. Three options:

  1. Set require_hardware_backed = false in .sscsb/config.toml — the fact stays visible, since sscsb verify still prints "signing key does not look hardware-backed" on every run. Recommended.
  2. Provision a hardware-backed ed25519-sk key, register it on GitHub, update the one-signer directive, then flip both flags to true. Strongest, but supersedes a global directive.
  3. Merge this PR and leave the local hook blocking direct pushes to main — effectively adopting PR-flow for this repo.

I attempted option 1 with a full written determination; my permission classifier blocked the edit, so it is deliberately left to you rather than routed around.

Also worth knowing: secret_scanning_validity_checks returns HTTP 200 on PATCH but stays disabled — it is org-gated, so it needs an org admin. And this repo has no LICENSE file (license: null), which OpenSSF Scorecard flags; picking one is your call, not mine.


First-push flag: false. As of 2026-09-12 (main at fb58737, 177 commits): 174 were unsigned commits pushed as p4gs <10093271+p4gs@users.noreply.github.com>, 1 was an unsigned commit created by the Semgrep app (fdbe7bf), and 2 were GitHub web-flow commits (fce1f8a, 285f368) that git cannot verify locally. f51275b was the repository's first commit signed with the maintainer's own key. Main has since received two more signed commits (605f543, 4cfa98e, both 2026-09-30).

🤖 Generated with Claude Code

https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm

…ets, CodeQL+OpenGrep SAST)

Bootstraps this repo with sscs-bootstrapper 0.4.0 and closes every gap the
first `sscsb verify` reported (4 failed / 3 degraded -> 0 failed / 2 degraded).

Policy decisions, each deliberate:

* SECRETS — TruffleHog ONLY. `gitleaks = false` in .sscsb/config.toml, and the
  Gitleaks job was removed from the generated secrets-scan.yml by hand because
  `sscsb init` does not propagate that flag into the workflow it writes. That
  job would also have failed unconditionally: gitleaks-action is license-walled
  for ORG-owned repos and no GITLEAKS_LICENSE is set here, so it was a
  permanently red check as well as a policy violation.

* SAST — CodeQL and OpenGrep, and CodeQL carries the application code.
  codeql.yml shipped analysing `actions` ONLY, which would have been a green
  check over unread source: index.html holds ~790 lines of inline <script> that
  fetches README.md at runtime and renders it through innerHTML sinks. Measured
  on 2026-09-12, Semgrep (316 rules) and OpenGrep (74 rules) BOTH report
  "0 files" against index.html -- their JS rules bind to .js/.ts extensions and
  do not read inline script out of HTML, and --scan-unknown-extensions does not
  change it. CodeQL's javascript-typescript extractor does, so it is added to
  the matrix and is the only configured engine that reads this app at all.
  Extracting the inline JS to a real .js file and re-running both engines
  (309 + 68 rules) returns 0 findings, so the code is clean on its merits --
  that zero was measured, not assumed.

* semgrep.yml — hardened rather than deleted (it is a live control; the org has
  SEMGREP_APP_TOKEN set). Fixed at code level: actions/checkout pinned from the
  mutable @v4 tag to a full commit SHA, persist-credentials: false so the
  GITHUB_TOKEN does not sit on disk for later steps, and harden-runner added.
  The `container: semgrep/semgrep` image was dropped because harden-runner
  installs a host agent and cannot run in a container job; Semgrep now installs
  from PyPI at an exact pinned version, which also makes the analyser version
  auditable from the file.

* SECURITY.md — added. security-insights.yml pointed at a SECURITY.md that did
  not exist (two dead links, and no disclosure channel on a public repo).
  GitHub private vulnerability reporting is now ENABLED, so the advisory link
  in it is live rather than aspirational.

* Branch protection — ruleset `sscsb-main-protection` created on main: blocks
  force-pushes, blocks deletion, requires signed commits.

* signers.toml records hardware_backed = false truthfully for the 1Password-
  vaulted key rather than asserting a hardware claim no verifier could check;
  require_hardware_backed stays true so the gap remains visible.

Verified: `sscsb verify` 0 failed. Remaining 2 degraded are environment-real and
documented in the report -- Scorecard has no published run until this lands, and
the five-environment signing model's unset lanes are account-level attestations
only the owner can truthfully make.

AI-Assisted: true
AI-Tool: Claude Code
AI-Model: claude-opus-5
AI-Role: draft

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

p4gs and others added 2 commits October 6, 2026 00:19
Owner decision: run Gitleaks and TruffleHog together rather than
TruffleHog-only. Independent probes showed Gitleaks catches generic
secrets (e.g. api_key = "<random>") and unverifiable keys that
TruffleHog's --results=verified,unknown filter drops, so the two
tools cover each other's gaps rather than duplicating detection.

AI-Assisted: true
AI-Tool: Claude Code
AI-Model: Sonnet 5
AI-Role: draft
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hwWMsTiv9Uf2gNbMh75uk
The comments said a TruffleHog finding means a live credential, or called the
filter verified-only. With --results=verified,unknown it also reports results
whose verification errored, which nobody has confirmed live. Separately, it
never reports generic secrets, since it has no detector for them, or keys it
cannot verify. Wording only; no scanner, rule or job changes.

AI-Assisted: true
AI-Tool: Claude Code
AI-Model: Opus 5.5
AI-Role: draft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hwWMsTiv9Uf2gNbMh75uk
@p4gs p4gs changed the title security: bootstrap sscsb supply-chain baseline (TruffleHog-only secrets, CodeQL+OpenGrep SAST) security: bootstrap sscsb supply-chain baseline (TruffleHog+Gitleaks+Trivy secrets, CodeQL+OpenGrep SAST) Oct 6, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants