Skip to content

chore(security): bootstrap sscs-bootstrapper and close the gaps it found - #6

Open
p4gs wants to merge 3 commits into
mainfrom
chore/sscsb-bootstrap
Open

p4gs wants to merge 3 commits into
mainfrom
chore/sscsb-bootstrap

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 1f4133c.

What

First-time sscsb init bootstrap of this repository (47 controls, 32 enabled), plus a fix for every real finding the first sscsb verify produced.

sscsb verify before → after: 23 pass / 3 fail / 3 degraded / 3 info (the first verify run after sscsb init, before any fixes) → 27 pass / 1 fail / 2 degraded / 2 info (at the original bootstrap commit 12094c5, with controls.secrets.gitleaks = false; 15 controls deliberately disabled in both runs). Not re-run since Gitleaks was turned back on in 1f4133c. The signed local scan record committed in 10a0d73 (.sscsb/scan-record.local.json + .sig) was also taken at 12094c5 and still lists gitleaks: disabled in config; it has not been regenerated or re-signed since 1f4133c.

Scanner choice per surface

Surface Tool Why
Credentials TruffleHog + Gitleaks + Trivy's secret scanner (controls.secrets.trufflehog = true, controls.secrets.gitleaks = true; Trivy via vuln-scan.yml's scanners: vuln,secret,misconfig) 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. Trivy's filesystem scan runs its own built-in secret-detection rules as a third, independent pass, outside the dedicated secrets-scan.yml job. CI runs the MIT-licensed gitleaks binary directly (pinned v8.30.1, checksum-verified before execution), not gitleaks-action, since that action (non-OSS EULA since v2.0.0) refuses to scan org-owned repos without a GITLEAKS_LICENSE key. The key itself is free from gitleaks.io, but none is configured for this org, and running the binary avoids both obtaining the key and the action's license-validation call.
SAST CodeQL + OpenGrep, both on Two engines with genuinely different reach: OpenGrep is rule-driven and fast, CodeQL is interprocedural. Both genuinely support this repo's languages (Rust + Actions).

