Supply-Chain Vetting¶
ca9 vet is the broader package-security command. It builds on normalized inventory and
checks dependency risk beyond CVE reachability.
The current implementation is intentionally local-first:
- fyn is optional; ca9 reads
fyn.locknatively when present. - npm projects can be inventoried from
package-lock.json. - package code is never installed, imported, or executed.
- artifact downloads are explicit and hash-verified by default.
- installed package-intelligence feeds are used locally for malware and package-age policy.
- OSV malware advisory queries are opt-in.
- Real incident replay fixtures track current coverage and gaps.
Incident Replay¶
Replay recent real incidents against ca9's current supported surfaces:
The fixtures live in tests/fixtures/incidents/ and currently cover May 2026 npm package
compromise, PyPI import-time malware, and GitHub token/codebase exfiltration patterns. See
docs/proof/incident-coverage.md for the current matrix.
Demo Fixture¶
For a screenshot-ready report, run the local supply-chain fixture:
The fixture generates a fyn.lock with local hash-pinned wheels and shows three blocking
findings: dependency confusion, suspicious .pth startup execution, and a denied license.
It also writes demo/supply_chain/ca9-vet.json for docs or CI artifact screenshots.
ca9 supply-chain report for .../demo/supply_chain/repo
Packages: 4 | Edges: 3 | Findings: 3 | Block: 3 | Warn: 0
Artifact scans: 3 | Skipped artifacts: 0
Inventory¶
Inspect the normalized package inventory:
When a repository has fyn.lock or npm package-lock.json, inventory includes:
- resolved package names and versions
- direct/transitive/project dependency kind
- dependency edges
- groups, markers, extras, and npm dependency classes where available
- artifact URLs, hashes/integrity values, upload times, sizes, and registries
- source evidence for each package and edge
Without lockfiles, ca9 falls back to native Python manifest readers for pyproject.toml,
requirements*.txt, Pipfile, uv.lock, and poetry.lock.
Basic Vetting¶
Run local metadata checks:
This checks:
- untrusted package indexes
- missing artifact hashes
- missing artifact metadata
- source-only install risk
- mutable package sources
- malware entries from the installed
ca9.feed.v1feed - package-age policy when enabled in
ca9.toml
Direct dependencies from untrusted indexes, feed-backed malware matches, and
too-new package versions are blocking findings by default. Missing package-age
release times warn by default and block when [mode].offline is block or
strict. Weaker local metadata signals are warnings unless policy support is
expanded.
Artifact Static Analysis¶
Run artifact-based malicious package heuristics:
By default, ca9 only downloads artifacts that have hashes in the inventory. It verifies the hash, safely unpacks Python wheels/sdists and npm tarballs, rejects path traversal or unsafe archive links, and scans files statically.
Current blocking rules include:
.pthstartup executionsitecustomize.py/usercustomize.pysuspicious startup behavior- install-time
setup.pyprocess/network/eval/exec behavior - encoded payload decode plus execution
- credential access near outbound network code
- top-level import-time risky behavior
- npm install lifecycle hooks
- npm install hooks that shell out, fetch, decode, or eval
- npm encoded payload execution, including base64 and hex decoding
- npm credential access near network or process execution
Suspicious process execution outside setup/import startup paths is marked for investigation.
Use this only if you want ca9 to download package artifacts:
Artifacts without hashes are skipped unless you explicitly opt in:
Package Feeds and Package Age¶
Install a package-intelligence feed before relying on deterministic offline malware or package-age decisions:
ca9 feed update with no --from uses CA9_FEED_URL when set, otherwise the built-in
default feed URL. The default URL points at the project feed branch; until that branch has
been published, use --from or CA9_FEED_URL.
Feed bundles use schema ca9.feed.v1 and contain npm-malware, pypi-malware,
npm-releases, and pypi-releases. Feed snapshots are stored under
~/.cache/ca9/feed/ and verified with SHA-256 content hashes before use. Tampered feeds
always block.
Enable package-age policy in ca9.toml:
If a release timestamp is present and younger than minimum_hours, ca9 blocks. If a
release dataset declares covers_since, a missing version can pass when it is provably
older than the configured minimum. Otherwise the release time is unknown; the default
offline mode warns, and [mode].offline = "block" blocks.
Malicious Advisory Query¶
Query OSV for known malicious-package advisories:
ca9 treats OSV MAL-*, PYSEC-MAL-*, explicit malicious-package metadata, and
malware-labeled GHSA/OSV advisories as blocking malware findings for PyPI and npm. This
is separate from the installed local feed: the feed path is deterministic and local,
while --malware-query asks OSV or uses the OSV cache. Use --offline to restrict the
query path to cached OSV data.
GitHub Actions Workflow Scanning¶
Scan workflow files for risky token and trust-boundary patterns:
The workflow scanner flags:
pull_request_targetworkflows that check out pull request-controlled code- broad write-capable
GITHUB_TOKENpermissions id-token: writeOIDC token minting- mutable action references such as
@main - cache use across
pull_request_targettrust boundaries - source-clone commands such as
gh repo clone - encoded shell payloads such as
base64 -d | bash - cloud metadata, OIDC request token, and broad credential-file harvest patterns
High-risk combinations such as pull request-code checkout, broad write permissions, and source-clone commands with write-capable token scope are blocking by default. Encoded shell execution, cloud metadata probing, and broad local credential-file access are also blocking because they match common CI/CD credential-stealing behavior. Lower-confidence cases such as OIDC write scope alone are marked for investigation.
Dependency Confusion¶
Protect internal package names from resolving from public or unexpected indexes:
ca9 vet --repo . \
--internal-package 'acme-*' \
--private-index https://packages.acme.internal/simple
An internal direct dependency that resolves outside the configured private indexes is a blocking dependency-confusion finding. Transitive matches are marked for investigation.
Use --trusted-index to define package indexes that are generally trusted:
ca9 vet --repo . \
--trusted-index https://pypi.org/simple \
--trusted-index https://packages.acme.internal/simple
License Policy¶
Gate denied licenses from wheel/sdist metadata:
ca9 reads:
- wheel
.dist-info/METADATA - sdist
PKG-INFO License-ExpressionLicenseClassifier: License :: ...
Denied licenses on direct dependencies are blocking findings. Denied licenses on transitive dependencies are investigation findings until richer policy configuration lands.
Warn when scanned artifacts do not declare a known license:
License checks require artifact metadata, so --deny-license and --require-known-license
implicitly enable artifact collection. The same safe artifact rules apply: hashes are
required by default, archives are safely unpacked, and package code is not executed.
JSON Output¶
The JSON schema includes:
- inventory summary
- findings with signal type, severity, package key, evidence, and metadata
- decisions with action, policy ID, and reason
- artifact scan counts and skipped artifact counts
Example signal types:
untrusted_registrydependency_confusionmalwarenew_package_versionpackage_age_unknowngithub_actions_pull_request_target_checkoutgithub_actions_oidc_writegithub_actions_write_permissionspython-startup-pth-execpython-startup-customize-execsetup-install-execencoded-executioncredential-network-exfiltrationimport-time-risky-behaviorsilent-process-executionnpm-install-scriptnpm-install-script-execnpm-encoded-executionnpm-credential-exfiltrationdenied_licenseunknown_license
Exit Codes¶
| Code | Meaning |
|---|---|
0 |
No blocking supply-chain findings. |
1 |
Blocking supply-chain findings were found. |
Warnings and investigation findings remain visible in output but do not currently fail the default gate unless they are represented as blocking decisions.
Current Limits¶
ca9 does not yet implement every dependency attack class. The current vet path covers
the core local gates first: feed-backed malware, optional OSV/GHSA malware advisory
matching, package-age policy, dependency confusion/internal source policy, artifact
integrity basics, Python/npm artifact heuristics, GitHub Actions workflow risk patterns,
and license policy.
Still planned:
- typosquatting and namespace confusion
- lockfile poisoning diffs
- hosted complete recent-release datasets for package-age feeds
- PyPI yanked/project metadata
- maintainer or repository hijack signals
- provenance/Sigstore/SLSA checks
- signed feed snapshots
- range-aware feed matching for bounded OSV malware advisories
- yarn, pnpm, uv, poetry, pipx, npx, requirements-file, direct URL, local path, and alternate pip-source runtime mediation