Skip to content

Releases: TableCheck-Labs/codeartifact-shield

v0.9.0: workspace fix + exotic-source detection + attestation verification with signer pinning

Choose a tag to compare

@DragonStuff DragonStuff released this 20 May 15:09
v0.9.0

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.json

v0.8.0

Choose a tag to compare

@DragonStuff DragonStuff released this 14 May 03:55
v0.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 high

Env: CAS_OSV_ENDPOINTS (whitespace-separated).

How it scales

  • Flattened parallelism. The batch query is dispatched across the (endpoint × chunk) cross product — E endpoints × C chunks → up to E·C in-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.0

Scoped 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.py module with PackageAllowlist + parse_spec. Reused by audit / cooldown / scripts.
  • 260 tests pass (was 237). Ruff + mypy strict clean.

v0.7.3

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 04:39
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 own dependencies + nested node_modules/*, mirrors the @semantic-release/npm → npm → lodash/semver → lru-cache real-world shape.
  • test_nested_bundle_inside_bundle_is_not_orphan — outer bundles mid, which itself declares bundleDependencies of 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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 01:20
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_retry helper — retries URLError, TimeoutError, OSError, HTTP 5xx, HTTP 429. Honours Retry-After (capped at 60s). HTTP 404, other 4xx, and JSONDecodeError are 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 N flag (default 2) on both commands. Shared env: CAS_RETRIES.

Why

Closes two distinct v0.7.1 CI failures observed in production:

  1. A single URLError on registry.npmjs.org failing the cooldown gate.
  2. 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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

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-repository extend the probe phase: a package that 404s on --probe-private is then probed against the CA endpoint (bearer-token auth via boto3). A hit demotes the finding to INFO unaudited_allowed — saves enumerating private packages.
  • Parallel HEAD probes: probe phase uses ThreadPoolExecutor with --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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

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:

  1. auditjs / Sonatype OSS Index format: {"ignore": [{"id": "CVE-..."}]}. Existing auditjs.json files work without modification.
  2. 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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

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

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

Dependency bumps

Runtime:

  • click 8.3.0 → 8.3.3 (patch).

Dev:

  • pytest 8.3.5 → 9.0.3
  • pytest-cov 6.0.0 → 7.1.0
  • ruff 0.12.2 → 0.15.12
  • mypy 1.18.2 → 2.1.0

Lint and strict-mode fixups required by the new ruff and mypy versions. No behavior change.

v0.4.0

Choose a tag to compare

@DragonStuff DragonStuff released this 13 May 00:58

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 / scripts now catch ValueError from load_lockfile and emit a clean [HIGH] FAIL — unsupported lockfileVersion 1 instead of a Python traceback. Surfaced while sweeping ~18 v1-format archive repos.