-
-
Notifications
You must be signed in to change notification settings - Fork 0
Vulnerability Management
synaptic vuln audits a repository's resolved dependencies against an OSV
advisory corpus, decides whether each vulnerability actually applies here, and
keeps an auditable record of what was found and what was decided about it.
Analysis never touches the network. The only network step is fetching the advisory corpus, which is cached locally and can be disabled entirely.
# First run fetches the advisory corpus into ~/.synaptic/advisories.
synaptic vuln scan
# Persist findings to the auditable ledger.
synaptic vuln scan --record
# Ask whether a package is safe before adding it.
synaptic vuln check cargo:openssl --version 0.10.55corpus: ~/.synaptic/advisories/cargo (2698 advisories, 0 unreadable, newest 2026-08-06T09:15:04Z)
packages scanned: 450
findings: 1 (0 applicable, 0 suppressed by exception)
[P2] RUSTSEC-2023-0071 cargo:rsa@0.9.10
Marvin Attack: potential key recovery through timing sidechannels
id=vuln_finding_2852589210b6008289db2c9e state=ReviewRequired severity=Medium (5.9)
path: cargo:synaptic-sqlaudit@0.9.0 -> cargo:sqlx@0.8.6 -> cargo:sqlx-mysql@0.8.6 -> cargo:rsa@0.9.10
fix: NoFixAvailable
Every report names its corpus and that corpus's newest advisory. A scanner that hides its corpus invites you to read "no findings" as "no vulnerabilities".
Each finding carries an evidence ladder:
evidence:
[Gate] VersionInAffectedRange: the resolved version falls inside an affected range
[LowersPriority] NoFirstPartyUsage: no first-party usage observed; this bounds nothing on its own
[Informational] AdvisoryNamesNoFunctions: the advisory names no vulnerable functions, so reachability is undecidable from it
| State | Meaning |
|---|---|
Applicable |
the version is in range and the vulnerable code is reached, or the package is a used direct runtime dependency |
ReviewRequired |
in range, but reachability could not be confirmed |
NotApplicable |
the advisory was withdrawn, or the version is outside every affected range |
NotApplicable is only ever reached through those two gates. Unreachable
symbols, absent first-party usage and development-only scope are all recorded
and all de-rank a finding, but none of them makes it not-applicable. Static
reachability is incomplete in every language Synaptic reads, dispatch can be
dynamic, and advisory function lists are frequently absent. "We found nothing"
is not "there is nothing". A test sweeps all 128 boolean input combinations to
keep that invariant true.
When a graph is present, reachability uses it: SDK stub nodes for external packages, and whether first-party code reaches them. Without a graph the scan still runs, it simply produces fewer raising signals.
priority = severity band x applicability x runtime reachability
CVSS base scores come from the advisory's own v3.x vector. No environmental re-scoring is performed. Unknown severity is treated as Medium, not Low: an advisory nobody scored is not thereby harmless. Fix availability is deliberately not an input, because an unfixed critical is not less urgent than a fixed one.
vuln scan discovers every lockfile in the repository and audits them together,
so a polyglot repository is scanned as a whole.
| Lockfile | Ecosystem | Dependency paths |
|---|---|---|
Cargo.lock |
cargo | yes |
package-lock.json (v1, v2, v3) |
npm | yes |
pnpm-lock.yaml |
npm | yes |
yarn.lock |
npm | yes |
poetry.lock |
PyPI | yes |
uv.lock |
PyPI | yes |
composer.lock |
Packagist | yes |
Gemfile.lock |
RubyGems | yes |
packages.lock.json |
NuGet | yes |
Podfile.lock |
CocoaPods | yes |
go.mod |
Go | no |
Package.resolved |
SwiftPM | no |
pubspec.lock |
Pub | no |
mix.lock |
Hex | no |
gradle.lockfile |
Maven | no |
Discovery honours .gitignore. Generated and vendored trees carry other
projects' lockfiles, and auditing them reports dependencies the repository does
not have. On Synaptic itself, descending into the ignored synaptic-out/
directory picked up 24 vendored lockfiles and turned a one-second scan into a
ten-minute one.
"Dependency paths: no" means the format records resolved versions but not what each package depends on, so those findings name the vulnerable package without a chain to it. They are still detected, prioritised and remediable.
Every row is backed by a fixture in the crate's coverage test, which fails if a format is added without one, so this table cannot drift from what actually parses.
packages scanned: 452
WARNING: 1 package(s) NOT audited (no corpus for npm); they are not known to be clean
Resolved in order: --advisories <dir> if given, otherwise the shared cache at
~/.synaptic/advisories/<ecosystem>/, downloaded from OSV's bulk export on
first use and refreshed after seven days.
synaptic vuln sync # refresh the cargo corpus
synaptic vuln sync --ecosystem pypi # another ecosystem
synaptic vuln scan --offline # never fetch; fail if nothing is cachedBulk export is used rather than per-package API queries because it costs one request instead of one per package, everything afterwards works offline, and it never discloses what this repository depends on.
Exports above 64 MB are refused rather than downloaded silently. npm's is about 218 MB, so it needs one explicit fetch:
synaptic vuln sync --ecosystem npm --max-bytes 300000000--offline never fetches. With nothing cached it fails rather than reporting a
clean scan against an empty corpus, because "no findings" and "no advisories"
must never look alike.
Three MCP tools let assistants avoid generating known-vulnerable code:
| Tool | Purpose |
|---|---|
vuln_check_dependency |
called before writing a dependency into a manifest |
vuln_findings |
current findings with applicability and priority |
vuln_explain |
one finding's evidence, path, plan and history |
Point the server at a corpus with SYNAPTIC_VULN_ADVISORIES, or place one at
.synaptic/vuln/advisories. The server never downloads: an operator decides
explicitly what corpus an agent answers from.
vuln_check_dependency returns allowed, constrained or blocked plus the
constraint to use. Where several advisories affect a package it returns the
strictest floor:
Blocked cargo:openssl at 0.10.55
Use >=0.10.72. This constraint comes from advisory metadata and has NOT been
checked against a registry, so confirm the version resolves.
- RUSTSEC-2023-0072: affected; fixed in 0.10.60
- RUSTSEC-2024-0357: affected; fixed in 0.10.66
- RUSTSEC-2025-0004: affected; fixed in 0.10.70
- RUSTSEC-2025-0022: affected; fixed in 0.10.72
Two honesty rules the tools hold to, because an agent cannot verify them itself:
a constraint is Unverified (the advisory says it fixes the issue; whether such
a release exists has not been checked), and no corpus configured means
unknown, never safe.
.synaptic/vuln-policy.toml, created by synaptic vuln init:
schema = 1
[[deny]]
package = "npm:request"
reason = "unmaintained"
replacement = "npm:undici"
[[pin]]
package = "cargo:example-crate"
minimum = "0.10.66"
reason = "organisation floor"
[[exception]]
finding = "vuln_finding_..."
reason = "vulnerable path is not reachable in this build"
expires = "2027-02-01"
approved_by = "security-review"Exception expiry is mandatory: a policy whose exception lacks a valid
YYYY-MM-DD expiry is rejected. Combined with --fail-on, the expiry is
enforced rather than advisory. With a live exception the scan reports the
finding as suppressed and exits 0; once the date passes the same command reports
it as active and exits non-zero, failing the build until someone renews or
removes the acceptance.
synaptic vuln accept vuln_finding_... \
--reason "vulnerable path unreachable" \
--until 2027-02-01 \
--approved-by security-reviewUnlike a cargo-deny ignore, which never expires and carries no approver, this
record is versioned in the repository and comes back on its own.
.synaptic/vuln/findings/<id>.json, one file per finding. Identity is a digest
of repository, advisory, package and resolved version, so rescanning is
idempotent and a finding accumulates history rather than fragmenting.
The repository half of that digest is the git remote normalized to
host/namespace/repository, not the checkout path. A policy exception names a
finding id, so a path-derived id would differ on every machine and a shared
exception would silently fail to match.
Decisions are appended, never rewritten. Records are written through a temporary file and renamed, so an interrupted write cannot truncate an audit record.
| Trigger | How |
|---|---|
| Scheduled | a cron job running synaptic vuln scan --record --fail-on p1
|
| Dependency change | the same command in a job watching lockfile paths |
| Ad hoc |
synaptic vuln scan / synaptic vuln check
|
| Agent | the three MCP tools |
--fail-on <p0|p1|p2|p3> exits non-zero when any finding is at or above that
priority, which is what makes it usable as a gate.
Four formats record what a package is needed for, and the scan reads it:
package-lock.json (dev), composer.lock (packages-dev), pubspec.lock
(dependency: "direct dev") and poetry.lock (groups or category).
For formats that record edges but no kind, Cargo above all, the manifest
supplies the kind and the lockfile supplies the edges. A crate reached only
through a [dev-dependencies] entry is therefore known to be development-only,
not just the entry itself.
Where nothing records a kind, runtime use is assumed and the finding says so:
its evidence carries dependency_scope_unrecorded, which is a different claim
from development_only_dependency. One is a reading, the other is an
assumption, and the report never lets the second pass for the first.
A development-only or feature-gated finding is de-ranked. Neither is ever dismissed: a feature this build leaves off is one another build turns on.
The bulk export stays the default for a whole-repository scan: one request, it works offline afterwards, and it tells nobody what this repository depends on.
synaptic vuln scan --online # add the API to the local corpora
synaptic vuln check cargo:serde # queries the API directly
synaptic vuln check cargo:serde --offline # local corpus onlyvuln check asks about a single package, so it goes online by default and says
which source answered. vuln scan stays offline-first, because a scan would
send the whole dependency list.
SYNAPTIC_OFFLINE=1 disables every online path regardless of flags. A failed
query is reported, never read as an empty result: check falls back to a local
corpus and labels the answer DEGRADED.
Each gap makes the tool report less certainty, never more.
- CVSS v4.0 vectors that do not parse report an unknown severity band, treated as Medium. Parseable ones are scored, and a v4 vector wins over a v3 one.
- CVSS threat and environmental metrics are not applied; scores are base scores, as they are for v3.
- Maven and Gradle without dependency locking are audited from
pom.xmldeclarations only. Transitive dependencies are invisible, so the ecosystem is reported asdirect declarations onlyand counted apart from the fully resolved ones.build.gradleversions are not parsed; use an SBOM. - Eleven of the fifteen formats record no dependency kind. Where they also record no edges, every package in them is assumed runtime-reachable.
-
uv.lockrecords dev groups as edges from the root rather than per package, and is not read for scope. - Cargo feature resolution covers this repository's own manifests only. A dependency gated behind a disabled feature of a registry crate is not resolved, because that needs the registry index.
- A live query covers only the ecosystems OSV publishes a name for, and it discloses the queried package list to OSV.
- Commands for the full flag reference
- MCP Server for the agent-facing tools
- Configuration for policy file placement
Getting started
Concepts
Using Synaptic
- Commands
- API Maintenance
- Extraction
- Querying
- Cross-Language Edges
- SQL Auditing
- Vulnerability Management
- Analysis and Reports
- Output Formats
- Visualizations
Integrations
Scaling
Reference