SEC-01/02, DEP-01/02, IMG-01/02/03, CI-03. Wave 1: image-hygiene runtime guard (TDD), audit+outdated wrappers (TDD), eslint-plugin-security fold, gitleaks config+baseline+.dockerignore. Wave 2: ci.yml security job + gate wiring, publish.yml hygiene assertions + boot-smoke. esbuild GHSA-gv7w-rqvm-qjhr waivered in 16-02 before the gate goes live.
140 lines
11 KiB
Markdown
140 lines
11 KiB
Markdown
---
|
|
phase: 16-ci-dependency-audit-and-security-checks
|
|
plan: 05
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["16-02", "16-03", "16-04"]
|
|
files_modified:
|
|
- .gitea/workflows/ci.yml
|
|
autonomous: true
|
|
requirements: [CI-03, SEC-01, DEP-01, DEP-02]
|
|
must_haves:
|
|
truths:
|
|
- "A new security job runs in parallel with fast-checks: gitleaks scans every PR (including doc-only), while pnpm audit + pnpm outdated run only when changes.outputs.code is true"
|
|
- "The gitleaks PR-diff scan is blocking and uses the committed config + baseline; the base.sha availability assumption is probed before the scan relies on it, with a merge-base fallback"
|
|
- "The security job is wired into the gate aggregator with an individual needs.security.result check that requires success (not success-or-skipped), since gitleaks always runs"
|
|
artifacts:
|
|
- path: ".gitea/workflows/ci.yml"
|
|
provides: "security job (gitleaks + check-audit.mjs + check-outdated.mjs) + updated gate"
|
|
contains: "security:"
|
|
key_links:
|
|
- from: ".gitea/workflows/ci.yml"
|
|
to: "scripts/check-audit.mjs"
|
|
via: "node scripts/check-audit.mjs step (code-gated)"
|
|
pattern: "check-audit"
|
|
- from: ".gitea/workflows/ci.yml gate"
|
|
to: "security job"
|
|
via: "needs.security.result == success check"
|
|
pattern: "needs.security.result"
|
|
---
|
|
|
|
<objective>
|
|
Add a dedicated `security` job to the existing PR workflow (`.gitea/workflows/ci.yml`) — parallel to `fast-checks` — that runs gitleaks on every PR (blocking, D-12) and runs `check-audit.mjs` (blocking on unwaived High+Critical, D-04) and `check-outdated.mjs` (advisory-only, D-06) only on code/lockfile-changing PRs. Then wire `security` into the `gate` aggregator with an individual `needs.security.result` check (D-14 / D-15). This realizes the D-11 gating posture: gitleaks and pnpm audit High+Critical are blocking; pnpm outdated is advisory and never gates.
|
|
|
|
Purpose: Centralizes the new PR-time security/dependency checks into one isolated, parallel job so a secret-leak or unwaived advisory is clearly attributable and does not pollute fast-checks. This is ADDITIVE — it does not restructure the existing changes/fast-checks/api/harness/gate topology. eslint-plugin-security is NOT a step here (it already runs inside the existing fast-checks `pnpm lint` via 16-03 — this plan only relies on that).
|
|
|
|
Output: The modified `ci.yml`. Consumes the scripts/config from 16-02 (check-audit.mjs, check-outdated.mjs), 16-03 (eslint-plugin-security already in the lint step), and 16-04 (.gitleaks.toml, gitleaks-baseline.json). Honors all Gitea runner constraints: no actions/cache, ubuntu-latest, set -euo pipefail, REGISTRY_PAT naming (n/a here), individual needs.X.result (Gitea #31007).
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md
|
|
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md
|
|
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md
|
|
@.gitea/workflows/ci.yml
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add the security job (gitleaks always; audit/outdated code-gated) with a base.sha probe</name>
|
|
<read_first>
|
|
- .gitea/workflows/ci.yml (existing job skeletons: fast-checks lines 33-66, api conditional pattern lines 68-72, the `changes`/paths-filter job lines 8-31 — needs.changes.outputs.code)
|
|
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md (the full `security` job skeleton; set -euo pipefail convention; no actions/cache rule; node: import convention)
|
|
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Secret Scanning section — gitleaks v8.30.1 install via curl|tar; `gitleaks git --log-opts="--no-merges BASE..HEAD" --config .gitleaks.toml --baseline-path scripts/gitleaks-baseline.json --exit-code 1`; fetch-depth:0 requirement; Pitfall 3; Assumption A2 + Open Question 1 — base.sha may be empty on Gitea, fallback `git merge-base $(git rev-parse origin/${{ github.base_ref }}) HEAD`; the security job needs:[changes], if pull_request)
|
|
- scripts/check-audit.mjs and scripts/check-outdated.mjs (created in 16-02 — invoked here)
|
|
- .gitleaks.toml and scripts/gitleaks-baseline.json (created in 16-04 — referenced here)
|
|
</read_first>
|
|
<action>
|
|
In ci.yml add a new `security` job (placed after `harness`, before `gate`) with `runs-on: ubuntu-latest`, `needs: [changes]`, `if: github.event_name == 'pull_request'`. Steps in order: (1) actions/checkout@v4 with `fetch-depth: 0` (Pitfall 3 — base.sha must be local). (2) A "Probe PR base/head SHA" step (always runs) that echoes `github.event.pull_request.base.sha` and `head.sha`, computes `BASE_SHA` from the event context and, if empty, falls back to `git merge-base "$(git rev-parse origin/${{ github.base_ref }})" HEAD`, exporting BASE_SHA and HEAD_SHA to $GITHUB_ENV (Assumption A2 / OQ-1). (3) Install gitleaks: `set -euo pipefail`, pin VERSION=8.30.1, curl the linux_x64 tarball, tar -xz gitleaks, chmod +x, mv to /usr/local/bin. (4) "Secret scan (PR diff, blocking)" always-runs: `set -euo pipefail`, run `gitleaks git --log-opts="--no-merges ${BASE_SHA}..${HEAD_SHA}" --config .gitleaks.toml --baseline-path scripts/gitleaks-baseline.json --report-path /tmp/gitleaks-pr-report.json --exit-code 1`. (5) actions/setup-node@v4 node 22, (6) corepack enable pnpm, (7) pnpm install --frozen-lockfile, (8) `node scripts/check-audit.mjs`, (9) `node scripts/check-outdated.mjs` — steps 5-9 EACH carry `if: needs.changes.outputs.code == 'true'` (step-level, NOT job-level — D-12 so gitleaks still runs on doc-only PRs). Do NOT add actions/cache. Every multi-line run block starts with `set -euo pipefail`. Commit: `ci(16-05): add security job (gitleaks always; audit/outdated code-gated)`.
|
|
</action>
|
|
<verify>
|
|
<automated>command -v yq >/dev/null 2>&1 && yq '.jobs.security' .gitea/workflows/ci.yml >/dev/null || python3 -c "import yaml,sys; d=yaml.safe_load(open('.gitea/workflows/ci.yml')); j=d['jobs']['security']; assert j['needs']==['changes']; print('security job parses OK')"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- ci.yml has a `security` job with `needs: [changes]`, `if: github.event_name == 'pull_request'`, and `fetch-depth: 0` checkout
|
|
- A base/head SHA probe step computes BASE_SHA with a `git merge-base` fallback when the event context is empty
|
|
- gitleaks install + scan steps have NO `if:` (always run, D-12); the gitleaks scan references --config .gitleaks.toml --baseline-path scripts/gitleaks-baseline.json --exit-code 1
|
|
- The pnpm/setup-node/check-audit/check-outdated steps each carry `if: needs.changes.outputs.code == 'true'`
|
|
- No `actions/cache` in the security job; every `run: |` block starts with `set -euo pipefail`
|
|
</acceptance_criteria>
|
|
<done>The security job runs gitleaks unconditionally and the dependency checks behind the code filter, with a base.sha probe + fallback.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire the security job into the gate aggregator (individual needs.security.result check)</name>
|
|
<read_first>
|
|
- .gitea/workflows/ci.yml (the gate job lines 345-367 — `needs: [fast-checks, changes, api, harness]`, the individual needs.X.result checks, the #31007 wildcard-bug comment)
|
|
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md (Updated gate needs list + security check; the rule that security must require SUCCESS, not "success OR skipped", because gitleaks always runs)
|
|
</read_first>
|
|
<action>
|
|
In ci.yml update the `gate` job: add `security` to its `needs:` list (so it becomes `needs: [fast-checks, changes, api, harness, security]`). In the gate shell script, add a NEW individual check after the existing fast-checks check and BEFORE the `for result in ... api ... harness` loop: if `needs.security.result != success` then echo the result and `exit 1`. Do NOT add security to the success-OR-skipped loop (the api/harness loop) — security always runs (gitleaks is unconditional), so it must strictly require success per Gitea #31007 individual-check convention. Leave the api/harness loop unchanged. Commit: `ci(16-05): wire security job into gate aggregator`.
|
|
</action>
|
|
<verify>
|
|
<automated>python3 -c "import yaml; d=yaml.safe_load(open('.gitea/workflows/ci.yml')); g=d['jobs']['gate']; assert 'security' in g['needs'], 'security not in gate needs'; print('gate needs OK')" && grep -q "needs.security.result" .gitea/workflows/ci.yml && echo CHECK-OK</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- gate `needs:` includes `security`
|
|
- The gate script has an individual `needs.security.result` check that exits 1 unless it equals `success`
|
|
- security is NOT folded into the api/harness success-or-skipped loop
|
|
- The existing fast-checks / api / harness gate logic is unchanged
|
|
</acceptance_criteria>
|
|
<done>The gate requires the security job to succeed via an individual result check, consistent with the #31007 workaround.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| PR author → main branch | The PR workflow is the enforcement point before code reaches the trusted main branch |
|
|
| CI runner network → external download | gitleaks binary is fetched from GitHub releases at a pinned version |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-16-15 | Information Disclosure | A secret introduced in a PR diff (including a doc/config-only PR) reaches main | mitigate | D-02/D-12: gitleaks runs unconditionally in the security job on every PR with --exit-code 1 (Task 1); gate requires security success (Task 2) |
|
|
| T-16-16 | Tampering | Unwaived High/Critical dependency advisory merges to main | mitigate | D-04: check-audit.mjs runs code-gated and exits 1 on unwaived High+Critical (Task 1); gate blocks |
|
|
| T-16-17 | Repudiation | gitleaks silently scans nothing (fetch-depth:1 → empty base.sha → no commits in range → exit 0) | mitigate | Pitfall 3 + A2: fetch-depth:0 + a base.sha probe with merge-base fallback (Task 1) ensures the diff range is real |
|
|
| T-16-SC | Tampering | gitleaks binary download from GitHub releases could be substituted | mitigate | Version pinned to 8.30.1; download from the official gitleaks/gitleaks releases path (Task 1). Note: no checksum verification this phase — accepted residual for a pinned tag from the canonical source |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- ci.yml parses as valid YAML; `jobs.security` exists with needs:[changes]
|
|
- gitleaks steps have no `if:`; audit/outdated steps are code-gated
|
|
- gate `needs:` includes security and the script has an individual needs.security.result==success check
|
|
- No actions/cache; set -euo pipefail on every new multi-line run block
|
|
- Live proof (a deliberate-secret PR failing the gate, and a doc-only PR still running gitleaks) is a phase-verification manual check, not a unit test
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- security job added parallel to fast-checks; gitleaks always, audit/outdated code-gated
|
|
- base.sha probe + merge-base fallback present
|
|
- gate requires security success via an individual result check
|
|
- Fully additive — existing topology untouched; all runner constraints honored
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-05-SUMMARY.md` when done.
|
|
</output>
|