Covers the typosquatting gate (#588) and the transitive vulnerability scanner (#589).
- Script:
scripts/detect-typosquatting.js— Levenshtein distance over every name inpackage.json+server/package.jsonvs. a curated popular-package list. Distance 1 flags (distance 2 only for names ≥ 8 chars; bases < 5 chars skipped to avoid generic-base noise like@playwright/testvsjest). - Maintainer check:
node scripts/detect-typosquatting.js --check-maintainersqueries npm metadata with a 5 s timeout. Offline/network failures warn only. - Gate:
npm run security:typosquat(also in CIsupply-chainjob). Exit 1 = PR blocked. Triage: verify the flagged name on npmjs.com, check publish age and maintainers, then rename or exception-document.
- Script:
scripts/transitive-vulnerability-scanner.js— builds the DAG frompackage-lock.json(node_modulesnesting = depth), maps advisories in OSV shape onto exact versions, isolates hits at depth ≥ 5, and suggestspackage.jsonoverrides. - Offline default:
npm run security:transitive-vulnreports graph stats (currently 1271 packages, max depth 4) and exits 0 with no advisories. - Live OSV: append
--audit(best-effort, warns offline). Pinned advisories:--advisories <file>. CRITICAL/HIGH hits exit 1. - Cargo side:
contracts/**/Cargo.lockare resolved withstellar contract build/cargo test --locked; suggested[patch]entries go through the same reviewer flow as npmoverrides— never commit either without review.
Verified emergency-service callers bypass the API rate limiter. Entries live in
Redis (REDIS_URL) so they change at runtime with no redeploy; without
REDIS_URL an in-process store is used (single instance, lost on restart —
dev/test only).
- Code:
server/middleware/whitelist.ts(matching + admin API),server/lib/redis.ts(optional client), limiter hook inserver/middleware/rateLimiter.tsandcreateRateLimiterinserver/index.js. - Match rules: request IP inside a whitelisted IPv4/IPv6 CIDR, or an
X-API-Keyheader whose SHA-256 is registered. Match setsreq.bypassRateLimit; nothing else is relaxed (CORS, auth and body limits still apply). - Set
TRUST_PROXY(1on Render) soreq.ipis the real client. If it is wrong, either every caller looks like the proxy orX-Forwarded-Forcan be spoofed to fake a whitelisted IP. - Fail closed: if Redis is unreachable the normal limit applies. Subnet lists are cached ~5 s per worker, so changes on other workers take up to that long.
Requires Authorization: Bearer $WHITELIST_ADMIN_TOKEN; returns 503 if the
token is unset and 401 on a bad token.
| Method & path | Body | Effect |
|---|---|---|
GET /admin/whitelist |
– | List subnets and API-key metadata |
POST /admin/whitelist/subnets |
{ "cidr": "203.0.113.0/24", "label": "County 911" } |
Add a subnet |
DELETE /admin/whitelist/subnets |
{ "cidr": "203.0.113.0/24" } |
Remove a subnet |
POST /admin/whitelist/api-keys |
{ "label": "Dispatch CAD" } |
Create a key; the plaintext is returned once and only its hash is stored |
DELETE /admin/whitelist/api-keys/:id |
– | Revoke a key by id |
Rotate WHITELIST_ADMIN_TOKEN like any other secret; treat a leaked API key as
revoked immediately.
- Script:
scripts/audit-deps.js— readspackage-lock.jsonandserver/package-lock.json; shares its license matrix withscripts/security/license_compliance.js(license_policy.js). Writes a deterministiclicenses.json(no timestamps). - Gate:
npm run security:audit-deps:check(CIsupply-chainjob). Exit 1 = PR blocked. Server-only run:npm run audit:deps --workspace server. - DENIED — GPL/AGPL/SSPL/EUPL/OSL/CPAL/RPL. Replace the dependency; only
add to
EXCEPTIONSinlicense_policy.jsafter legal review. Weak copyleft (LGPL/MPL/...) is a REVIEW warning;--strictpromotes it to a failure. - INSTALL-SCRIPT — a package gained a lifecycle script. Read the script
(
npm view <pkg>@<ver> scripts); if legitimate, add it toINSTALL_SCRIPT_ALLOWLISTwith a reason.suspicious-install-scriptmeans the installed body matched curl|sh, eval, base64, remote URL or env-exfil patterns — treat as a possible compromise: do not install, pin the previous version and report upstream. - SOURCE — resolved tarball is off the trusted registries (or http/git), or integrity is missing/weak. Usually a tampered lockfile: regenerate it from a clean checkout and diff.
- STALE —
licenses.jsonno longer matches the lockfiles; runnpm run security:audit-depsand commit.