Vulnerability Scanning
Fleet-wide CVE intelligence wired into the architecture graph. CodeRadius matches every locked dependency version against OSV.dev and connects each advisory to the services, teams, and blast radius it actually touches.
Vulnerability Scanning
CodeRadius scans every package dependency in your fleet against the OSV.dev vulnerability database and writes the results into the architecture graph. Scanning is built into every analysis run: no flags, no configuration, no separate tool to operate.
The difference from a per-repo auditor is the graph. npm audit tells you a repository has a vulnerable package. CodeRadius tells you which services across the organization run the vulnerable version, who owns them, and what is downstream when you schedule the upgrade.
Vulnerability scanning is enabled by default. Every cr analyze code run enriches the graph with current advisory data as part of the standard pipeline.
How It Works
1. Lockfile extraction → exact installed versions from package-lock, composer.lock, go.sum, ...
2. Version-aware lookup → batched queries against the OSV.dev public database
3. Graph persistence → Vulnerability nodes linked to affected Package nodes
4. Surfacing → dashboard, CLI report, governance policies, CypherTwo design decisions drive the precision of the results:
- Lockfiles, not manifests. CodeRadius matches the exact version your lockfile pins, never a semver range. A
^4.17.0declaration that resolves to a patched4.17.21produces no finding. There is no range-expansion guesswork and no noise from versions you don't actually ship. - Per-version edges. When the same package appears at multiple versions across the fleet, each advisory edge records precisely which installed versions are affected. Repositories on the patched release are never flagged alongside the ones that need the upgrade.
Supported Ecosystems
| Ecosystem | Lockfiles read | OSV ecosystem |
|---|---|---|
| npm | package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock | npm |
| Composer | composer.lock | Packagist |
| Go | go.mod, go.sum | Go |
| Python | requirements.txt | PyPI |
Packages published by your own repositories are detected during analysis and excluded from scanning automatically. Internal package names never leave the machine.
Severity Model
Severity is derived from the CVSS v3/v4 vector published in the advisory:
| Severity | CVSS score |
|---|---|
CRITICAL | ≥ 9.0 |
HIGH | ≥ 7.0 |
MEDIUM | ≥ 4.0 |
LOW | > 0 |
UNKNOWN | No CVSS vector published |
The raw CVSS score and vector are stored alongside the categorical tier, so downstream policies can threshold on either.
Running a Scan
Automatic (default)
Every analysis run scans dependencies as part of its pipeline. The step appears as Enriching Vulnerability Data in the task list, and the run summary reports the outcome:
cr analyze code ./services/*
# ...
# ✔ Enriching Vulnerability Data
# Found 12 vulnerabilities across 340 packages (322 cached)On Demand
cr analyze vuln re-scans the packages already in the graph without re-analyzing source code. Use it to refresh advisory data between syncs, or after a CVE disclosure when you need an answer now:
# Re-scan all packages in the graph
cr analyze vuln
# Bypass the 24h cache and force a re-fetch from OSV
cr analyze vuln --refresh
# Air-gapped / offline: evaluate against cached advisory data only
cr analyze vuln --offline
# Machine-readable output for CI pipelines
cr analyze vuln --json| Option | Description |
|---|---|
--refresh | Force re-fetch from OSV even if the local cache is fresh |
--offline | Skip all API calls, evaluate using cached data only |
--json | Output the scan result as JSON |
-v, --verbose | Enable verbose logging |
The standalone command operates on the dependency graph, so it requires at least one prior cr analyze code run. Lockfile extraction happens during analysis; the scan itself never touches your source tree.
Caching and Network Behavior
Scanning is designed to be invisible in the inner loop and safe in CI:
- 24-hour local cache. Every
package@versionlookup is cached in~/.coderadius/cache/osv/. Repeat runs within the TTL make zero network calls for unchanged dependencies.--refreshbypasses the cache;--offlineuses it exclusively. - Batched queries. Lookups are sent to the OSV batch endpoint in chunks, not one request per package. A fleet-wide first scan is a handful of HTTP calls, not thousands.
- Fail-open. Rate limits are retried with exponential backoff; timeouts and network failures degrade to a warning and an empty result. A vulnerability scan failure never fails your analysis run or your CI pipeline. Offline machines simply skip enrichment.
Privacy and Data Handling
The scan sends exactly three fields per public package: the ecosystem, the package name, and the installed version. Nothing else.
- No source code, file contents, or file paths leave the machine.
- No repository names, service names, or organizational metadata are transmitted.
- Internal packages are excluded before querying, so private package names are never disclosed.
- The only external endpoint is
api.osv.dev, the public database operated by Google's Open Source Security Team.
For environments where even public package names cannot be transmitted, run with --offline against a cache seeded from a network-connected host.
The Graph Model
Each advisory becomes a first-class node, connected to every affected package with a version-aware edge:
(Repository)-[:DEPENDS_ON {installedVersion}]->(Package)-[:HAS_VULNERABILITY]->(Vulnerability)Vulnerability node
| Property | Description |
|---|---|
osvId | Canonical OSV identifier (e.g. GHSA-xxxx-...) |
aliases | Cross-references, including the CVE-* identifier when one exists |
summary | Human-readable advisory summary |
severity | CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN |
cvssScore, cvssVector | Raw CVSS data when published |
published, modified, withdrawn | Advisory lifecycle timestamps |
references | Links to the advisory, patches, and write-ups |
HAS_VULNERABILITY edge
| Property | Description |
|---|---|
vulnerableInstalledVersions | The installed versions in your fleet that are affected |
fixedVersion | The first release that resolves the advisory |
introducedVersion | The release that introduced the flaw |
affectedRanges | The full affected-range data from the advisory |
Query the Blast Radius
Because advisories live in the same graph as your topology, remediation questions are one traversal away:
// Which repositories run a critical-severity vulnerable version,
// and what release ships the fix?
MATCH (r:Repository)-[d:DEPENDS_ON]->(p:Package)
-[hv:HAS_VULNERABILITY]->(v:Vulnerability)
WHERE v.severity = 'CRITICAL'
AND d.installedVersion IN hv.vulnerableInstalledVersions
RETURN v.osvId, p.name, d.installedVersion, hv.fixedVersion, r.name
ORDER BY v.cvssScore DESCWhere Results Surface
Architecture Dashboard
The Package Intelligence view (cr ui) renders vulnerability data in context:
- A Vulnerable KPI and a one-click Security filter scoped to affected packages.
- CVE badges on each package row, color-coded by severity, with the advisory summary on hover.
- Per-version stamping: the version breakdown shows which installed versions carry which CVEs, so a package that is patched in one repo and exposed in another reads at a glance.
- Consumer drill-down: expand a package to see every repository pinned to a vulnerable version, with team ownership alongside.
For CI and custom tooling, cr ui --json exposes the same data in the deps domain of the payload.
Governance Policies
Advisory data participates in Governance like any other graph fact. A policy that blocks high-severity exposure across the fleet:
id: cr-sec-001-no-high-severity-cves
name: No high-severity vulnerabilities in locked dependencies
description: >
Every external package pinned by a lockfile must be free of known
CRITICAL or HIGH advisories affecting the installed version.
severity: error
scope: package
tags:
- security
- golden-path
query: |
MATCH (p:Package)
WHERE p.isInternal = false
OPTIONAL MATCH (p)-[hv:HAS_VULNERABILITY]->(v:Vulnerability)
WHERE v.severity IN ['CRITICAL', 'HIGH']
AND v.withdrawn IS NULL
WITH p, collect(v.osvId) AS advisories
RETURN
p.id AS entityId,
p.name AS entityName,
'package' AS entityType,
CASE WHEN size(advisories) > 0 THEN 'fail' ELSE 'pass' END AS status,
CASE WHEN size(advisories) > 0
THEN 'Known high-severity advisories: ' + reduce(s = '', a IN advisories | s + a + ' ')
ELSE '' END AS detailRun it with cr policy verify to gate merges, or let the dashboard's Governance tab track compliance over time.
FAQ
Does scanning slow down my analysis runs?
No. Lookups are batched, cached for 24 hours, and run as a single pipeline step. On a warm cache the step completes in well under a second; on a cold cache it costs a few batched HTTP calls.
What happens in CI when OSV.dev is unreachable?
The step logs a warning and continues. Vulnerability enrichment is fail-open by design: a network failure never breaks an analysis run or a pipeline.
Are dev dependencies scanned?
Yes. Every package with an exact locked version is scanned, and the dependency edge records whether it is a dev dependency, so you can separate the two in queries and policies.
Why doesn't a vulnerable semver range show up?
CodeRadius scans installed versions, not declared ranges. If your lockfile resolves a vulnerable range to a patched release, you are not exposed and nothing is flagged. That is the point.
How fresh is the advisory data?
Advisory responses are cached for 24 hours per package@version. Use cr analyze vuln --refresh to force a re-fetch, for example right after a major CVE disclosure.
Can I run this in an air-gapped environment?
Yes. Seed the cache from a connected host (cr analyze vuln --refresh), copy ~/.coderadius/cache/osv/ to the isolated environment, and run with --offline.
What about withdrawn advisories?
Advisories are occasionally withdrawn upstream. The withdrawn timestamp is persisted on the Vulnerability node, so queries and policies can exclude them explicitly with WHERE v.withdrawn IS NULL, as the governance example above does.