CodeRadius LogoCodeRadius Docs

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, Cypher

Two 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.0 declaration that resolves to a patched 4.17.21 produces 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

EcosystemLockfiles readOSV ecosystem
npmpackage-lock.json, yarn.lock, pnpm-lock.yaml, bun.locknpm
Composercomposer.lockPackagist
Gogo.mod, go.sumGo
Pythonrequirements.txtPyPI

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:

SeverityCVSS score
CRITICAL≥ 9.0
HIGH≥ 7.0
MEDIUM≥ 4.0
LOW> 0
UNKNOWNNo 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
OptionDescription
--refreshForce re-fetch from OSV even if the local cache is fresh
--offlineSkip all API calls, evaluate using cached data only
--jsonOutput the scan result as JSON
-v, --verboseEnable 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@version lookup is cached in ~/.coderadius/cache/osv/. Repeat runs within the TTL make zero network calls for unchanged dependencies. --refresh bypasses the cache; --offline uses 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

PropertyDescription
osvIdCanonical OSV identifier (e.g. GHSA-xxxx-...)
aliasesCross-references, including the CVE-* identifier when one exists
summaryHuman-readable advisory summary
severityCRITICAL | HIGH | MEDIUM | LOW | UNKNOWN
cvssScore, cvssVectorRaw CVSS data when published
published, modified, withdrawnAdvisory lifecycle timestamps
referencesLinks to the advisory, patches, and write-ups

HAS_VULNERABILITY edge

PropertyDescription
vulnerableInstalledVersionsThe installed versions in your fleet that are affected
fixedVersionThe first release that resolves the advisory
introducedVersionThe release that introduced the flaw
affectedRangesThe 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 DESC

Where 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 detail

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

On this page