ci(security): flag <30d-old packages introduced by a PR + allowlist #136
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol#136
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Carol's existing supply-chain defences address one specific threat: a new malicious version of an established package. ADR-0009's Renovate
minimumReleaseAge: "7 days"quarantine, ADR-0010's install-script allowlist, and the existing scanner suite (npm audit, OSV, Trivy, gitleaks) all target version-level compromise of known dependencies.They do NOT address the orthogonal threat: a fresh malicious package. Typosquats, dependency-confusion attacks, and brand-new sleeper packages are by definition newly-published. Renovate's version-age quarantine doesn't help — it gates by release date of a version, not by when the package first existed on npm. A new transitive pulled in via a feature PR's lockfile resolution flows past every existing gate.
Recent supply-chain attacks (the npm
xzanalogue cases) have consistently shown that brand-new packages are the most common shape. A 30-day "package must have existed for a month" policy catches the bulk of these without meaningfully delaying legitimate work.Scope
scripts/lib/package-ages.mjs— pure logic, no I/O:extractPackagesFromLockfile(lockfile)— walks npm v3packagesmap, returns unique package names with their first-seen version (handles scoped names like@scope/name).newPackages(base, current)— set difference: packages incurrentbut not inbase.flagOffenders({ packageMetas, allowlist, thresholdDays, now })— pure filter.fetchPackageMeta(name, fetchImpl)— registry call (one I/O surface, injectable for tests).scripts/ci/check-package-ages.mjs— wraps the lib. Reads currentpackage-lock.json+ base viagit show origin/<base>:package-lock.json+.dep-policy.json. Querieshttps://registry.npmjs.org/<pkg>fortime.createdon each new package. Writes Markdown to a body file. Printsflag=<bool>.scripts/ci/post-deps-comment.mjs— sticky-comment poster. Marker<!-- deps-comment -->. Scanner-pattern delete-on-clean (matches ADR-0016): no offenders → no comment. Differs from coverage's always-post..dep-policy.jsonat repo root. Format:.forgejo/workflows/pr.yml—package_ages— runs on PRs,fetch-depth: 0so it cangit show origin/<base>:package-lock.json. Never blocks the workflow (soft signal, same shape as coverage).tests/scripts/package-ages.test.ts— unit tests for the pure lib. Cover lockfile parsing (root entry skipped, scoped names handled, no-duplicates),newPackagesset difference,flagOffenders(allowlist hits, threshold boundary, no-offenders empty result). MockfetchPackageMetavia the injectablefetchImpl.docs/ci.mdPackage-ages section.CLAUDE.md"Working in this repo" note + cross-reference.Out of scope
Acceptance
.dep-policy.jsonin the same PR (or a follow-up commit) causes the comment to disappear on the next CI run.