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.
- Script:
scripts/license-compliance.jsscans npm (package-lock.json) and Cargo (cargo metadata) licenses and evaluates SPDX expressions. CI fails on strong copyleft (GPL/AGPL/SSPL/…) that isn't listed inLICENSE_EXCEPTIONS. npm run security:license-gateruns the same check as CI.npm run licenses:generateregenerateslicenses.json.- Policy, exception process and triage:
docs/legal-compliance.md.
- Script:
scripts/auto-patch-cve.js. It reads GitHub Security Advisories, either throughnpm audit(the default, backed by the GitHub Advisory DB) or the GitHub REST/advisories?affects=API (--source github). It then plans the minimum fixed version for every vulnerablename@versionin the lockfile: the lowest published, non-deprecated, non-prerelease version that no known advisory matches, preferring the same major. - Strategy:
- For a direct dependency, the bot bumps the range in
package.jsonorserver/package.jsonand keeps the existing^/~style. - For a transitive dependency, it writes a root
overridesfloor ("pkg": "^fixed") when every installed copy is on the fixed major. Otherwise it writes a version-keyed override ("pkg@1.2.3": "1.2.4") so copies on other majors stay untouched. - Fixes that need a major bump are listed for a human. Pass
--allow-majorto apply them.
- For a direct dependency, the bot bumps the range in
- Modes:
npm run security:cve-patchprints the plan only.npm run security:cve-patch:applywrites the manifests, runsnpm install, confirms that the lockfile andnpm auditno longer report the patched advisories, then runs every--verify <cmd>(defaultnpm test). If any step fails, it restorespackage.json,server/package.jsonandpackage-lock.json.--open-pralso commits the change onsecurity/cve-patch-*, pushes it, and opens a PR with the advisory table, verification results and the follow-up list.
- Automation:
.github/workflows/cve-patch-bot.ymlruns weekly and on manual dispatch. PRs opened withGITHUB_TOKENdon't trigger CI, so set aCVE_BOT_TOKENsecret (fine-grained PAT or GitHub App) to get checks on bot PRs. CI'ssupply-chainjob runs the plan in report-only mode and fails only on critical advisories. - Triage for a bot PR:
- Review the advisory links.
- Check that the version moves are patch or minor.
- Merge.
- Handle "Needs manual follow-up" items as separate PRs, for example a dependency upgrade that removes the vulnerable package.
- Policy:
crypto-pins.jsonlists the Barretenberg/Noir packages (@aztec/bb.js,@noir-lang/*) with their exact pinned version, tier, rationale, ADR reference and last review date. - Gate:
npm run security:crypto-pins(scripts/check-crypto-pins.js, also a step of CI'ssupply-chainjob and a--verifystep of the CVE patch bot) fails with a readable explanation when:- a pinned package resolves to another version anywhere in
package-lock.json; - the
@noir-langfamily resolves to mixed versions; - a
barretenbergcrate inCargo.lock(none today) differs from the pinned@aztec/bb.js; - a pin lacks a rationale or review date.
- a pinned package resolves to another version anywhere in
- Stale pins: a pin not reviewed for more than
staleAfterDays(180) is reported asSTALE;--strict-staleturns that into a failure. - Bot behaviour: packages in the
crypto-coretier are never applied byscripts/auto-patch-cve.js. They appear under "Needs manual follow-up" and still count towards--fail-on. - ABI checklist for a deliberate crypto-core bump (one PR):
- Bump
package.json/package-lock.jsonand any coupled Cargo pin together. - Recompile the circuits and run the real prove/verify path, not a mock.
- Update
version,rationaleandreviewedincrypto-pins.json. - Roll back by reverting that single PR; never revert only one lockfile.
- Bump
- Not covered here: unmocked prover smoke tests in CI, staged PWA rollout of WASM changes, and typosquat-hold ordering for the bot.
-
Script:
scripts/security/verify_maintainer_keys.js, configured byconfig/maintainer-keys.json. It verifies OpenPGP signatures on commits (--range), annotated tags (--tags <glob>) and release artifacts (--artifact f --signature f.asc) in-process. It includes an RFC 4880 parser and uses node:crypto for RSA, EdDSA and ECDSA, so no gpg is needed. -
Revocation data is live. Keys are fetched from keys.openpgp.org and keyserver.ubuntu.com, merged, and cached in
.cache/maintainer-keys/forcacheTtlHours.--refreshforces a refetch and--offlineuses the cache only. Revocation certificates are verified cryptographically, so a forged one is ignored. -
Revocation rules (RFC 4880 §5.2.3.23):
Reason Signatures before the revocation Signatures after 0x02 key compromised / 0x00 no reason invalid invalid 0x01 superseded / 0x03 retired valid invalid This is the case the tool exists for. A release signed before anyone discovered that the key was compromised still fails.
-
Web of trust: a signer is trusted when it is a
trustedKeysroot, or when it has at leastminCertificationsvalid, unrevoked certifications from a root. Certifications from a root that was itself revoked as compromised don't count. The roots are the GitHub web-flow keys, which cover merges made in the UI; add maintainer keys as the team adopts signing. -
dependencies[]pins critical upstream maintainers' key fingerprints ({ "name", "fingerprints": [...] }).--pinnedchecks each key against the live revocation lists:- compromised: FAIL (re-verify every release that key signed)
- superseded: WARN (pin the successor key)
-
Gates:
- Bad signatures, compromised keys and revoked trusted roots always fail.
- Unsigned, unknown-key, untrusted and SSH-signed objects warn, and fail
under
--strict.
-
Where it runs:
- CI
supply-chain: PR commit range. verify-keys.yml: pushes, release tags, and a nightly--refreshsweep of everyv*tag and all pinned keys.slsa-provenance.yml: before release builds. Set the repository variableREQUIRE_SIGNED_RELEASES=trueto make release tags--strict.
- CI
-
If it fails on a compromised key:
- Freeze releases.
- Identify every artifact signed by that fingerprint (the
signerfield in--jsonoutput). - Rebuild and re-sign them with a fresh key.
- Rotate
trustedKeys/dependencies[].
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.
Detects and blocks malicious network egress (data exfiltration) while the
build pipeline runs: dependency installation and npm run build.
- Script:
scripts/monitor-build-egress.sh [options] [--] <command>— starts a capture, runs the command, then classifies every captured destination.--classify [file]re-audits an existing capture log (this is what the tests exercise). - Backends (
--backend auto|tcpdump|iptables|none):- tcpdump —
tcpdump -i any -nn -l -Q out 'ip or ip6'; usessudo -nwhen not root. This is the CI backend. - iptables — root only. Installs an
EGRESS-MONITORchain onOUTPUTthat RETURNs loopback/established/allowlisted destinations, LOGs the rest with theEGRESS-DENY:prefix, and with--enforceREJECTs them so the build is blocked at the socket level. The chain is removed on exit. - none — no capture.
--strictfails the run; otherwise the build continues with a warning (Render uses this: noCAP_NET_RAW).
- tcpdump —
- Classification rules, in order:
- deny list wins —
169.254.169.254and friends plusmetadata.google.internal/instance-dataare always unauthorized; - an allowlisted hostname in the same line legitimises the flow (DNS query or SNI), so CDN IPs are accepted without pinning them;
- destination IP inside an allowlisted CIDR → allowed;
- anything else →
UNAUTHORIZED(reasonip-not-allowlisted/domain-not-allowlisted) and exit 1.
- deny list wins —
- Built-in allowlist: loopback,
10/8,172.16/12,192.168/16, CGNAT100.64/10, ULA/link-local IPv6,/etc/resolv.confnameservers, and registry/documentation hosts (npm, GitHub, PyPI, crates.io, nodejs.org). Allowlisted domains are resolved up-front (parallel, 3 s bound per lookup) so their current IPs are covered too. - Extending the policy:
EGRESS_ALLOWLIST(comma separated), anEGRESS_ALLOWLIST_FILE/--allowlist-file(one entry per line,#comments), or theEGRESS_ALLOWLISTenv var Render exposes. Never widen a broad CIDR (e.g. all of0.0.0.0/0) to silence a finding — investigate first. - Artifacts (uploaded by CI as
build-egress-audit):artifacts/build-egress/<step>/egress-capture.log(raw),egress-audit.log(one verdict per line) andegress-summary.log(counts, allowlist size, build exit code, verdict). - Gates:
npm run security:egress— runnpm run buildunder the monitor;npm run security:egress:strict— also fail when capture is unavailable;npm run security:egress:audit— re-classify a saved capture;- CI job
build-egress-monitor— strict install gate, then the production build with--ignore-command-exitso this job owns the network verdict (the app build's own exit code is recorded in the summary only); render.yamlbuildCommand— the deploy build runs with--allow-no-capturebecause Render build containers cannot capture.
- Triage of a finding: open
egress-audit.log, take thedst=/domain=fields. If it is a legitimate build vendor, add it to the allowlist with a reason in the same PR. If it is not explainable, treat the dependency as compromised: do not publish the artifact, pin the previous version, rotate any secrets present in the build environment, and report upstream.
- CI/CD build egress transcript records unexpected DNS query (
*.exfil.invalidor high-entropy subdomain). - Query-string beacon (
?canary=...or param entropy > 4.0) observed on allowed or unallowed hosts. - Unauthorized outbound SYN/TCP connection during
installorbuildphase. - Leaked Stellar seed or Mapbox token detected during artifact scan.
- Immediate Release Freeze:
- Abort current pipeline run; reject release artifact signing.
- Quarantine candidate deployment in Render / GitHub Actions.
- Stellar Key Compromise Protocol:
- Rotate master operator and hot wallet signing keys immediately:
- Generate new Ed25519 seed keypair via offline HSM / secure vault.
- Fund new public key on Soroban network.
- Transfer admin privileges via
set_admincontract invocation with multi-sig approval. - Revoke compromised signing public key from contract maintainer set.
- Rotate master operator and hot wallet signing keys immediately:
- Mapbox & Infrastructure Token Revocation:
- Revoke compromised Mapbox secret token (
sk.*) in Mapbox Developer Console. - Issue replacement token with strict scope limited to HelPhone vector tileset URLs.
- Update Render and GitHub secret vaults.
- Revoke compromised Mapbox secret token (
- Attestation & Post-Mortem:
- Produce audited egress transcript hash and compare against clean build baseline.
- File CVE advisory and incident bundle per
docs/disaster-recovery.md.