mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:01:31 +00:00
02438ad9e1
Twelve findings from the architecture diligence audit's Phase 3 bundle
closed in one PR. All touch the CI workflows + small doc-drift fixes
across the production Go tree + migration headers.
CI workflow changes
====================
TEST-H1 — Race detection on ./... -short
.github/workflows/ci.yml:106 was a 9-package explicit list. Audit
finding TEST-H1 flagged that 25+ packages (internal/auth/*,
internal/repository/*, internal/mcp, internal/scep, internal/pkcs7,
internal/api/router, internal/api/acme, internal/cli, internal/cms,
internal/config, internal/deploy, internal/integration,
internal/ratelimit, internal/secret, internal/trustanchor, all of
cmd/) silently dropped off race coverage.
Post-fix: 'go test -race -short ./... -count=1 -timeout 600s'.
76 testing.Short() guards already cover testcontainers + live-DB
integration suites, so -short keeps the long-running tests out.
TEST-H2 — Cross-platform build matrix
New 'cross-platform-build' job in ci.yml. Matrix:
ubuntu-latest + windows-latest + macos-latest, fail-fast: false.
Builds cmd/server + cmd/agent + cmd/cli + cmd/mcp-server on each.
Catches Windows-specific regressions (path separators, file
permissions, exec.Command semantics) the pre-Phase-3 Ubuntu-only
CI missed.
TEST-L1 — actions/setup-go cache: true (explicit)
setup-go v5 defaults cache: true; making it explicit so a future
setup-go upgrade can't silently flip it. Re-runs hit the Go module
+ build cache instead of recompiling cold.
TEST-M1 — Mutation-testing floor at 55%
security-deep-scan.yml::go-mutesting step rewritten. Removed
continue-on-error + per-package '|| true'. New post-loop check
extracts every 'The mutation score is X.YZ' line and fails the
step if any package drops below 0.55. Floor rationale: starter
ratio catches major regressions without rejecting the audit's
'this is OK' steady state; raise quarterly.
TEST-M2 — 3 advisory deep-scan gates promoted to blocking
Removed continue-on-error: true from:
- gosec (filtered to G201/G202/G304/G108 high-signal rules:
SQL-injection + path-traversal + pprof-exposed)
- osv-scanner (multi-ecosystem CVE; complements govulncheck
which is already blocking in ci.yml)
- trivy image scan (--severity HIGH,CRITICAL --exit-code 1)
continue-on-error count: 15 → 11.
ZAP / schemathesis / nuclei / testssl stay advisory because their
false-positive rates on https://localhost:8443-targeted DAST runs
are high.
TEST-M3 — Playwright harness stub
web/package.json adds '@playwright/test' devDep + 'e2e' / 'e2e:install'
npm scripts. web/playwright.config.ts ships single chromium project
with webServer block pointing at 'npm run dev'. web/src/__tests__/
e2e/smoke.spec.ts proves the harness wires through. The full 15-flow
suite ships in frontend-design-audit Phase 8 (TEST-H1 in THAT audit);
this is the wiring + a single smoke test as the regression floor.
New Makefile target: 'make e2e-test'.
Doc/code drift fixes
====================
TEST-M4 + ARCH-L2 — Skip inventory artifact + CI guard
scripts/skip-inventory.sh walks every t.Skip site under cmd/ +
internal/ + deploy/test/ and emits docs/testing/skip-inventory.md
grouped by package with file:line:expression triples. Current
inventory: 142 t.Skip sites, 76 testing.Short() guards.
scripts/ci-guards/skip-inventory-drift.sh regenerates and fails on
diff (excluding the 'Last reviewed' timestamp line which drifts
daily). The Markdown is the canonical acquisition-diligence artifact
for 'what tests are being skipped and why.'
ARCH-H3 — MCP catalogue floor reconciliation
Audit framing was '121 vs floor 150 — doc/code drift.' Live count
via the test's actual regex over all 5 tool files (tools.go +
tools_audit_fix.go + tools_auth.go + tools_auth_bundle2.go +
tools_est.go): 155 unique 'Name: "certctl_*"' declarations.
Pre-Phase-3 audit measured tools.go in isolation (121) and missed
the other 4 files (+34 unique names). The test at
internal/ciparity/surface_parity_test.go::TestSurfaceParity_MCP
passes today (155 ≥ 150). Added a clarifying comment near
mcpBaselineFloor explaining the measurement scope so future
reviewers don't repeat the audit's framing error.
STATUS: stale — no code drift, just a measurement scoping error in
the audit.
ARCH-L1 — panic() rationale comments
5 panic sites in production Go (excluding _test.go):
- internal/repository/postgres/tx.go:84
- internal/service/issuer.go:861 (mustJSON)
- internal/service/est.go:728 (mustParseTime)
- internal/service/acme.go:1288 (rand source failure — already documented)
- internal/pkcs7/certrep.go:270 (OID marshal — already documented)
Added ARCH-L1 rationale comments to the 3 sites that didn't have
them. All 5 are defensible impossible-path / rethrow / hardcoded-
constant guards.
ARCH-L3 — Migration IF-NOT-EXISTS carve-outs
4 migrations skip the literal 'IF NOT EXISTS' token but ARE
idempotent via different Postgres patterns:
- 000014_policy_violation_severity_check.up.sql: ALTER TABLE
ADD CONSTRAINT CHECK doesn't accept IF NOT EXISTS; idempotency
via DROP CONSTRAINT IF EXISTS preamble.
- 000018_audit_events_worm.up.sql: CREATE OR REPLACE FUNCTION
+ DROP TRIGGER IF EXISTS + CREATE TRIGGER + DO $$ pg_roles
existence check. CREATE TRIGGER doesn't take IF NOT EXISTS.
- 000030_rbac_admin_perms.up.sql: INSERT ... ON CONFLICT DO NOTHING.
- 000039_audit_crit1_perms.up.sql: same INSERT + ON CONFLICT pattern.
Added ARCH-L3 header comments to each explaining the carve-out so
reviewers don't flag the missing literal token.
STATUS: largely stale — migrations are already idempotent.
ARCH-L4 — TODO/FIXME → see #<descriptor>
5 TODOs rewritten to the allowed 'see #<descriptor>' pattern:
- internal/repository/postgres/auth.go:220 → see #bundle-2-scope-fk
- internal/connector/discovery/gcpsm/gcpsm.go:547 → see #gcpsm-pagination
- internal/service/audit.go:244 → see #audit-pagination-count
- internal/service/job.go:295, 299 → see #validation-job-impl
New CI guard scripts/ci-guards/no-todo-in-prod.sh grep-fails any
new TODO/FIXME in cmd/ + internal/ (excluding _test.go); allows
'see #N' / 'see #<descriptor>' patterns.
Sandbox limitation
==================
The 6.1 GB certctl working tree fills the sandbox volume; go1.25.10
toolchain download fails with 'no space left on device' (sandbox has
1.25.9; go.mod requires 1.25.10). Local 'go test' / 'go build' NOT
run in this commit. Operator must run 'make verify' on their
workstation before push per CLAUDE.md operating rules.
The smoke.spec.ts NOT executed in the sandbox (no chromium installed).
Operator runs 'cd web && npm install && npx playwright install
--with-deps chromium && npm run e2e' on first wire-up.
All CI guards (no-todo-in-prod, skip-inventory-drift, G-3
env-docs-drift, doc-rot-detector, and every existing guard) verified
clean by running each individually.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-TEST-H1,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-H2,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-M1,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-M2,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-M3,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-M4,
cowork/certctl-architecture-diligence-audit.html#fix-TEST-L1,
cowork/certctl-architecture-diligence-audit.html#fix-ARCH-H3,
cowork/certctl-architecture-diligence-audit.html#fix-ARCH-L1,
cowork/certctl-architecture-diligence-audit.html#fix-ARCH-L2,
cowork/certctl-architecture-diligence-audit.html#fix-ARCH-L3,
cowork/certctl-architecture-diligence-audit.html#fix-ARCH-L4
241 lines
9.9 KiB
YAML
241 lines
9.9 KiB
YAML
name: security-deep-scan
|
|
|
|
# Bundle-7 / Audit D-001..D-007:
|
|
# Slow / containerized scans on a daily schedule + manual dispatch.
|
|
# Per-PR fast gates live in ci.yml; this workflow runs the heavyweight
|
|
# tools that need docker, network egress to scanner registries, or
|
|
# longer wall-clock budgets than a per-PR check tolerates.
|
|
#
|
|
# Scope:
|
|
# trivy image container CVE + secret scan
|
|
# syft SBOM CycloneDX SBOM artefact upload
|
|
# ZAP baseline DAST baseline against a live deploy_test stack (D-004)
|
|
# nuclei template-based vuln scan against the same stack
|
|
# schemathesis OpenAPI fuzz against the running server
|
|
# testssl.sh TLS configuration audit (D-005)
|
|
# race detector x10 full -count=10 race run on the entire test suite (D-002)
|
|
# gosec Go security static analysis (slow first run)
|
|
# go-mutesting mutation testing on crypto cluster (D-003)
|
|
# semgrep p/react-security frontend XSS / dangerouslySetInnerHTML / target=_blank ruleset (D-007)
|
|
#
|
|
# Each step is best-effort — failures are uploaded as artefacts but do
|
|
# NOT block the workflow. Triage happens via the Bundle-7 receipt
|
|
# the project's comprehensive-audit tool-output directory.
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '0 6 * * *' # daily 06:00 UTC
|
|
workflow_dispatch: {}
|
|
|
|
permissions:
|
|
contents: read
|
|
security-events: write # SARIF upload to GitHub code scanning
|
|
|
|
jobs:
|
|
deep-scan:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
|
with:
|
|
go-version: '1.25'
|
|
|
|
- name: Install Go-based tools
|
|
run: bash scripts/install-security-tools.sh
|
|
continue-on-error: true
|
|
|
|
# --- Static analysis (slow paths) ---
|
|
|
|
- name: gosec (G201/G202/G304/G108 subset — Phase 3 TEST-M2 hard gate)
|
|
# Phase 3 TEST-M2 closure (2026-05-13): gosec promoted from
|
|
# continue-on-error (advisory) to blocking on the 4 high-signal
|
|
# rule subset that targets real prod-bug classes:
|
|
# G201 = SQL string formatting (SQL injection)
|
|
# G202 = SQL string concatenation (SQL injection)
|
|
# G304 = file-path traversal via tainted input
|
|
# G108 = profiling endpoint exposed
|
|
# Other gosec rules (G1xx-G7xx broadly) remain in the SARIF
|
|
# report but don't gate the build — they have higher false-
|
|
# positive rates than these 4.
|
|
run: $(go env GOPATH)/bin/gosec -fmt sarif -out gosec.sarif -include=G201,G202,G304,G108 ./...
|
|
|
|
- name: osv-scanner (multi-ecosystem CVE — Phase 3 TEST-M2 hard gate)
|
|
# Phase 3 TEST-M2 closure (2026-05-13): osv-scanner promoted from
|
|
# advisory to blocking. Complements govulncheck (already blocking
|
|
# in ci.yml) by covering non-Go dependencies (npm under web/,
|
|
# any docker base image deps). Findings fail the build; the
|
|
# exact CVE list lands in osv-scanner.json as a receipt either way.
|
|
run: $(go env GOPATH)/bin/osv-scanner -r --format json --output osv-scanner.json .
|
|
|
|
# --- Race detector at -count=10 (D-002) ---
|
|
|
|
- name: go test -race -count=10 (full suite)
|
|
run: |
|
|
go test -race -count=10 -short ./... 2>&1 | tee go-test-race.txt
|
|
continue-on-error: true
|
|
|
|
# --- Coverage receipts for crypto cluster (H-005) ---
|
|
|
|
- name: go test -cover (crypto cluster)
|
|
run: |
|
|
go test -cover -covermode=atomic \
|
|
./internal/crypto/... \
|
|
./internal/pkcs7/... \
|
|
./internal/connector/issuer/local/... \
|
|
2>&1 | tee go-test-cover.txt
|
|
|
|
# --- Mutation testing on crypto cluster (D-003) ---
|
|
#
|
|
# Operator runbook: docs/testing-strategy.md::Mutation testing.
|
|
# Tool: go-mutesting (https://github.com/zimmski/go-mutesting). Each
|
|
# package is mutated independently; the per-package summary line
|
|
# (`The mutation score is X.YZ`) is grep-extracted into the receipt.
|
|
# Acceptance threshold: ≥80% kill ratio per package; surviving
|
|
# mutants get triaged in the project's comprehensive-audit notes/
|
|
# d003-mutation-results.md (per-mutant action item or
|
|
# equivalent-mutation justification).
|
|
|
|
- name: Install go-mutesting
|
|
run: go install github.com/zimmski/go-mutesting/cmd/go-mutesting@latest
|
|
continue-on-error: true
|
|
|
|
- name: go-mutesting (crypto cluster — Phase 3 TEST-M1 hard gate at 55%)
|
|
# Phase 3 TEST-M1 closure (2026-05-13): go-mutesting promoted
|
|
# from advisory (continue-on-error + per-package `|| true`) to
|
|
# blocking with an explicit mutation-score floor of 55%.
|
|
# Per-package summary lines emit `The mutation score is X.YZ`;
|
|
# the awk filter extracts each, and the post-loop check fails
|
|
# the step if any package drops below 0.55.
|
|
#
|
|
# Floor rationale: 55% is the starter ratio that catches major
|
|
# regressions without rejecting the audit's "this is OK" steady
|
|
# state. Raise quarterly as the test suite hardens; the floor
|
|
# change ships in the same commit that adds the strengthening
|
|
# tests so the ratchet is documented.
|
|
run: |
|
|
set -e
|
|
: > go-mutesting.txt
|
|
for pkg in ./internal/crypto/... ./internal/pkcs7/... ./internal/connector/issuer/local/...; do
|
|
echo "=== $pkg ===" | tee -a go-mutesting.txt
|
|
$(go env GOPATH)/bin/go-mutesting "$pkg" 2>&1 | tee -a go-mutesting.txt
|
|
done
|
|
# Extract every "The mutation score is X.YZ" line; fail on any
|
|
# score below 0.55. The check works against floats via awk so
|
|
# 0.55 is the literal threshold (not a percentage).
|
|
floor=0.55
|
|
fail=0
|
|
while IFS= read -r score; do
|
|
ok=$(awk -v s="$score" -v f="$floor" 'BEGIN{print (s>=f) ? 1 : 0}')
|
|
if [ "$ok" -ne 1 ]; then
|
|
echo "::error::mutation score $score below floor $floor"
|
|
fail=1
|
|
fi
|
|
done < <(grep -oE "The mutation score is [0-9.]+" go-mutesting.txt | awk '{print $NF}')
|
|
exit $fail
|
|
|
|
# --- Container + supply chain (D-001 partial, D-006 partial) ---
|
|
|
|
- name: Build certctl image
|
|
run: docker build -t certctl:deep-scan .
|
|
continue-on-error: true
|
|
|
|
- name: trivy image scan (HIGH+CRITICAL — Phase 3 TEST-M2 hard gate)
|
|
# Phase 3 TEST-M2 closure (2026-05-13): trivy promoted from
|
|
# advisory to blocking. --severity filter keeps the gate
|
|
# noise-free (LOW + MEDIUM findings stay in the JSON receipt
|
|
# but don't fail the build); --exit-code 1 makes HIGH+CRITICAL
|
|
# findings the actual gate. Trivy is the third hard deep-scan
|
|
# gate (alongside gosec + osv-scanner); ZAP / schemathesis /
|
|
# nuclei / testssl stay advisory because their false-positive
|
|
# rates on https://localhost:8443-targeted DAST runs are high.
|
|
run: |
|
|
docker run --rm -v "$PWD":/src aquasec/trivy:latest image \
|
|
--format json --output /src/trivy.json \
|
|
--severity HIGH,CRITICAL \
|
|
--exit-code 1 \
|
|
certctl:deep-scan
|
|
|
|
- name: syft SBOM
|
|
run: |
|
|
docker run --rm -v "$PWD":/src anchore/syft:latest dir:/src \
|
|
-o cyclonedx-json > syft.cyclonedx.json || true
|
|
continue-on-error: true
|
|
|
|
# --- DAST against a live stack (D-004) ---
|
|
|
|
- name: docker compose up (test stack)
|
|
run: |
|
|
docker compose -f deploy/docker-compose.yml up -d
|
|
sleep 20
|
|
continue-on-error: true
|
|
|
|
- name: ZAP baseline
|
|
uses: zaproxy/action-baseline@1e1871e84428617b969d4a1f981a8255630d54b0 # v0.10.0
|
|
with:
|
|
target: 'https://localhost:8443'
|
|
continue-on-error: true
|
|
|
|
- name: schemathesis (OpenAPI fuzz)
|
|
run: |
|
|
pip install schemathesis
|
|
schemathesis run --base-url https://localhost:8443 \
|
|
--hypothesis-max-examples=50 api/openapi.yaml || true
|
|
continue-on-error: true
|
|
|
|
- name: nuclei
|
|
run: |
|
|
docker run --rm --network host projectdiscovery/nuclei:latest \
|
|
-u https://localhost:8443 -j -o nuclei.json || true
|
|
continue-on-error: true
|
|
|
|
# --- TLS audit (D-005) ---
|
|
|
|
- name: testssl.sh
|
|
run: |
|
|
docker run --rm -v "$PWD":/data drwetter/testssl.sh:latest \
|
|
--jsonfile /data/testssl.json https://localhost:8443 || true
|
|
continue-on-error: true
|
|
|
|
- name: docker compose down
|
|
run: docker compose -f deploy/docker-compose.yml down || true
|
|
if: always()
|
|
|
|
# --- Frontend XSS / unsafe-link ruleset (D-007) ---
|
|
#
|
|
# Operator runbook: docs/testing-strategy.md::Frontend semgrep.
|
|
# Bundle 8 already verified `dangerouslySetInnerHTML` count at
|
|
# zero and the `target="_blank"` rel-noopener pin via grep
|
|
# guards in ci.yml — semgrep p/react-security adds defence in
|
|
# depth (it catches escape patterns the grep guards don't see,
|
|
# e.g., href={user_input}, eval, document.write).
|
|
|
|
- name: semgrep p/react-security (frontend)
|
|
run: |
|
|
docker run --rm -v "$PWD":/src returntocorp/semgrep:latest \
|
|
semgrep --config=p/react-security --json /src/web/src \
|
|
> semgrep-react.json 2>semgrep-react.stderr || true
|
|
continue-on-error: true
|
|
|
|
# --- Upload everything as artefacts ---
|
|
|
|
- name: Upload deep-scan receipts
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
if: always()
|
|
with:
|
|
name: security-deep-scan-${{ github.run_id }}
|
|
path: |
|
|
gosec.sarif
|
|
osv-scanner.json
|
|
go-test-race.txt
|
|
go-test-cover.txt
|
|
go-mutesting.txt
|
|
trivy.json
|
|
syft.cyclonedx.json
|
|
nuclei.json
|
|
testssl.json
|
|
semgrep-react.json
|
|
semgrep-react.stderr
|
|
retention-days: 30
|