Releases: TableCheck-Labs/codeartifact-shield
Release list
v0.9.0: workspace fix + exotic-source detection + attestation verification with signer pinning
What's new
Fix: npm v7+ workspace declaration entries
Workspace path keys (e.g. system/i18n) without link: true were slipping past the existing filter and getting probed against the registry, surfacing as bogus private_blocked / unaudited_private / orphan findings. Added is_installable_entry() guard across all 5 consumer modules (audit, cooldown, scripts, registry, drift).
cas registry --fail-on-exotic
Unified flag that fails the build on git-sourced, tarball-sourced, or file-sourced entries. Tarball detection identifies HTTPS URLs that bypass the npm registry API (no /-/ in path). Implies --fail-on-git.
cas trust — attestation + provenance verification
New subcommand that audits npm attestation trust levels and detects downgrades.
Policies:
--policy audit— report trust levels--policy no-downgrade— fail if trust degraded vs. previous version--policy require-provenance— fail if any package lacks provenance
Sigstore signature verification (--verify-signatures):
Verifies sigstore certificate chain + transparency log inclusion for provenance attestations. Requires pip install codeartifact-shield[trust].
Signer pinning (--signers-file):
Per-package identity enforcement. Records the exact GitHub Actions workflow (SAN URI + OIDC issuer) that built each package. On subsequent runs, any identity change — different workflow, different issuer, or lost provenance — is a CRITICAL failure.
# Bootstrap: learn signers from known-good lockfile
cas trust package-lock.json --update-signers --signers-file .cas-signers.json
# CI: enforce pinned signers
cas trust package-lock.json --verify-signatures --signers-file .cas-signers.jsonv0.8.0
Multi-endpoint OSV — point cas at as many vuln sources as you want
cas audit accepts --osv-endpoint URL (repeatable). Every endpoint must speak the OSV HTTP contract (POST /v1/querybatch + GET /v1/vulns/{id}). Default is unchanged: a single endpoint pointing at https://api.osv.dev.
Pair it with cas-server to publish your own SOC advisories that cas audit consumes alongside OSV.dev — every advisory you merge surfaces as a build-failing finding within minutes, without waiting for OSV/GHSA upstream to absorb a CVE.
cas audit ./package-lock.json \
--osv-endpoint https://api.osv.dev \
--osv-endpoint https://cas-server.internal \
--min-severity highEnv: CAS_OSV_ENDPOINTS (whitespace-separated).
How it scales
- Flattened parallelism. The batch query is dispatched across the
(endpoint × chunk)cross product —Eendpoints ×Cchunks → up toE·Cin-flight requests, capped by--max-workers(default bumped 20 → 32). Adding endpoints costs almost nothing in wall time. - Resilient fallthrough. One endpoint down + at least one answering → the build succeeds against the answering set. All endpoints down → a single consolidated
network_error. - Detail-fetch fallback. The first endpoint that returned a vuln id is the preferred source for the detail; on transient failure cas falls back to other endpoints that also returned it.
Cross-source dedup
https://cas-server.internal publishes EX-2026-0001 with aliases: ["GHSA-abcd-..."]; https://api.osv.dev returns plain GHSA-abcd-... for the same (pkg, ver). cas emits one finding. Canonical vuln_id is the lex-smallest in the alias-overlap group; the other id appears in the merged aliases list; severity is the max across the group. New AuditFinding.source field records which endpoint surfaced the canonical — visible in both human and JSON output.
Performance
Measured against a real 2521-package lockfile:
| Mode | Single endpoint | Two endpoints |
|---|---|---|
| Cold cache | ~10s | ~9s |
| Warm cache | ~4s | <1s |
Versioned allowlist
Every --allow / --allow-private flag on cas cooldown, cas audit, and cas scripts now accepts either form, mixable in the same list:
| Entry | Matches |
|---|---|
name |
Every installed version of the package. |
name@version |
Only that exact version. |
cas cooldown ./package-lock.json --allow lodash@4.17.21
cas scripts ./package-lock.json --allow esbuild@0.20.2 --allow @parcel/watcher
cas audit ./package-lock.json --allow-private @internal/lib@1.0.0Scoped names parsed correctly. Name comparison is case-insensitive; version comparison is exact (SemVer prerelease casing is significant).
Breaking change — audit --json schema
AuditReport.unaudited_blocked and .unaudited_allowed are now list[tuple[str, str]] (name, version) instead of list[str]. The JSON output gains a version field on unaudited_private and unaudited_private_allowed entries:
{"severity": "HIGH", "type": "unaudited_private", "package": "@my/pkg", "version": "1.0.0"}This is required so versioned --allow-private entries can be applied per-(name, version). Consumers of the old name-only list must adapt.
Internals
- New
_allowlist.pymodule withPackageAllowlist+parse_spec. Reused by audit / cooldown / scripts. - 260 tests pass (was 237). Ruff + mypy strict clean.
v0.7.3
cas drift — fix orphan-detection short-circuit on bundleDependencies
The orphan-detection BFS pre-added a bundled child to reachable and then enqueued it for processing. The dequeue path then short-circuited on child_key in reachable, never walking the bundle's own dependencies / peerDependencies / optionalDependencies / nested bundleDependencies. Any transitively-bundled tree was flagged as orphan.
Repro
Projects that depend on @semantic-release/npm@9 (which bundles the npm CLI, ~137 entries under node_modules/npm/node_modules/*) saw the entire bundled tree flagged as orphans on cas drift. The published workaround was cas drift --no-transitive.
After the fix, the same drift call against the same lockfile reports 0 spurious orphans. Projects pinning --no-transitive can now drop the flag.
Tests
Two new regression tests cover the case the previous test_bundled_entries_are_not_orphans missed:
test_bundled_entries_transitive_deps_are_not_orphans— bundled child has its owndependencies+ nestednode_modules/*, mirrors the@semantic-release/npm → npm → lodash/semver → lru-cachereal-world shape.test_nested_bundle_inside_bundle_is_not_orphan— outer bundles mid, which itself declaresbundleDependenciesof inner.
227 tests pass (was 225). Ruff + mypy strict clean.
Migration
No flag or config changes. Pure bug fix. Anyone on v0.7.2 should bump.
v0.7.2
HTTP retry + resilient endpoint fallthrough
Transient network blips on one of ~2500 packages no longer fail the build when a later endpoint successfully resolves the same name.
Changes
- New
_http.with_retryhelper — retriesURLError,TimeoutError,OSError, HTTP 5xx, HTTP 429. HonoursRetry-After(capped at 60s). HTTP 404, other 4xx, andJSONDecodeErrorare never retried. cas cooldown+cas audit: per-name error tracking across endpoint fallthrough. The build only fails on a transient error when the name errored on at least one endpoint and was never resolved on any other.- New
--retries Nflag (default2) on both commands. Shared env:CAS_RETRIES.
Why
Closes two distinct v0.7.1 CI failures observed in production:
- A single
URLErroronregistry.npmjs.orgfailing the cooldown gate. - A probe-registry error short-circuiting the audit CodeArtifact fallback (
Private-package probe failed for <pkg> (probe-registry)).
Both are inherent to large lockfiles (~2500 packages → 2500 HTTP calls) where a transient blip on any single request is effectively guaranteed.
Resilient fallthrough semantics
- Transient error on endpoint A → retry exponentially (
base_delay * 4^attempt). - Retries exhausted on A but B resolves the same
(name, version)→ error discarded, build passes. - Errored on at least one endpoint and never resolved on any other → surface as the build-failing finding (so genuine multi-endpoint outages still fail loudly).
225 tests pass (216 prior + 12 retry-helper + 9 resilient-fallthrough). Ruff + mypy strict clean.
v0.7.1
cas audit — CodeArtifact endpoint support + parallel HEAD probes + probe cache
Closes the asymmetry between cas audit and cas cooldown: both now accept the same --ca-domain / --ca-repository flags, mirror each other's deployment model (npm-only / npm+CA / CA-only), and share CAS_ALLOW_PRIVATE for --allow-private.
Changes
--ca-domain+--ca-repositoryextend the probe phase: a package that 404s on--probe-privateis then probed against the CA endpoint (bearer-token auth via boto3). A hit demotes the finding to INFOunaudited_allowed— saves enumerating private packages.- Parallel HEAD probes: probe phase uses
ThreadPoolExecutorwith--max-workers(default 20). Switched from GET to HEAD — the registry returns ~247 KB of full package metadata on GET, but we only need the status code. On a 2500-package lockfile, downloads drop from ~600 MB to a few KB. - Parallel OSV vuln-detail fetches.
--probe-cache PATH— JSON cache of probe results across CI runs. Entries never invalidate (package existence is stable). A fully-cached audit completes in <10s on a 2500-package lockfile.
Performance (real diner-frontend-next lockfile, 2521 packages)
| Mode | Wall time |
|---|---|
| v0.7.0 first run (serial GET, no cache) | 224 sec |
| v0.7.1 first run (parallel HEAD + cache) | 17 sec |
| v0.7.1 cached | 9 sec |
13–25× speedup.
Internals
RegistryEndpoint and build_codeartifact_endpoint extracted from cooldown.py into a new _registry.py. cooldown still re-exports them for backward compatibility.
Breaking env-var change
CAS_AUDIT_ALLOW_PRIVATE and CAS_COOLDOWN_ALLOW_PRIVATE are replaced by CAS_ALLOW_PRIVATE, shared across both commands. Update any CI env that references the old names.
204 tests pass (was 199). Ruff + mypy strict clean.
v0.7.0
New: cas cooldown
Fails when any installed (name, version) was published more recently than --min-age days (default 14). Defends against rapid-install supply-chain attacks where a malicious version is live for hours before any scanner sees it. Inspired by StepSecurity's npm-package-cooldown-check, kevinslin/safe-npm, and pnpm's minimumReleaseAge setting.
Three deployment scenarios supported:
| Setup | Recommended flags |
|---|---|
| Public npm only | default --registry registry.npmjs.org |
| CodeArtifact + npm | --ca-domain + --ca-repository (falls back to CA on 404) |
| CodeArtifact-only private | --ca-first + --ca-domain + --ca-repository |
Performance: parallel fetch via ThreadPoolExecutor (--max-workers 20 default). On a 2521-package lockfile: ~16s first run, ~0.5s cached. Down from 2–5 minutes serial.
Disk cache (--cache PATH): publish times are immutable so cached entries are always valid. Aggressively populated from every fetched metadata response. Corrupt cache silently falls back to fresh fetch.
Per-version endpoint fallthrough: when an endpoint returns metadata but the specific version isn't in time[], cas falls through to the next endpoint. Closes the false-positive class observed against real lockfiles where the public npm registry returns a placeholder for org scopes without serving the private versions.
Secure-by-default (breaking)
A (name, version) that no configured endpoint can resolve is now HIGH cooldown_private_unresolvable (was silent INFO in earlier prototypes). Catches typo'd deps, lockfile tampering, and configuration gaps. Opt-out: --allow-private <name>.
Matching change for cas audit --probe-private: packages OSV doesn't know AND public npm doesn't know are now HIGH unaudited_private (was INFO). Opt-out: --allow-private <name>.
Aliased package names
Lockfile entries like node_modules/string-width-cjs with name: "string-width" are now queried against the canonical name. Shared extract_package_name(key, entry) helper applies the same fix across cas cooldown, cas audit, and cas scripts.
195 tests pass (was 164). Lint + mypy strict clean.
v0.6.0
New: cas audit
AWS CodeArtifact's npm proxy doesn't implement the audit endpoint (/-/npm/v1/security/advisories/bulk), so npm audit against a CodeArtifact-proxied registry silently returns no findings. cas audit queries the OSV.dev API directly — the federated database osv-scanner uses, covering the GitHub Advisory Database, npm's own advisory feed, and others. No auth required.
Flags: --allow (repeatable), --min-severity, --whitelist FILE, --json.
Env: CAS_AUDIT_ALLOW, CAS_AUDIT_WHITELIST.
Whitelist formats
--whitelist FILE accepts both:
- auditjs / Sonatype OSS Index format:
{"ignore": [{"id": "CVE-..."}]}. Existingauditjs.jsonfiles work without modification. - Plain JSON array of vuln IDs.
Suppressions match against the primary vuln id and every alias, case-insensitive — so a CVE id in the whitelist also suppresses the GHSA alias and vice versa.
Test fixtures are real responses captured from api.osv.dev so the test shapes track production behavior.
v0.5.0
New: cas pin
Fails when package.json contains non-exact direct-dep declarations (ranges, dist-tags, file: / link: paths, tarball URLs, git refs without a full 40-char SHA). Default behavior is fail-closed: any non-exact spec in dependencies / devDependencies / optionalDependencies exits 1 with a HIGH-severity finding.
What counts as pinned:
- Exact SemVer (
1.2.3, including prerelease and build metadata). workspace:protocol.npm:aliases targeting an exact version.- Git URLs / GitHub shorthand with a full 40-char commit SHA fragment.
peerDependencies excluded by default (peers are idiomatically ranges per npm convention); opt-in via --include-peer.
Flags: --allow, --scope, --include-peer, --json.
Env: CAS_ALLOWED_UNPINNED.
v0.4.1
Dependency bumps
Runtime:
click8.3.0 → 8.3.3 (patch).
Dev:
pytest8.3.5 → 9.0.3pytest-cov6.0.0 → 7.1.0ruff0.12.2 → 0.15.12mypy1.18.2 → 2.1.0
Lint and strict-mode fixups required by the new ruff and mypy versions. No behavior change.
v0.4.0
cas registry — auto-detect primary registries
Sweeping cas across a multi-repo org (some on CodeArtifact, some on public npm, some mid-migration) needed a way to gate every project against its own primary registry instead of forcing a single allowlist.
Without --allowed-host: cas registry now reads the lockfile's resolved URL distribution and treats every host carrying ≥20% of the top host's entry count as primary. A 100% CA lockfile, a 100% npm lockfile, and a legitimate CA + corporate-mirror mix all pass cleanly. One-off anomalies (the dependency-confusion attack signature) still fall below the threshold and are flagged as CRITICAL.
With --allowed-host: unchanged. Strict, label-anchored suffix match.
RegistryReport gains detected_primary_hosts: list[str], surfaced in both human and JSON output.
Fixes
cas drift/registry/scriptsnow catchValueErrorfromload_lockfileand emit a clean[HIGH] FAIL — unsupported lockfileVersion 1instead of a Python traceback. Surfaced while sweeping ~18 v1-format archive repos.