This is the canonical definition of application-security and software-supply-chain rigor for every repo in this portfolio. It owns the machinery: ASVS level, SAST/SCA/secret-scanning configuration, Action SHA-pinning, SBOM/signing/provenance, and the token-permission model. Repos record only project-specific values and findings — the threat model narrative, the residual-risk register, the per-repo ASVS level declaration — in docs/RESPONSIBLE-TECH-AUDITS.md (methodology: RESPONSIBLE-TECH-FRAMEWORK.md §F) and their ROADMAP.md Metrics table. Reference, don't repeat.
Enforcement is binary. A control is AUTO-GATE (mechanically checkable, merge-blocking in CI, no
|| true, nocontinue-on-error) or REVIEW-GATE (human judgment, paired with a checklist line and a committed, dated artifact regenerated on release). There is no aspirational third category. The portfolio already proves every target here is achievable:habitable,trans-docs-navigator, andcivic-ai-eval-harness/govchat-evalalready ship the full SHA-pin + SBOM + Sigstore + OIDC posture. The work is propagation, not invention.
CI/CD-pipeline hardening (token permissions, OIDC, branch rulesets, workflow SAST) lives in CI-CD-STANDARD.md; this document covers it only where it is load-bearing for supply-chain integrity and cross-references the rest. Toolchain floors (ruff/mypy/coverage) live in CODE-QUALITY-STANDARD.md.
Target framework is OWASP ASVS 5.0.0 (May 2025). Every repo declares its level in docs/RESPONSIBLE-TECH-AUDITS.md §F. There is no silent default — a repo with no declaration fails review.
| Repo class | ASVS target | Rationale | Examples |
|---|---|---|---|
| Default floor | L1 | ASVS 5.0 states L1 is achievable with automated tooling alone; that maps exactly to our AUTO-GATE set. | every repo, minimum |
| Touches PII / identity / location | L2 | Field-level authz (BOPLA V8.2.1), cross-tenant isolation, breached-password checks, OIDC acr/amr validation. |
fare-assistant (transit PII), self-osint-monitor, olive-bark-logger, trans-docs-navigator, ledger |
| Catastrophic-breach surface | L3 (none today) | Hardware phishing-resistant factor, adaptive authz, annual threat-model + leadership justification. No current repo hosts auth at this blast radius; if one does, it declares L3 and adopts the V6/V8/V10 L3 review-gates. | — |
L1 is satisfied entirely by §3–§4 AUTO-GATEs (parameterized queries, output encoding, TLS 1.2+, server-side function- and object-level authz). L2 adds the authz integration tests in §5 and the OAuth/OIDC review-gate. ledger's no-outing guarantee and women-artist-discovery's "no identity inference" AST test are project-specific ASVS-V8 abuse-case controls — keep them; they are exemplars, not exceptions.
A repo with no authentication, no authorization surface, and no network ingress (a pure offline CLI or library, e.g. tods-validate, swelter as a library) declares ASVS: N/A (no auth/authz/ingress surface) with that exact reason. It still inherits all of §3, §4, §6, §7 — supply-chain and scanning are never N/A for a repo that ships code.
2. Privacy-first repos: hardened posture (olive-bark-logger, self-osint-monitor, trans-docs-navigator)
These three carry the highest individual-harm blast radius (a logger of sensitive activity, an OSINT self-monitor, and trans-resource navigation). They adopt every AUTO-GATE below as merge-blocking with zero waivers, plus:
| Control | Target | Measured by | Gate |
|---|---|---|---|
| No-secret/no-PII in logs | zero matches for password/token/email/Authorization field values |
Semgrep custom rule + log-assertion integration test (jq over emitted JSON) |
AUTO-GATE |
| Consent gate before feature code | sequenced before any M1 feature; CI asserts gate module imported on every entrypoint | static import test | AUTO-GATE (self-osint-monitor) |
| No third-party exfiltration | StepSecurity Harden-Runner egress allowlist in every job; deny-by-default | Harden-Runner block mode + audit |
AUTO-GATE |
| Sentinel-identity tripwire | injected sentinel survives full pipeline without leaking | isolated CI job (lift ledger's no-outing test pattern) |
AUTO-GATE |
| ASVS L2 | full §5 authz tests | integration suite | AUTO-GATE |
self-osint-monitor is spec-only today (no CI, no pyproject, no tests). It scaffolds this standard as its M0 deliverable before any M1 feature code — Harden-Runner, gitleaks pre-commit + CI, SHA-pinned actions, top-level permissions: contents: read, and the consent gate land first.
Semgrep for fast diff-aware PR coverage (~10 s, SARIF to Code Scanning); CodeQL for deep cross-file taint/dataflow on a nightly schedule + required check on main. Rejected: Semgrep alone (misses multi-file auth bypasses); CodeQL alone (too slow for every PR, no IaC breadth).
| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Semgrep findings | zero unwaived HIGH/CRITICAL at merge | semgrep ci --sarif |
AUTO-GATE |
| CodeQL findings | zero unwaived HIGH/CRITICAL on main |
github/codeql-action, nightly + required on push to main |
AUTO-GATE (graduate davis-bike-hazard-map from advisory) |
| Workflow SAST | zero high/critical | CodeQL language: actions + zizmor (see §7) |
AUTO-GATE |
| Waiver hygiene | every waiver has expiry + reason | committed .semgrep-waivers.yml, reviewed quarterly |
REVIEW-GATE |
# .github/workflows/sast.yml — Semgrep PR gate
permissions:
contents: read
jobs:
semgrep:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # upload SARIF only
steps:
- uses: actions/checkout@<40-char-sha> # v4.2.2
with: { persist-credentials: false }
- uses: semgrep/semgrep-action@<40-char-sha> # v1.x
- run: semgrep ci --sarif --output=semgrep.sarif --severity=ERROR --severity=WARNING
- uses: github/codeql-action/upload-sarif@<40-char-sha> # v3.x
with: { sarif_file: semgrep.sarif }make verify runs semgrep ci locally so the gate is byte-for-byte reproducible (matches the portfolio's make verify == CI discipline).
OSV-Scanner is the portfolio baseline (queries the same OSV DB as Scorecard's Vulnerabilities check, covers 20+ ecosystems incl. transitive lockfile deps). pip-audit / npm audit run additionally per ecosystem. The || true on pip-audit in ledger and nearmiss is deleted — that neuter defeats the gate entirely.
| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Python vulns | zero HIGH+CRITICAL with a fix available | pip-audit (no || true) + osv-scanner -r . over uv.lock |
AUTO-GATE |
| Node vulns | zero HIGH+CRITICAL | npm audit --audit-level=high |
AUTO-GATE (frontends + personal-site Lambda) |
| Transitive coverage | lockfile present & scanned | osv-scanner fails if no uv.lock/package-lock.json |
AUTO-GATE |
| Update automation | Dependabot or Renovate config present (Scorecard Dependency-Update-Tool) |
committed dependabot.yml/renovate.json |
AUTO-GATE |
| Open critical alerts | none merge-able | branch ruleset blocks merge on open Dependabot alert CVSS ≥ 7.0 | AUTO-GATE |
| Unfixable HIGH/CRITICAL waiver | tracked + VEX-justified | committed vex.json (CycloneDX 1.7 VEX), quarterly review |
REVIEW-GATE |
gtfs-scorecard (fetches external GTFS zips, runs a Java subprocess) and fare-assistant (transit PII) currently have the thinnest scanning despite the highest sensitivity — they adopt the full SCA + secret-scan set first.
Two-gate gitleaks: pre-commit (Gate 1) and CI diff (Gate 2). TruffleHog runs a scheduled full-history scan (Gate 3) with live-credential verification.
| Gate | Tool | Target | Gate |
|---|---|---|---|
| 1 pre-commit | gitleaks v8.30.1 |
zero unredacted matches | AUTO-GATE |
| 2 CI diff | gitleaks --exit-code 1 --redact (no || true) |
zero matches | AUTO-GATE |
| 3 scheduled | trufflehog git --object-discovery --results=verified,unknown --fail |
zero verified live creds | AUTO-GATE (page on hit) |
# .pre-commit-config.yaml — Gate 1 (also closes the pre-commit adoption gap)
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks: [{ id: gitleaks }]Pre-commit adoption is currently the minority (~6 of 19 repos). This config is now mandatory in every repo; the gitleaks + ruff + mypy hooks are the required floor.
L1 floor (AUTO-GATE, all repos with ingress): parameterized queries only (no string-built SQL — Semgrep rule), output encoding on all user-controlled HTML sinks (XSS), TLS 1.2+ asserted, server-side function-level authz (V8.1.1) and object-level authz (V8.1.2).
L2 (AUTO-GATE, PII repos): an integration suite asserting every protected endpoint returns 403 to an unauthorized principal and that object-level (BOLA/IDOR) and field-level (BOPLA) access is denied cross-tenant. Block deploy if any protected route lacks a negative-path test.
# tests/test_authz.py — the negative-path assertion, every protected route
@pytest.mark.parametrize("route", PROTECTED_ROUTES)
def test_unauthorized_principal_gets_403(client, route):
assert client.get(route, headers=other_tenant_token()).status_code == 403| Metric | Target | Measured by | Gate |
|---|---|---|---|
| Injection / XSS | zero | Semgrep taint rules + DAST in staging | AUTO-GATE |
| Function-level authz | every protected route 403s unauth | parametrized integration test | AUTO-GATE (L2) |
| Object-level authz (BOLA) | cross-tenant access denied | integration test w/ second principal | AUTO-GATE (L2) |
| OAuth/OIDC (V10) | PKCE enforced; acr/amr validated; sender-constrained tokens |
security architecture review | REVIEW-GATE (identity-critical services) |
| Annual pentest (BOLA/BOPLA/tenant) | report attached to release | manual + human triage | REVIEW-GATE (L2 PII repos) |
This is the portfolio's largest open gap (~13 of 19 repos non-conformant on SHA-pinning) and the most active threat — the March 2026 trivy-action force-push and tj-actions compromises were real exfiltration events, not hypotheticals.
This section owns the supply-chain machinery (SBOM, signing, provenance) that a release invokes. The release process and policy — SemVer, signed tags, CHANGELOG, the tag-triggered pipeline, and Trusted Publishing — lives in
RELEASE-AND-VERSIONING-STANDARD.md. When that standard says "sign + attest → SECURITY §6," this is the section it means.
Every uses: — third-party actions, actions/*, reusable workflows, and the deploy path — is pinned to a full 40-char commit SHA with a trailing # vX.Y.Z comment. Tags and branches are immutable-reference failures. This explicitly closes nearmiss (0/17 SHA), gtfs-scorecard (0/2), and trans-docs-navigator's single straggler in deploy-aws-preview.yml.
# correct — immutable, human-readable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@<40-char-sha> # v5.x
# WRONG — mutable, exploitable
- uses: actions/checkout@v4
- uses: gitleaks/gitleaks-action@v2Renovate keeps SHAs current and survivable:
// renovate.json
{
"extends": ["config:recommended", "helpers:pinGitHubActionDigestsToSemver"],
"minimumReleaseAge": "72 hours"
}Migration: run StepSecurity Action-Advisor (app.stepsecurity.io) or pin-github-action over each workflow to auto-generate SHA-pinned stubs.
npx pin-github-action .github/workflows/*.yml # rewrites tags -> SHA + comment| Metric | Target | Measured by | Gate |
|---|---|---|---|
Every uses: SHA-pinned |
100% incl. reusable + deploy | Scorecard Pinned-Dependencies ≥ 9/10 on default branch |
AUTO-GATE |
| SHAs kept current | Renovate active, 72h cooldown | committed renovate.json |
AUTO-GATE |
Generate a CycloneDX 1.7 (ECMA-424) SBOM for every release artifact and every container image; validate against schema before signing; fail the build on schema failure. Rejected SPDX-only: CycloneDX 1.7's attestation + VEX + ML-BOM modelCard shapes fit our SLSA and AI-repo needs better; both are NTIA/CISA/EU-CRA accepted.
syft . -o cyclonedx-json=sbom.cdx.json # or: cdxgen -t python -o sbom.cdx.json
cyclonedx-cli validate --input-file sbom.cdx.json --input-version v1_7
grype sbom:sbom.cdx.json --fail-on high # gate the SBOM itselfRequired component fields: name, version, purl, hashes (SHA-256). AI/RAG repos additionally emit an ML-BOM modelCard component for each model dependency (ties to the model-card lint in AI-EVALUATION-STANDARD.md).
Trivy/Grype image scan, threshold standardized to CRITICAL,HIGH portfolio-wide (the eval harnesses' CRITICAL-only setting is raised). Closes the missing scans in davis-bike-hazard-map, habitable, civic-rag-starter-kit, queer-the-stacks, trans-docs-navigator.
trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 1 $IMAGEKeyless cosign (OIDC via the workflow identity; Fulcio cert + Rekor log; no long-lived keys). Target SLSA Build L2 minimum (signed provenance, hosted build, consumer-verifiable); L3 for public/critical packages via slsa-framework/slsa-github-generator (signing key inaccessible to build steps, isolated ephemeral build). Provenance predicate https://slsa.dev/provenance/v1.
# release signing — keyless, OIDC identity
permissions:
id-token: write # OIDC
contents: write # attach release assets
attestations: write
steps:
- uses: sigstore/cosign-installer@<40-char-sha> # v3.x
- run: cosign sign --yes $IMAGE
- run: cosign attest --yes --predicate sbom.cdx.json --type cyclonedx $IMAGE
- uses: actions/attest-build-provenance@<40-char-sha> # v2.x (SLSA L2 + Source Track)
with: { subject-path: 'dist/*' }Caching is disabled in any job that signs/publishes/generates provenance (cache poisoning violates SLSA isolation). Concurrency group on release jobs. Both rules cross-reference CI-CD-STANDARD.md.
Deployment-side: consumers verify before install.
cosign verify --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp='^https://github.com/<org>/<repo>/' $IMAGE
slsa-verifier verify-artifact --source-uri github.com/<org>/<repo> dist/<artifact>ossf/scorecard-action nightly + on PRs to default branch; SARIF to Code Scanning.
| Scorecard check | Target | Gate |
|---|---|---|
Pinned-Dependencies |
≥ 9/10 | AUTO-GATE |
Token-Permissions |
10/10 | AUTO-GATE |
Dangerous-Workflow |
10/10 | AUTO-GATE |
Branch-Protection |
≥ 8/10 | AUTO-GATE |
Signed-Releases |
10/10 (.intoto.jsonl on last 5) |
AUTO-GATE (release repos) |
Vulnerabilities |
10/10 | AUTO-GATE |
| Aggregate | ≥ 8/10 | AUTO-GATE |
| Monthly Scorecard report committed | present, dated | REVIEW-GATE |
Owned in detail by CI-CD-STANDARD.md; the supply-chain-load-bearing minimums repeated here so a repo author can't miss them:
| Control | Target | Measured by | Gate |
|---|---|---|---|
Top-level permissions |
contents: read, per-job escalation only |
present in every workflow (closes gtfs-scorecard, jobradar, personal-site, tods-validate) |
AUTO-GATE |
| Cloud creds | OIDC only, no long-lived secrets, sub scoped to repo:…:environment:… |
audit-log alert on new long-lived secret | AUTO-GATE |
| Workflow SAST | zizmor required check on any PR touching .github/workflows/ |
merge-blocked on high/critical | AUTO-GATE |
persist-credentials: false |
on every actions/checkout |
zizmor / grep | AUTO-GATE |
No direct push to main |
branch ruleset, no admin bypass, ≥1 required review | committed ruleset export | AUTO-GATE |
CODEOWNERS routes .github/workflows/ + security-critical files |
required reviewer | committed CODEOWNERS |
AUTO-GATE |
| Branch ruleset + CODEOWNERS as committed artifacts | present | repo files | REVIEW-GATE (close portfolio-wide absence) |
# zizmor as a required check
permissions: { contents: read }
jobs:
zizmor:
if: contains(toJSON(github.event.pull_request.changed_files), '.github/workflows/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<40-char-sha> # v4.2.2
with: { persist-credentials: false }
- uses: zizmorcore/zizmor-action@<40-char-sha> # v0.xEvery repo's docs/RESPONSIBLE-TECH-AUDITS.md §F records, with no blanks:
- ASVS level (L1 / L2 / L3 /
N/A (reason)). - Container scanning: enabled, or
N/A (no Dockerfile). - SBOM + signing: enabled on release, or
N/A (not a release-producing repo)— note this is rare; libraries published to PyPI do produce releases. - Secret-management policy (OSPS-BR-07.02): storage, rotation, revocation — committed once, reviewed annually.
- VEX for any unfixable HIGH/CRITICAL dependency CVE.
An N/A is a declared decision with a one-line reason, reviewed like any other. A blank is a defect.
queer-the-stacks/queer-specfic-readershare thequeer_the_stackspackage — an undocumented fork/rename. Reconcile to one canonical repo before applying this standard, or both drift and double-count. REVIEW-GATE.gtfs-scorecardhas no repo-root config; its project lives atpipeline/. CI here must targetpipeline/(or hoist config to root) so portfolio tooling does not silently skip it. AUTO-GATE (CI path declared).
Ordered, all blocking; make verify runs the local-runnable subset byte-for-byte:
1. secret scan → gitleaks pre-commit (Gate 1) + CI diff (Gate 2)
2. SAST → semgrep ci (HIGH/CRITICAL) ; CodeQL nightly + required on main
3. SCA → pip-audit / npm audit --audit-level=high / osv-scanner (no || true)
4. authz tests → 403-on-unauth + BOLA/BOPLA (L2 repos)
5. container scan → trivy image --severity CRITICAL,HIGH (Dockerfile repos)
6. workflow SAST → zizmor (PRs touching .github/workflows/)
7. supply chain (rel) → SBOM (CycloneDX 1.7) -> validate -> cosign sign + attest -> SLSA provenance
8. scorecard → ossf/scorecard-action, aggregate >= 8, criticals at target
9. (human) review → threat-model sign-off, VEX, secrets policy, pentest (L2)
Last verified: 2026-06-21 · Recheck cadence: per OWASP ASVS, SLSA, CycloneDX, OpenSSF Scorecard/Baseline, and Sigstore release; and immediately on any disclosed GitHub Actions supply-chain compromise. Confirm current tool versions (gitleaks, TruffleHog, Scorecard, cosign, CycloneDX spec) at build time.