sscsb's secrets-scan.yml template emits a gitleaks-action job. That job was dropped at bootstrap (12094c5) and replaced in 1f4133c by a hand-written job that runs the pinned, checksum-verified gitleaks 8.30.1 binary over the PR/push commit range, with a hand-written .gitleaks.toml ([extend] useDefault = true) as its ruleset. controls.secrets.gitleaks = true also turns Gitleaks back on in the local sscsb pre-commit hook — which passes that same --config .gitleaks.toml — alongside TruffleHog, and in what sscsb verify checks for. The pre-push range scan never read this key: it runs Gitleaks whenever the binary is on PATH, passing no --config flag at all. Gitleaks itself defaults to <target path>/.gitleaks.toml when --config is omitted, and the pre-push scan's target path is the repo root — so it already ran Gitleaks at 12094c5 (under gitleaks' built-in default ruleset, since no .gitleaks.toml existed in the repo yet) while the key was false, and from 1f4133c on it picks up the same .gitleaks.toml ruleset as the CI job and pre-commit hook automatically, with nothing to wire up.

The archived semgrep/semgrep-action@v1 job in security.yml is retired. OpenGrep is Semgrep's open fork and resolves the same registry rulesets, so p/rust and p/security-audit were added to the OpenGrep run: same rule coverage, now on a pinned cosign-verified binary with SARIF uploaded to code scanning. CI on 1f4133c: Ran 19 rules on 63 files: 0 findings (SAST (OpenGrep) run 37414610635).

Findings fixed

  1. CODEOWNERS was completely inert. All ten rules named @grcengineering/security, a team that does not exist. GET /repos/grcengineering/cvm/codeowners/errors returned Unknown owner for every line, so no rule bound to anyone — including the branch-protection "require review from Code Owners" path. Rules now name @p4gs and additionally cover the new .sscsb/ policy surface. Swap back if a real, publicly-visible @grcengineering/security team with write access is ever created.
  2. deny.toml could not be parsed at all. cargo-deny 0.19.4: error[unexpected-value] ... unmaintained = "warn". The v2 schema turned unmaintained into a scope, and removed vulnerability, unlicensed, copyleft. The deny job — a required status check on main — could therefore never have run. Rewritten to the v2 schema, same intent, and tightened (unmaintained = "all"). cargo deny check now exits 0.
  3. Two Renovate config files. sscsb init wrote renovate.json5 next to the pre-existing renovate.json; Renovate aborts with "Found multiple config file names" when both exist. Merged into renovate.json5 (old cool-down rules preserved, isVulnerabilityAlert migrated to the modern vulnerabilityAlerts block); renovate.json deleted.
  4. Dockerfile base images were tag-pinned, not digest-pinned. Both cgr.dev/chainguard/rust:latest-dev and cgr.dev/chainguard/static:latest now carry sha256 digests (resolved 2026-09-12).
  5. CodeQL analysed actions only — not this workspace's Rust. rust added to the matrix with build-mode: none.
  6. release.yml and security.yml were unhardened. harden-runner added to all nine jobs, persist-credentials: false on every checkout, --locked on cargo install.
  7. security-insights.yml REPLACE-ME administrator placeholder filled in; reporting channel aligned with SECURITY.md.

Left open, with reasons

  • branch-protection — FAIL. main is missing Require signed commits; everything else already passes (required PRs, force-push block, required checks, deletion protection, stale-review dismissal, strict up-to-date). required_approving_review_count is 0 (checked 2026-10-06), so no reviewer-count gate applies here. This is a repo-admin remote write, deliberately not made by an agent. sscsb harden branch-protection --apply is not the fix here — it only edits rulesets, and this repo uses classic branch protection, so its plan output is no ruleset targets this branch — skipped. The one-line fix is:
    gh api -X POST repos/grcengineering/cvm/branches/main/protection/required_signatures
    Three Scorecard sub-gaps (≥1 required approving review, currently 0; code-owner review; last-push approval) need a second reviewer and cannot be satisfied by a solo maintainer.
  • signing-model — DEGRADED. Remaining items are account-level attestations only the maintainer can truthfully assert (github-web vigilant mode + phishing-resistant MFA, Codespaces GPG verification, Claude GitHub App authorization). The agent-claude-code lane wants a distinct agent commit identity, which conflicts with this operator's standing one-signer directive — a policy call, not a config gap.
  • scorecard — DEGRADED. No published Scorecard results yet; scorecard.yml runs on push to the default branch, so this resolves once this PR lands.

Merge note

main currently requires a PR but 0 approving reviews, with enforce_admins: true (checked 2026-10-06); PR #6 reports mergeStateStatus: CLEAN / mergeable: MERGEABLE, so no reviewer or protection change is needed to merge it. Required signed commits are still missing (see branch-protection above), which remains a repo-admin action.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm

Runs `sscsb init` on this repo for the first time (47 controls, 32 enabled)
and fixes every real finding the first verify produced.

Policy: strongest-tool-only.
* Credential scanning is TruffleHog alone — `controls.secrets.gitleaks = false`.
  TruffleHog verifies a candidate against the issuing provider, so a finding is
  a live credential rather than a regex hit; gitleaks-action additionally needs
  a paid license for org-owned repositories. sscsb's secrets-scan.yml template
  emits the gitleaks job regardless of that key, so the job is removed by hand
  and must be removed again after any `sscsb init` re-run.
* SAST is CodeQL + OpenGrep, both enabled. The archived
  semgrep/semgrep-action@v1 job in security.yml is retired; OpenGrep is
  Semgrep's open fork and resolves the same rulesets, so `p/rust` and
  `p/security-audit` were added to the OpenGrep run — same coverage, on a
  pinned cosign-verified binary, with SARIF uploaded to code scanning.

Findings fixed:
* CODEOWNERS was entirely inert. All ten rules named @grcengineering/security,
  a team that does not exist — GitHub's codeowners/errors endpoint returned
  "Unknown owner" for every line, so no rule bound to anyone and the
  code-owner-review path had nothing behind it. Rules now name @p4gs and cover
  the new .sscsb/ policy surface.
* deny.toml could not be parsed by cargo-deny 0.19.4 at all: `unmaintained`
  became a scope rather than a severity in the v2 schema, and `vulnerability`,
  `unlicensed` and `copyleft` were removed. The `deny` job in security.yml —
  a required status check on main — could therefore never have run. Rewritten
  to the v2 schema with the same intent, and tightened: unmaintained = "all".
* Two Renovate config files (pre-existing renovate.json plus the renovate.json5
  sscsb wrote) make Renovate abort with "Found multiple config file names".
  Merged into renovate.json5; renovate.json deleted.
* Dockerfile base images were tag-pinned, not digest-pinned. Both now carry
  sha256 digests (resolved 2026-09-12).
* CodeQL analysed `actions` only, not this workspace's Rust. Added `rust` to
  the matrix with build-mode none.
* release.yml and security.yml: harden-runner added to all ten jobs,
  persist-credentials: false on every checkout, --locked on cargo install.
* security-insights.yml REPLACE-ME administrator filled in.

Verify: 23 pass / 3 fail / 3 degraded before, 27 pass / 1 fail / 2 degraded
after. Remaining: branch-protection (required signed commits is a repo-admin
remote write, left for a human), signing-model (account-level attestations
only the maintainer can truthfully make), scorecard (no published results until
this lands on the default branch).

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 September 12, 2026 11:59
`sscsb scan --local` writes the checks that are only observable on a developer
machine — which key git signs with, whether the installed hooks actually block,
what is in the package-trust baseline, which scanners are on PATH — and signs
the result in the `sscsb-scan-record` namespace with the repository's own
committed trust anchor (.sscsb/policy/allowed_signers). A clone cannot observe
any of it, so without this record the public directory scores those controls
`unverified` and the repository reads provisional however good its posture is.

Signed by the human-class signer this repo commits as approved
(SHA256:prXatGO56nl8Or4JdDSzIIcj8hZE1jBxnFaXZOnAPDQ); ssh-keygen -Y verify
returns Good for the namespace. Record asserts 28 pass / 1 fail / 1 degraded at
12094c5 — the residual FAIL is branch-protection's missing 'Require signed
commits', a repo-admin remote write left for a human.

Not submitted to the public directory: `sscsb scan --local --submit` is a
separate, deliberate publication step.

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
Owner decision: run Gitleaks alongside TruffleHog rather than
TruffleHog-only. Gitleaks catches generic and unverifiable secrets
(e.g. a plausible api_key literal, or key formats with no
provider-verification endpoint) that TruffleHog's --results=verified,unknown
filter drops; independent probes confirmed this detection gap. CI runs
the MIT-licensed gitleaks binary directly (pinned v8.30.1, checksum-verified
before execution) since gitleaks-action requires a paid license for
organization-owned repos.

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
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