Cross-ecosystem dependency security scanner.
It reads lockfiles and registry metadata, and can sample selected package files, then flags known vulnerabilities and heuristic supply-chain signals for review. It has zero external runtime npm dependencies: Acorn is vendored and the rest uses Node.js built-ins.
Experimental and pre-1.0. It runs and finds things, but it is unproven against live attacks. Detection is heuristic, so expect false positives and misses. Findings are signals for a human to review, not verdicts. The
guardandmonitorcommands are the least exercised parts and should be treated as early work.
Documentation: versioned in-repository reference. Treat it as authoritative for this source tree; external copies are not release artifacts.
# Try it without a global install
npx @penumbraforge/vexes scan
See it run — click "Run workflow" to watch vexes scan a demo project with known-vulnerable packages across npm, PyPI, and Cargo.
$ vexes scan --path demo
vexes v0.6.1 -- scanning dependencies
Found 33 unique packages across 3 dependency file(s)
-- CRITICAL --------------------------------------------------
tar 6.1.0 (npm)
GHSA-23hp-3jrh-7fpw -- node-tar: Decompression/parse DoS via unlimited input
Fixed in: >= 7.5.19
https://osv.dev/vulnerability/GHSA-23hp-3jrh-7fpw
minimist 1.2.5 (npm)
GHSA-xvch-5gv4-984h -- Prototype Pollution in minimist
Fixed in: >= 1.2.6
https://osv.dev/vulnerability/GHSA-xvch-5gv4-984h
...
(illustrative output from a dated demo run, trimmed for the README — 219 findings:
27 critical . 109 high . 83 moderate across npm, pypi, cargo, as of 2026-08-28;
counts drift as OSV data changes)
vexes checks dependencies against OSV.dev and organizes its npm/PyPI heuristic work into four layers. Individual layers run only where the ecosystem and available metadata support them. Each result is a signal, not a conclusion — a starting point for review:
| Layer | Detection Method | What it flags |
|---|---|---|
| 1. Source-pattern analysis | Parses selected JavaScript with vendored Acorn; uses pattern matching for selected Python files | Calls and strings associated with code execution, process spawning, credentials, networking, obfuscation, and dynamic loading |
| 2. Dependency metadata | Profiles dependencies added by the analyzed npm release relative to its previous published version | Very new packages, same-publisher staging, consumer install hooks, and names similar to a curated popular-package list |
| 3. Behavioral fingerprinting | Compares observable install-hook capabilities when both the analyzed and previous published version's data is available | A package version that newly adds network, process, or credential-related behavior |
| 4. Registry metadata | Analyzes publish history, maintainers, timing, and attestation fields | Publisher changes, rapid publishes, dormant-package activity, and metadata inconsistencies |
Two kinds of signal, weighted differently:
- Vulnerability layer (OSV) — an upstream advisory match.
scanqueries OSV.dev and reports versions that OSV places in an affected range. Theconfidence: provenfield means the advisory match is deterministic; it does not prove exploitation or that a package is malicious. Dependency enumeration, SARIF, and OSV-cross-checked fix candidates are built on this layer. - Heuristic supply-chain signals —
analyzeandinspectgrade each signal (proven/deterministic/heuristic/inferred). Explicit malicious-package OSV records becomeKNOWN_MALICIOUS; ordinary affected-range matches becomeKNOWN_VULNERABILITY. Both areprovenmatches to upstream data, not proof of compromise or exploitation. Metadata facts (publishing-account change, consumer install hooks, publish timing) aredeterministic; pattern/AST matches areheuristicand can false-positive. Bounded tarball sampling is npm/PyPI only. Thescancommand covers the other documented ecosystems with OSV; the heuristic analysis command currently does not.
Normal successful JSON paths for scan, analyze, fix, monitor --ci,
inspect, doctor, and licenses share a versioned top-level envelope.
Command-specific payloads differ, and early errors, setup/uninstall flows,
explain/triage, and long-running watch output are not yet a uniform contract.
The red team test suite contains hand-built, offline fixtures for behaviors such as environment-variable collection, downloader-shaped install scripts, obfuscated payload strings, typosquat names, dynamic loading, DNS access, and capability changes. They are synthetic inputs written for the tests, not copies of malware, and the tests only assert that selected signals fire.
Passing these tests shows that vexes flags these particular fixtures. It does not show that it would flag a historical incident as actually published, or that it would catch a new one. There are also selected benign controls, but their small curated set is not a population false-positive estimate or a substitute for real-world evidence, which vexes does not yet have.
On top of the unit tests, a detection benchmark records three
dated observations. Part A matches 5/5 exact advisory identities for historical
package versions. Part B detects 5/5 technique-specific synthetic attacks and
passes 2/2 gated negative controls under their stated forbidden/HIGH+ criteria.
Part C reports HIGH/CRITICAL heuristic
flags for 1/10 exact-version, digest-pinned live samples and 0 current blocking
advisories as of 2026-08-28. The live cases inspect bounded entry/install-like
source samples, not whole packages, so the full run reports INCOMPLETE; that
expected coverage status exits 0 only when its integrity and policy gates pass.
Execution errors exit 2, regressions exit 1, and --require-complete makes the
sampled-coverage limitation blocking. The benchmark does not retrieve
known-malware artifacts or execute package code.
# Global install
npm install -g @penumbraforge/vexes
# Or run directly
npx @penumbraforge/vexes scan
# Or clone and run
git clone https://github.com/penumbraforge/vexes.git
cd vexes && node bin/vexes.js scan --path /your/project
Requirements: Node.js >= 22.13.0. node:sqlite first appeared in 22.5.0
behind an experimental flag and became usable without that flag in 22.13.0.
Enumerates supported dependency records, queries OSV.dev, and reports versions that OSV places in affected ranges.
vexes scan # Scan current directory
vexes scan --path ./my-project # Scan a specific directory
vexes scan --ecosystem npm # Scan only npm dependencies
vexes scan --severity critical # Only show critical vulnerabilities
vexes scan --fix # Show advisory-derived upgrade hints
vexes scan --json # Machine-readable JSON output
vexes scan --cached # Accept stale cache hits; misses may still query OSV
vexes scan --min-reachability reachable # Deprecated no-op; findings are never filtered this way
vexes scan --ai --json # + optional LLM import context per finding
Ecosystems supported: npm (package-lock.json, pnpm-lock.yaml, yarn.lock), PyPI (Pipfile.lock, poetry.lock, requirements.txt, pyproject.toml), Cargo (Cargo.lock), Go (go.mod preferred, go.sum fallback), Ruby (Gemfile.lock), PHP (composer.lock), NuGet (packages.lock.json), Java (gradle.lockfile, pom.xml), Hex (mix.lock), Dart/pub (pubspec.lock)
Resolved lockfiles are preferred. Ruby Gemfile, PHP composer.json, NuGet
project files, and Java pom.xml fallbacks can contribute exact pins but are
always marked incomplete because they are not resolved graphs. Go uses
go.mod when present; replaced modules are excluded and make coverage
incomplete. If only go.sum exists, vexes parses its checksum history, which
can include modules no longer active in the build. Unrecognized lockfile shapes
fail visibly instead of being treated as empty; the pnpm parser currently
accepts lockfile major versions 6 and 9. Ruby lock entries require the canonical
RubyGems remote and a Bundler CHECKSUMS SHA-256 entry; legacy unchecked locks
remain visibly incomplete because a mirror can serve different bytes for the
same coordinate.
Direct-import evidence: Tier A scans selected project source forms (Acorn
for JavaScript; lightweight scans for TypeScript, Python, and Rust) and adds
importEvidence: found_static | found_dynamic | not_found | unknown to each
finding. not_found means only that no matching import appeared in source vexes
successfully parsed. Tools, package binaries, plugin loaders, computed imports,
workspace siblings, skipped files, and parser failures can still load a
dependency. Unsupported ecosystems and projects with no parsable source for
that ecosystem are unknown. The older reachability values
reachable | lazy | dead | unknown remain for compatibility only.
--min-reachability is deprecated and ignored; direct-import evidence never
suppresses an advisory.
Optional AI import context (--ai): an LLM can classify the limited
direct-import evidence already collected by the scan. Per finding, vexes sends
that evidence plus a control-character-stripped, truncated advisory summary.
The model answers reachable | plausible | unclear with a one-line reason. A
package import is not proof that the vulnerable API or code path runs, so this
classification is context for review rather than an exploitability verdict. It
is attached under findings[].aiImportContext. The historical
findings[].exploitability field remains a deprecated alias. Summary roll-ups
use .aiReachable/.plausible/.unclear/.aiError; .exploitable remains a
deprecated alias for .aiReachable and does not prove exploitation.
vexes scan --ai --json # find vulns and ask the configured LLM which may matter
Point itself at a provider with any one of:
export VEXES_AI_BASE=http://localhost:11434 # OpenAI-compatible (Ollama/Spark)
export VEXES_AI_KEY=optional-bearer-token # only when that endpoint requires it
export ANTHROPIC_BASE_URL=http://cluster:8888 # Anthropic-compatible cluster (vLLM native Messages API)
export ANTHROPIC_AUTH_TOKEN=local # Bearer token for the cluster (model auto-discovered via /v1/models)
export ANTHROPIC_API_KEY=sk-ant-... # hosted Anthropic API
VEXES_AI_BASE wins. A custom Anthropic-compatible route requires either
ANTHROPIC_AUTH_TOKEN (Bearer) or ANTHROPIC_API_KEY (x-api-key); the hosted
route requires ANTHROPIC_API_KEY. Advisory metadata is untrusted and is framed
as data in the prompt. A remote configured provider receives summarized
package/advisory/import facts; source files are not sent.
AI classifications are advisory metadata, never a filter: a deterministic finding is
never silenced by an AI opinion, and an AI failure never flips complete to
false — it degrades to a warning. If no provider is configured, --ai is
skipped, a warning is added, and the deterministic scan result is unchanged.
Exit codes: 0 = requested checks complete with no findings at the active
threshold, 1 = findings at the threshold, 2 = error/incomplete scan. Exit 0
is not a safety verdict.
Downloads registry metadata, pattern-checks inspectable inline install-script content, and profiles observable changes between versions. The output is a list of signals to review; expect both false positives and misses.
vexes analyze # Prefer parser-identified direct dependencies
vexes analyze --deep # Download + inspect a bounded file sample
vexes analyze --explain lodash # Detailed breakdown for one package
vexes analyze --strict # Fail on reported signals at the active severity threshold
vexes analyze -v # Show all signals including LOW
vexes analyze --json # Machine-readable JSON output
When --deep samples a package, its bounded coverage is reported as
complete: false and the command exits 2 even if sampling itself succeeds;
findings remain available for review. For npm, downloaded compressed bytes are
checked against registry SRI or shasum metadata when present. Missing npm digest
metadata is a warning on the static path; the current PyPI sdist path is not
digest-bound.
For selected at-risk npm packages, provenance lookup is another requested stage. An unavailable lookup or a present bundle whose payload cannot be decoded makes analysis incomplete. A decoded bundle is cross-referenced but not cryptographically verified.
Detection signals:
KNOWN_MALICIOUS-- OSV explicitly identifies a malicious package/version (MAL-*or malware-typed record)KNOWN_VULNERABILITY-- Version falls in an ordinary OSV vulnerability range; it does not imply malware or exploitationKNOWN_COMPROMISED/OSV_MATCH-- Compatibility names; normalizedscanfindings currently retainKNOWN_COMPROMISED, while new analysis results use the two names aboveMAINTAINER_CHANGE-- Publishing account changed (possible account takeover)POSTINSTALL_SCRIPT-- Has consumer install hooks (preinstall,install, orpostinstall)RAPID_PUBLISH-- Version published suspiciously quickly after previousVERSION_ANOMALY-- Major version jump or dormancy followed by sudden publishTYPOSQUAT-- Name suspiciously similar to a popular packagePHANTOM_DEPENDENCY-- Dependency added by the analyzed npm release is < 7 days old or has sparse registry metadataCIRCULAR_STAGING-- Such a dependency was published by the same account as the parent within 48 hoursCAPABILITY_ESCALATION-- Previous version's capabilities known and the package gained dangerous ones between versions (requires a real previous-version diff)INITIAL_DANGEROUS_CAPABILITY-- Analyzed version's inspectable install scripts have dangerous capabilities, but usable previous-version script data was unavailable, so no diff is claimedAST_DANGEROUS_PATTERN-- Selected risk-associated patterns in inspectable inline install-hook contentTARBALL_DANGEROUS_PATTERN-- Pattern matches in selected files from a bounded tarball sampleHOMOGLYPH-- Package name contains suspicious Unicode (zero-width chars, RTL override, non-ASCII)MISSING_PROVENANCE-- No npm Sigstore provenance attestation among packages selected for that stageATTESTATION_IDENTITY_MISMATCH-- Decoded attestation identity fields that disagree with the package name or declared repository. vexes decodes and cross-references attested fields; it does not verify DSSE signatures, certificates, or transparency-log inclusion proofs — so it can catch a mismatch between what an attestation says and what the package claims, but it cannot confirm the attestation is genuine. Presence or internal consistency of an attestation is not a trust verdict.SIGNATURE_SPOOFis retained only as a compatibility name.NO_REPOSITORY-- No source repository link
Coverage caveats for name-based detection: typosquat similarity is computed against a curated list of ~165 npm / ~105 PyPI popular packages — a typosquat of anything outside that list is invisible. Similarity thresholds are length-dependent (names ≤3 chars are never compared; 4–6 chars allow distance 1; 7+ allow distance 2). Scoped names are compared as their full spelling, including the scope, which can make the distance threshold less useful. Homoglyph detection flags invisible Unicode, BIDI overrides, and any non-ASCII character. Normal public-registry naming rules make that check largely dormant; it remains a defensive primitive for malformed/synthetic metadata. It detects "non-ASCII present," not true Unicode-confusable mapping.
With --sandbox, analyze may execute one selected npm entrypoint (bin,
main, or index.js) for each of up to five HIGH/CRITICAL candidates;
inspect may execute one for its target. Execution uses a bounded partial
extraction under an accepted Linux bwrap host. It does not run lifecycle
hooks. PyPI sandbox extraction and execution are unsupported and refused; this
does not affect static PyPI --deep sampling. The accepted bwrap layout
omits the user's home and project by default and restricts writes to the
extracted workdir and throwaway temp, but it exposes read-only host runtime
trees (/usr, /bin, and available loader-library directories). /etc, the
user's home, and the calling project are not mounted. This is private-data
isolation, not a guarantee that package code can read no host information. The current macOS
sandbox-exec profile is refused because its broad file-read rule also exposes
arbitrary user files. A launch probe checks that the primitive can start a
benign Node process, not that containment is escape-proof. The recorder observes a subset of Node
process, network, and filesystem APIs, is not inherited by child processes,
and is not tamper-resistant. The child receives a minimal environment rather
than arbitrary parent environment variables. Treat this as best-effort
telemetry, not a safe boundary for known malware. Execution is opt-in and refused when no accepted
primitive is available or when npm registry digest metadata is unavailable.
Because recorder absence is not trusted as negative evidence, a requested
sandbox stage remains complete: false / exit 2 even when a selected run
launches; positive observations are still retained. A timeout or nonzero child
exit is a failed stage, not behavioral evidence.
For npm lockfiles, finds vulnerabilities and suggests POSIX-shell-quoted upgrade commands. Package/version coordinates are validated before a command is shown; the npm form uses an option boundary. At request time, a candidate is presented only after OSV returns no matching advisory for that version. Generate the resolved lockfile, rescan it, and test the upgrade before treating it as remediation.
vexes fix # Show OSV-cross-checked upgrade candidates
vexes fix --json # Machine-readable output
Experimental, public-registry npm only. Guard accepts explicit
npm install/i/add <package> requests with a narrow set of flags. It resolves
the proposed manifest and lockfile in a disposable project copy with lifecycle
scripts disabled, preserves individual lockfile occurrences, and requires each
new or changed artifact to have a public-registry HTTPS URL and integrity value.
It cross-checks exact-version registry metadata, analyzes the changed
occurrences, then applies an approved manifest/lockfile with
npm install --ignore-scripts. The final lockfile must match the approved
occurrence, version, resolved URL, and integrity fields.
This verifies npm metadata and lockfile identity, not the bytes placed in
node_modules, and it is not a transaction: each reviewed manifest file is
staged with an atomic same-directory replacement and restored on detected
failure or drift, but node_modules may be partially changed. An actively
racing same-user process remains outside this boundary.
Lifecycle scripts remain disabled for the guarded install. Bare installs,
workspaces, local/git/URL specs, npx, pnpm, and Yarn are refused. JSON mode is
assessment-only and never installs. Treat guard as a review boundary, not proof
that an artifact is safe.
vexes guard -- npm install axios # Guard a specific install
vexes guard --setup # Currently disabled; fails closed
vexes guard --uninstall # Remove a wrapper installed by an older release
vexes guard --force -- npm install axios # Override HIGH only; never incomplete/known-advisory/CRITICAL blocks
How it works:
- Copies the current manifest and lockfile into a disposable resolver directory
- Runs npm there in lockfile-only mode with scripts disabled
- Diffs occurrence-aware snapshots and validates each new/changed public-registry artifact identity
- Checks exact proposed versions against OSV, registry metadata, and behavioral signals
- Blocks any known advisory, CRITICAL evidence, or incomplete analysis; prompts on HIGH (or accepts
--forcefor HIGH only) - In human mode, applies the approved files, installs with scripts disabled, and verifies the final manifest and lockfile identity; in JSON mode, stops after assessment
Two modes for CI/CD and development:
# CI mode -- one-shot scan with GitHub Actions annotations
vexes monitor --ci # Default: fail on HIGH+
vexes monitor --ci --severity critical # Only fail on CRITICAL
vexes monitor --ci --sarif # SARIF output for GitHub code scanning
vexes monitor --ci --json # Machine-readable JSON
# Watch mode -- continuous local monitoring
vexes monitor --watch # Watch lockfiles + poll OSV hourly
vexes monitor --watch --interval 5 # Poll every 5 minutes
vexes monitor --watch --freshness 5 # + poll registry metadata every 5 min
Freshness layer (--freshness <minutes>): an experimental npm/PyPI poll
that records each registry's latest version in the local SQLite cache. The first
successful poll establishes a baseline; later polls grade a release only when
the observed latest version changes. An alert means "new since vexes last saw
it," not proof of a new publication or attack. If persistent cache state is
unavailable, freshness polling is disabled; per-package registry failures make
a cycle incomplete. It does not download or execute package code.
Collect evidence about one package spec on demand; it does not decide whether a dependency is safe. No project is required. npm and PyPI support differ, and the result combines available OSV, registry, heuristic, and decoded-attestation evidence. For npm, an unavailable requested provenance lookup or a present but undecodable bundle makes the result incomplete. A decoded bundle is cross-referenced, not cryptographically verified.
vexes inspect lodash@4.17.21 --json # JSON envelope
vexes inspect express # latest version
vexes inspect requests --ecosystem pypi
vexes inspect express@4.21.2 --deep # + inspect a bounded tarball file sample
Runs an installation smoke test: loads one fixture per parser, performs a SQLite write/read, and reports optional registry and sandbox availability. Passing does not attest to detection accuracy, provider readiness, or containment.
vexes doctor # human output
vexes doctor --json # machine output
A flat declared-license inventory — informational, not a standardized SPDX or
CycloneDX SBOM and never a security verdict. Uses
Google's deps.dev API (no key) for its declared-license strings (vexes does not
locally validate them as SPDX expressions) across
npm / pypi / cargo / go / nuget / java. Ecosystems deps.dev doesn't
cover (ruby, php, hex, pub) are skipped with a visible warning, not an
error — for those, vexes scan remains the vulnerability answer.
vexes licenses --path ./my-project
vexes licenses --json
For supported lookups, if any lookup fails, complete: false and exit 2. A
package with no declared
license exits 1 (VULNS_FOUND equivalent) so CI notices; it is a flagged
row, not an error. Requests are serialized with a small client-side interval,
and 429/5xx responses are retried with backoff.
{
"schemaVersion": "1.0",
"command": "licenses",
"complete": true,
"summary": { "total": 33, "withLicenses": 33, "missing": 0, "skipped": 0 },
"licenses": [
{ "package": "express", "version": "4.17.1", "ecosystem": "npm",
"licenses": ["MIT"], "url": "https://api.deps.dev/..." }
]
}
Turns scan findings into a draft plain-English prioritization. It is opt-in and uses a configured OpenAI-compatible endpoint, Anthropic-compatible endpoint, or hosted Anthropic API. Scanning still queries public OSV/registry endpoints; AI triage is a separate layer. A remote model receives summarized findings, not source files. Treat its output as a draft to verify.
# local OpenAI-compatible endpoint
export VEXES_AI_BASE=http://localhost:11434
# or a local Anthropic-compatible cluster (vLLM native Messages API)
export ANTHROPIC_BASE_URL=http://cluster:8888 ANTHROPIC_AUTH_TOKEN=local
# or hosted
export ANTHROPIC_API_KEY=sk-ant-...
vexes scan --format json | vexes explain # Triage a piped scan
vexes explain --input report.json # Triage a saved report
vexes explain --path ./my-project # Scan, then triage
Findings are summarized (package, version, severity, advisory, fix) before
sending; source files are not included in the model request. Set
VEXES_AI_MODEL (or ANTHROPIC_MODEL) to override the model; without either,
a cluster's model is auto-discovered from GET /v1/models.
GitHub Action:
# .github/workflows/vexes.yml
name: Dependency Security
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: penumbraforge/vexes@v0
id: vexes
with:
command: scan
severity: high
sarif-file: vexes.sarif # optional: upload where code scanning is enabled
fail-on-findings: 'true' # optional: block the build on findings
# Pair with upload-sarif to see results in GitHub code scanning:
- uses: github/codeql-action/upload-sarif@v3
if: always() && steps.vexes.outputs.sarif-file != ''
with:
sarif_file: ${{ steps.vexes.outputs.sarif-file }}
On a valid structured run, the action succeeds by default and exposes outputs
(vulnerabilities, critical, high, moderate, exit-code, and a validated
fresh sarif-file when requested) so you can branch on them. Empty, malformed,
or non-envelope JSON exposes exit code 2 without made-up zero counts. Set
fail-on-findings: 'true' to make findings or incompleteness a hard gate. The
action passes both --no-user-config and --no-project-config, so neither a
self-hosted runner's user policy nor a checked-out pull request's
.vexesrc.json can silently change the action's result. Explicit action inputs
still apply.
Or run directly:
- name: Security scan
run: npx @penumbraforge/vexes monitor --ci --severity high --no-user-config --no-project-config
# With SARIF upload to GitHub code scanning:
- name: Security scan (SARIF)
run: npx @penumbraforge/vexes monitor --ci --sarif --no-user-config --no-project-config > results.sarif
continue-on-error: true # preserve the report when findings make vexes exit 1
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
Place in your project root. Walks up directories to find it.
{
"ecosystems": ["npm", "pypi"],
"severity": "high",
"ignore": [],
"analyze": {
"signals": {
"NO_REPOSITORY": "off",
"POSTINSTALL_SCRIPT": "off"
}
},
"output": {
"color": "auto",
"format": "text"
}
}
Project config is allowlisted to reporting policy: ecosystems, severity,
ignore entries, strict/verbose/top presentation, signal switches, and output
format/color. It cannot opt into --sandbox, --deep, --fix, --ai, or
stale-cache use, and it cannot choose the cache location or TTLs. Those
operational modes require an explicit CLI flag; cache storage policy comes
from defaults or trusted user config. Use --no-project-config when the
repository being scanned is not trusted to choose reporting policy. Add
--no-user-config when a reproducible enforcement run must also ignore the
runner account's user policy.
User config can also choose the ordinary cache directory and TTLs. Operational
modes (--sandbox, --deep, --fix, --ai, and --cached) remain CLI-only
for each invocation. Allowed project policy overrides user policy unless
--no-project-config is supplied; --no-user-config skips the user file.
vexes ships with a curated exact-name list for common packages expected to use
consumer install hooks. Signals that carry the internal knownGood annotation
are downweighted rather than suppressed. Inline script content may still
produce findings, but file-based
commands such as node install.js are not inspected by this stage.
Vexes is callable by agents through its CLI; no MCP server is required. Normal
successful structured paths for scan, analyze, fix, monitor --ci,
inspect, doctor, and licenses use the envelope below. Payloads remain
command-specific. explain/triage, guard setup/uninstall, watch streams, and
some early errors are text paths today, so callers must check exit status and
validate JSON before trusting stdout.
{
"schemaVersion": "1.0",
"generator": { "name": "vexes", "version": "0.6.1", "engine": "v22.13.0" },
"timestamp": "2026-08-28T00:00:00.000Z",
"command": "scan",
"target": {
"dir": "/project",
"lockfiles": ["package-lock.json"],
"ecosystems": ["npm"]
},
"complete": true,
"result": { "complete": true, "warnings": [] },
"summary": { "total": 33, "vulnerable": 219, "critical": 27, "high": 109, "moderate": 83, "low": 0, "suppressed": 0,
"directImportEvidence": { "foundStatic": 0, "foundDynamic": 0, "notFound": 0, "unknown": 33 },
"reachable": 0, "lazy": 0, "dead": 0, "unreachable": 0 },
"findings": [ { "id": "GHSA-23hp-3jrh-7fpw", "package": "tar", "version": "6.1.0", "ecosystem": "npm",
"severityLevel": { "level": "CRITICAL", "order": 4 },
"confidence": "proven", "reachability": "unknown", "importEvidence": "unknown",
"signal": "KNOWN_COMPROMISED", "advisories": ["GHSA-23hp-3jrh-7fpw", "CVE-2026-59873"],
"fixed": ">= 7.5.19", "fixCommand": "npm install -- tar@7.5.21",
"llmSummary": "[CRITICAL] tar@6.1.0 (npm) — CVE-2026-59873: node-tar: Decompression/parse DoS via unlimited input. Fixed in >= 7.5.19. No direct import found in this project's source (may still load via tooling or dynamic paths)." } ]
}
This is an abridged example based on a dated demo scan. Because demo/ is lockfiles-only,
the current implementation emits unknown import evidence; absence of source
is not evidence that a dependency is unused. For documented structured paths, complete: false
means the result must not be treated as clean. Exit codes are normally 0 for
complete requested checks with no findings at the active threshold, 1 for
findings at the threshold, and 2 for error/incomplete; exit 0 is not a safety
verdict, and callers should still validate each command path. A scan's
fixCommand is derived from advisory fixed-version
events; use vexes fix for OSV- and registry-cross-checked candidates, then
generate and rescan the resolved lockfile. vexes doctor is an installation smoke test, not an accuracy or
security attestation. Full details: wiki/Agent-Integration.md.
bin/vexes.js CLI entrypoint, command router
src/
cli/
parse-args.js Hand-rolled argument parser
config.js Config loading with prototype pollution protection
output.js Terminal output, ANSI colors, spinner, sanitization
commands/
scan.js Vulnerability scanning via OSV
analyze.js 4-layer behavioral analysis
fix.js OSV-cross-checked fix candidates
guard.js Experimental public-npm assessment/install boundary
monitor.js CI annotations, SARIF output, watch mode
core/
constants.js URLs, thresholds, exit codes
fetcher.js Shared JSON HTTP client with retry/timeout/backoff
logger.js Leveled logger with terminal-control sanitization
allowlists.js Curated install-hook names, popular package sets
parsers/
npm.js package-lock.json v1/v2/v3, package.json fallback
pnpm.js pnpm-lock.yaml v6/v9
yarn.js yarn.lock v1 (classic) and v2+ (Berry)
pypi.js requirements.txt (-r recursive), poetry.lock, Pipfile.lock, pyproject.toml
cargo.js Cargo.lock
go.js go.mod (preferred), go.sum (fallback)
ruby.js Gemfile.lock
php.js composer.lock
dotnet.js packages.lock.json (NuGet)
java.js gradle.lockfile, pom.xml
advisories/
osv.js OSV.dev batch queries, CVSS v3.1 scoring, severity mapping
npm-registry.js npm registry metadata + provenance attestations
pypi-registry.js PyPI JSON API metadata
analysis/
ast-inspector.js Acorn-based AST analysis (JS) + pattern matching (Python)
signals.js Signal orchestrator, composite risk scoring
dep-graph.js Dependency graph profiling, typosquat detection
behavioral.js Capability fingerprinting, version diffing
tarball-inspector.js Tarball download, tar parsing, source inspection
diff.js Lockfile snapshot diffing
provenance.js Attestation field decoding and cross-checking
cache/
advisory-cache.js SQLite-backed cache with TTL, corruption recovery
vendor/
acorn.mjs Vendored acorn parser (zero npm deps)
test/
test-ast-inspector.js AST detection + selected benign controls
test-behavioral.js Behavioral profiling + diffing
test-cache.js SQLite cache, TTL, corruption resilience
test-dep-graph.js Typosquat detection
test-parse-args.js Argument parser
test-parsers.js All lockfile/manifest parsers
test-redteam.js Synthetic attack-technique fixtures
test-robustness.js Input validation, edge cases, security
-
Fail loud, not clean. A scanner that reports clean when it actually failed is misleading. If queries fail, vexes exits with code 2 and prints
SCAN INCOMPLETE. Invalid ecosystems are rejected instead of silently scanning nothing. -
Zero external runtime npm dependencies. Acorn is vendored; SQLite and the remaining runtime facilities come from Node.js.
-
Terminal injection defenses. Shared output and logger paths sanitize CSI, OSC, DCS/APC/PM/SOS, C1 controls, and bare ESC bytes. Some command-specific text paths still need complete sanitizer coverage, so terminal output is not treated as a security boundary.
-
Prototype pollution protection. Config merging drops
__proto__,constructor, andprototypekeys. -
Command-construction defenses. Guard uses
execFileSync(no shell) and accepts only explicit public-registry npm installs with a narrow flag grammar. Displayed upgrade commands validate package/version grammar, use POSIX-shell quoting, and add option boundaries where supported; they still require review before execution. Automatic shell-wrapper setup is currently disabled because it cannot faithfully route commands outside that narrow grammar. -
Bounded registry artifact fetching. Tarball downloads enforce streamed compressed/decompressed size limits. The initial URL and every redirect hop must use HTTPS on the npm/PyPI tarball host allowlist (maximum five redirects). npm bytes are checked against registry SRI or shasum metadata when supplied; dynamic extraction refuses missing npm digest metadata.
-
Cache resilience. Corrupted advisory rows are deleted; corrupt metadata and signal rows are treated as misses. Degraded analysis results are not cached. Analyze fetches current registry metadata and checks evidence plus OSV fingerprints before reusing a signal row. TTL is clamped (max 7 days advisory, 30 days for the historically named metadata/analysis-signal setting).
-
Cross-check fix candidates. The
fixcommand asks OSV about each candidate before presenting it. This means "no advisory OSV currently returned," not proof that the version is safe. -
Signal policy is explicit. Every documented analysis signal can be set to
"off"; the legacyKNOWN_COMPROMISEDswitch also controls the newer OSV signal names when they are not configured separately. Repository-local config can therefore suppress ordinary report evidence, so review committed policy or use--no-project-config. Use--no-user-configas well when runner-level policy must not affect a reproducible enforcement result. Guard clears project/user signal suppression and ignore entries before making an install decision. -
Allowlisting is a scoring adjustment. Signals carrying
knownGoodevidence are downweighted, not suppressed. Inspectable inline scripts still run through the pattern analyzer; file-based install commands may not. -
Suspicious Unicode presence. Package names are checked for invisible characters, BIDI overrides, and non-ASCII characters. This is not a Unicode-confusable or visual-homoglyph mapping, and normal public-registry coordinate rules make the check largely dormant on live metadata.
-
Occurrence-aware lockfile identity. npm guard preserves individual lockfile occurrences and binds approval to version, public-registry resolved URL, and integrity fields. It does not independently hash or verify downloaded or installed bytes.
Apache-2.0
Shadoe Myers (@penumbraforge)
