Files
familysync/.planning/phases/16-ci-dependency-audit-and-security-checks/16-REVIEW-FIX.md
T

8.1 KiB

phase, fixed_at, review_path, iteration, findings_in_scope, fixed, skipped, status
phase fixed_at review_path iteration findings_in_scope fixed skipped status
16-ci-dependency-audit-and-security-checks 2026-06-13T00:00:00Z .planning/phases/16-ci-dependency-audit-and-security-checks/16-REVIEW.md 1 9 6 3 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_names (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