--- phase: 16-ci-dependency-audit-and-security-checks fixed_at: 2026-06-13T00:00:00Z review_path: .planning/phases/16-ci-dependency-audit-and-security-checks/16-REVIEW.md iteration: 1 findings_in_scope: 9 fixed: 6 skipped: 3 status: partial --- # Phase 16: Code Review Fix Report **Fixed at:** 2026-06-13 **Source review:** .planning/phases/16-ci-dependency-audit-and-security-checks/16-REVIEW.md **Iteration:** 1 **Summary:** - Findings in scope: 9 (CR-01; WR-01..WR-05; IN-01..IN-04 — IN-04 carries no fix) - Fixed: 6 (CR-01, WR-01, WR-02, WR-03, WR-04, WR-05, plus IN-01) - Skipped: 3 (IN-02, IN-03, IN-04 — all optional/no-action) IN-01 was applied inside the CR-01 commit (same file, `scripts/check-audit.mjs`), so it does not get a standalone commit line below but is counted as fixed. ## Validation All validation run inside the isolated review-fix worktree: - `pnpm lint` — PASS (apps/api + apps/pwa, `--max-warnings 0`). Note: the project lint scope does not include `scripts/*.mjs`, so the edited `.mjs` scripts are syntax-checked via `node -c` instead (all pass). - `pnpm typecheck` — PASS (apps/api + apps/pwa `tsc --noEmit`, PWA e2e tsconfig). - `node --test scripts/__tests__/check-audit.test.mjs` — 8 pass / 0 fail (includes 3 new expired-waiver / isWaived assertions for CR-01). - `python3 -c 'yaml.safe_load(...)'` for `.gitea/workflows/ci.yml` and `.gitea/workflows/publish.yml` — both parse OK after edits. - Working tree clean after all commits (no stray/uncommitted changes). The API DB-integration tests (`tests/routes/lists.test.ts`, `tests/lib/listAccess.test.ts`, etc.) fail with `ER_ACCESS_DENIED` in this environment because no dev MariaDB is reachable. That is pre-existing/environmental and unrelated to these fixes; no new test failures were introduced. ## Fixed Issues ### CR-01: Audit-waiver `expires` field is decorative — expired waivers never re-block **Files modified:** `scripts/check-audit.mjs`, `scripts/__tests__/check-audit.test.mjs` **Commit:** 4bb205f **Applied fix:** Added an exported `isWaived(adv, allowlist)` predicate. An allowlist entry with no `expires`, or a future `expires`, waives the advisory; an entry whose `expires` is in the past (`<= Date.now()`) is treated as absent so the High/Critical advisory re-blocks. Applied `isWaived` in BOTH `selectBlocking` and `partitionAdvisories` (replacing the bare `!allowlist[...]` checks). Added three unit tests: expired waiver re-blocks via `selectBlocking`, expired waiver re-blocks via `partitionAdvisories`, and a direct `isWaived` truth-table test (future expiry → waived, past expiry → not waived, no-expiry → waived, missing → not waived). All 8 tests pass. ### WR-01: `github.base_ref` interpolated into a shell command (script-injection vector) **Files modified:** `.gitea/workflows/ci.yml` **Commit:** 26a6b2e **Applied fix:** Bound `github.event.pull_request.base.sha`, `head.sha`, and `github.base_ref` through an `env:` block (`PR_BASE_SHA`, `PR_HEAD_SHA`, `PR_BASE_REF`) on the "Probe PR base/head SHA" step. The `run:` body now references only the already-quoted shell variables — no `${{ ... }}` context interpolation inside the script. The merge-base fallback uses `git rev-parse "origin/$PR_BASE_REF"`. ### WR-03: `HEAD_SHA` has no fallback while `BASE_SHA` does — asymmetric defense **Files modified:** `.gitea/workflows/ci.yml` **Commit:** 26a6b2e (committed together with WR-01 — same step in the same file) **Applied fix:** Added a symmetric head fallback (`if [ -z "$HEAD_SHA" ]; then HEAD_SHA=$(git rev-parse HEAD); fi`) and an `echo "Secret-scan range: ${BASE_SHA}..${HEAD_SHA}"` line before the gitleaks invocation so the scanned range is logged rather than relying on git's `A..` → `A..HEAD` default. ### WR-02: Boot-smoke false-PASS if a regressed image emits ≥20 lines before binding **Files modified:** `.gitea/workflows/publish.yml` **Commit:** 3daa351 **Applied fix:** Captured `docker run` output into `OUT=$(...)` and read `EXIT=$?` from the docker command directly (no `| head -20` in the exit-bearing command), so a chatty-but-booting image can no longer SIGPIPE docker to exit 141 and false-PASS. `head -20` is now applied only to the printed `echo "$OUT"`. Treat both `0` and `124` as FAIL ("did not refuse boot"). Added a positive belt-and-suspenders assertion: the output must contain `DEV_AUTH_BYPASS=true is set in a production environment` (the exact D-08 guard marker from `bootGuards.ts`), so a refusal for an unrelated reason cannot masquerade as the guard working. > Logic note: this change alters the PASS/FAIL decision logic of a security smoke > test. The shell logic was reviewed against the guard marker string, but the actual > container behavior under the forbidden env is not exercisable in this environment > (no docker daemon / built image). Recommend a human confirm the smoke step on a > real publish run. ### WR-05: Static `.dockerignore` assertions use unquoted-regex `grep` (false-positive prone) **Files modified:** `.gitea/workflows/publish.yml` **Commit:** 3daa351 (committed together with WR-02 — same file) **Applied fix:** Switched the per-pattern assertion to comment-stripped, fixed-string matching: `grep -v '^[[:space:]]*#' .dockerignore | grep -qF "$pattern"`. Patterns are no longer treated as regexes (`.env` can't match `denv`) and a commented-out rule (`# .env was here`) no longer satisfies the check. Failure message updated to "missing active rule". ### WR-04: AUDIT-ADVISORY tier in the outdated report is effectively dead code **Files modified:** `scripts/check-outdated.mjs` **Commit:** 3e609b2 **Applied fix:** Took the SAFE relabel option (no risky full-tree rewrite). Renamed the tier from `AUDIT-ADVISORY` to `OUTDATED-WITH-ADVISORY` and documented, in the file header, the inline classification comment, and the printed header text, that it only matches outdated *direct* deps against advisory `module_name`s (most advisories are on transitive deps, so it rarely fires) and that the authoritative advisory gate is `check-audit.mjs`. The report still always exits 0 (advisory-only). The audit-parse-failure warning message was updated to the new tier name. ## Skipped Issues ### IN-02: `pnpm audit --json` is run twice per CI security job **File:** `scripts/check-audit.mjs:84`, `scripts/check-outdated.mjs:65` **Reason:** Skipped — explicitly optional and out of v1 performance scope per REVIEW.md ("Out of v1 performance scope and harmless"). The suggested fix (pipe one audit pass to both scripts via stdin/arg, or merge the two scripts) is a structural change to script interfaces and CI invocation with no correctness benefit; applying it here would be speculative scope creep. **Original issue:** Both scripts independently spawn `pnpm audit --json`, doubling the audit work in the security job. ### IN-03: `outdated-pins.json` reasons are not cross-checked against the audit allowlist **File:** `scripts/outdated-pins.json` / `scripts/audit-allowlist.json` **Reason:** Skipped — explicitly optional ("Documentation-level coupling only"). Both JSON files are flat maps that the consuming scripts iterate directly: `check-outdated.mjs` reads `pins[pkgName]` as a pin reason, and `check-audit.mjs` reads `allowlist[github_advisory_id]`. Injecting a meta `__note`/cross-reference key risks the consumers misreading it as real data (a `__note` pin would be treated as a pin reason if a package were ever named `__note`). The safer choice is to leave the data files as pure data rather than add inert-but-fragile meta keys. The suggested lint-step variant is net-new tooling, out of scope for a review fix. **Original issue:** Two independent suppression lists with no linkage between a pinned package and an audit waiver for the same package. ### IN-04: `expand.test.ts` is in scope but unrelated to this CI/security phase **File:** `apps/api/tests/broker/expand.test.ts` **Reason:** Skipped — no action required. REVIEW.md states "**Fix:** None." The reviewer found no defects; the file appears in the review set only because it was touched/moved and is orthogonal to this phase. **Original issue:** Well-constructed test noted for completeness; no defect. --- _Fixed: 2026-06-13_ _Fixer: Claude (gsd-code-fixer)_ _Iteration: 1_