161 lines
8.1 KiB
Markdown
161 lines
8.1 KiB
Markdown
---
|
|
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_
|