---
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_
