CodeRadius LogoCodeRadius Docs
User Guide

Vulnerability Scanning

CodeRadius matches every locked dependency version in your fleet against the OSV.dev advisory database and writes the result into the architecture graph. npm audit tells you a repository has a vulnerable package. CodeRadius tells you which services across the organization run that exact version, who owns them, and what's downstream when you schedule the upgrade. The advisory lives in the same graph as your topology.

Scanning is built into every analysis run. No flags, no separate tool, no configuration.


How it works

1. Lockfile extraction   → exact installed versions from package-lock.json, composer.lock, go.sum, ...
2. Version-aware lookup  → batched queries against the OSV.dev API, 24h cache in front
3. Graph persistence     → Vulnerability nodes linked to affected Package nodes
4. Surfacing             → dashboard, CLI report, governance policies, Cypher

Two decisions drive the precision of the result:

  • 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's no range-expansion guesswork.
  • Per-version edges. When the same package sits at different versions across repos, each advisory edge records exactly which installed versions are affected. A repo on the patched release is never flagged alongside one that isn't.

Supported ecosystems

EcosystemLockfiles readOSV ecosystem
npmpackage-lock.json, yarn.lock (classic + Berry), pnpm-lock.yaml, bun.locknpm
Composercomposer.lock (falls back to composer.json ranges if no lockfile)Packagist
Gogo.mod, go.sumGo

Python, Java, and any other language plugin without a lockfile extractor aren't scanned yet. No dependency edges with an installed version are produced for them, so nothing reaches OSV regardless of what ecosystem mapping exists internally.

Packages your own repositories publish are detected during analysis and excluded from scanning. Internal package names never leave the machine.

Severity model

Severity resolves in this order:

  1. Advisory-declared severity (database_specific.severity on GHSA records; MODERATE normalizes to MEDIUM).
  2. A base score computed from a CVSS v3.0/v3.1 vector, if one was published.
  3. UNKNOWN, if neither is available.

A CVSS v4 vector is stored as cvssVector when present, but is never scored. Only v3 vectors compute a numeric base score. So a v4-only advisory with no declared severity lands as UNKNOWN even though a vector exists.

SeverityCVSS score
CRITICAL≥ 9.0
HIGH≥ 7.0
MEDIUM≥ 4.0
LOW> 0
UNKNOWNNo scorable v3 vector and no advisory-declared severity

The raw cvssScore and cvssVector are stored alongside the categorical tier, so policies can threshold on either.


Running a scan

Automatic (default)

Every cr analyze code run scans dependencies as part of its pipeline, as the Enriching Vulnerability Data step:

cr analyze code ./services/*
# ...
# ✔ Enriching Vulnerability Data
#   Found 12 vulnerabilities across 340 packages (322 cached)

On demand

cr analyze vuln re-scans packages already in the graph without touching source code:

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

This command requires at least one prior cr analyze code run. It operates on Package nodes already in the graph and never reads your source tree. It exits with code 1 if the scan itself fails (not on finding vulnerabilities; this command reports, it doesn't gate).


Caching and network behavior

  • 24-hour local cache, keyed ecosystem:name:version, at ~/.coderadius/cache/osv/cache.json. Repeat runs within the TTL make zero network calls for unchanged dependencies. --refresh bypasses it; --offline uses it exclusively and skips anything not already cached.
  • Two request shapes, not one per package. Package versions are batched to the OSV querybatch endpoint in chunks of 1000. That endpoint returns skeleton records (id + modified timestamp only); every distinct advisory found then gets one hydration request to /v1/vulns/{id} (8 concurrent) to pull severity, summary, and affected ranges. A fleet-wide first scan costs ceil(package_versions / 1000) batch calls plus one call per distinct advisory matched. Not thousands of individual per-package requests, but not "a handful" either if your fleet has hundreds of distinct CVEs.
  • Fail-open on network failure. 429s are retried with exponential backoff and jitter (up to 3 attempts); a 30-second timeout or an unrecoverable network error degrades to a warning and an empty result for that batch. OSV being unreachable never fails an analysis run or a CI pipeline. A non-network failure during persistence (e.g. a graph write error) is not covered by this fail-open behavior and can fail the run like any other pipeline step.

Privacy

Each query sends exactly { package: { name, ecosystem }, version }: three fields, nothing else.

  • No source code, file contents, or file paths leave the machine.
  • No repository names, service names, or team/ownership metadata are transmitted.
  • Internal packages are excluded before querying, so private package names are never disclosed.
  • The only external endpoint is api.osv.dev.

For environments where even public package names can't cross the network, seed the cache from a connected host and run with --offline.


The graph model

(Repository)-[:DEPENDS_ON {installedVersion}]->(Package)-[:HAS_VULNERABILITY]->(Vulnerability)

Vulnerability node

PropertyDescription
osvIdCanonical OSV identifier (e.g. GHSA-xxxx-...).
aliasesCross-references, including the CVE-* id when one exists.
summaryAdvisory summary.
severityCRITICAL | HIGH | MEDIUM | LOW | UNKNOWN.
cvssScore, cvssVectorRaw CVSS data when published.
published, modified, withdrawnAdvisory lifecycle timestamps (withdrawn is null unless the advisory was pulled upstream).
referencesLinks to the advisory, patches, write-ups. Truncated to the first 5.
lastFetchedAtWhen this node was last refreshed from OSV.

HAS_VULNERABILITY edge

PropertyDescription
vulnerableInstalledVersionsThe installed versions in your fleet that are affected.
fixedVersionThe first release that resolves the advisory (from the affected-range events).
introducedVersionThe release that introduced the flaw.
affectedRangesThe full affected-range data from the advisory, as JSON.
lastVerifiedAt, valid_from_commit, valid_to_commitLifecycle bookkeeping.

DEPENDS_ON edge (from lockfile extraction)

PropertyDescription
installedVersionThe exact version the lockfile resolved to.
requiredVersionThe declared range from the manifest.
isDevWhether this is a dev-only dependency.

Per repo, one DEPENDS_ON edge exists per ecosystem:name. First occurrence wins, with a non-dev requirement preferred over a dev one if both appear. Lockfile processing has a 60-second timeout per repo; a repo that blows through it is skipped with a warning rather than stalling the whole run.

Query the blast radius

// 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) shows a Vulnerable KPI and a Vulnerable filter chip, CVE badges per package row color-coded by severity with the advisory summary on hover, a per-version breakdown so a package patched in one repo and exposed in another reads at a glance, and a consumer drill-down listing every repository pinned to a vulnerable version with its team owner.

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 rules like any other graph fact. A rule that flags high-severity exposure fleet-wide:

id: acme-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.
level: error
scope: package
tags:
  - security

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 --output graph to persist compliance, or gate merges on --fail-on error.


FAQ

Does scanning slow down analysis runs?

Lookups are batched and cached for 24 hours as a single pipeline step. On a warm cache almost nothing happens on the wire; on a cold cache it costs the batch calls plus one hydration call per distinct advisory matched.

What happens in CI when OSV.dev is unreachable?

The step logs a warning and continues with an empty result for that batch. Network failure is fail-open by design. See Caching and network behavior for the one case (a non-network persistence failure) that isn't covered by that guarantee.

Are dev dependencies scanned?

Yes. Every package with a locked, installed version is scanned regardless of isDev; the DEPENDS_ON.isDev flag lets you 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, nothing is flagged. That's the point of scanning lockfiles instead of manifests.

How fresh is the advisory data?

Cached 24 hours per package@version. Run cr analyze vuln --refresh right after a CVE disclosure to force a re-fetch.

Can I run this air-gapped?

Yes: seed the cache on a connected host (cr analyze vuln --refresh), copy ~/.coderadius/cache/osv/ to the isolated environment, and run with --offline.

What about withdrawn advisories?

The withdrawn timestamp is persisted on the Vulnerability node when an advisory is pulled upstream. Exclude them explicitly with WHERE v.withdrawn IS NULL, as the governance example above does.

On this page