Dependency analysis¶
Supported files¶
requirements*.txt, pyproject.toml (PEP 621/735, Poetry), poetry.lock, Pipfile.lock,
uv.lock, package.json, package-lock.json / npm-shrinkwrap.json, yarn.lock (v1 and
berry), pom.xml, build.gradle(.kts), go.mod, Cargo.lock, Cargo.toml,
packages.lock.json, *.csproj / *.fsproj / *.vbproj, Directory.Packages.props,
Directory.Build.props, packages.config, nuget.config, CycloneDX SBOMs (bom.json,
bom.xml, sbom.json, *.cdx.json, *.cdx.xml...).
| Ecosystem | Best file to scan | Notes |
|---|---|---|
| Go | go.mod |
Since Go 1.17, it lists every module of the build (// indirect included) at its selected version: it is handled like a lockfile. replace directives are applied (a fork raises DEP-003, a local path DEP-007). Pseudo-versions (v0.0.0-<date>-<commit>) are compared by date. The standard library (go and toolchain directives) is not checked: scan the binary with govulncheck. |
| Rust (crates.io) | Cargo.lock |
Every crate of the build, transitive included; the workspace members are skipped. A git source raises DEP-003, another registry DEP-005. Cargo.toml only holds ranges ("1.0" is ^1.0): it is checked for hygiene (*, git, path, registry, [patch], workspace = true). |
| .NET (NuGet) | packages.lock.json |
Enable it with <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>; the packages of every target framework are checked. In a *.csproj, Version="1.2.3" is the version restored (lowest applicable) and is checked; floating versions (1.*) raise DEP-002, versions managed in Directory.Packages.props or by an MSBuild property DEP-009. nuget.config is checked for HTTP sources (DEP-004) and for several sources without packageSourceMapping (DEP-005, dependency confusion). |
In a uv.lock, the project itself and its workspace members (editable or virtual sources) are
not dependencies and are skipped; any other git, url, path, directory or editable
source raises DEP-003.
CycloneDX SBOM (transitive dependencies)¶
pom.xml and build.gradle only list the direct dependencies: their transitive dependencies,
often the vulnerable ones, are not checked (a note says so in the result). Generate a CycloneDX
SBOM of the resolved dependency tree in the pipeline and scan it:
# Maven: writes target/bom.json and target/bom.xml
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
security-review-ci scan-deps target/bom.json --project my-project --require-database
With Gradle, the cyclonedxBom task of the org.cyclonedx.bom plugin produces the same file;
npm sbom --sbom-format cyclonedx does it for npm.
- The SBOM is read in JSON (
bomFormat: "CycloneDX") or XML (CycloneDX namespace); nested components are included. An XML SBOM with aDOCTYPEorENTITYis refused. - Each component is identified by its package URL (
purl):pkg:maven/...(Maven),pkg:npm/...(npm),pkg:pypi/...(PyPI),pkg:golang/...(Go),pkg:cargo/...(crates.io) andpkg:nuget/...(NuGet). An SBOM may mix these ecosystems. The other types (pkg:gem,pkg:composer...) and the components without apurlare counted in a note and not checked. - The versions are resolved: the SBOM is handled like a lockfile. A component of scope
excludedis markeddev; a Maven-SNAPSHOTversion raisesDEP-002.
Offline vulnerability database (OSV format)¶
The MCP server calls no external service: it looks up an OSV advisory base stored in the database. Two ways to fill it:
- manual import (no network access required);
- daily update by the controller, if it is allowed to reach the Internet, directly or through a proxy.
Manual import¶
On a connected machine, download the osv.dev exports
(https://osv-vulnerabilities.storage.googleapis.com/PyPI/all.zip, same for npm, Maven,
Go, crates.io and NuGet), transfer them, then import them (CLI-only operation, idempotent):
security-review-controller advisories import PyPI.zip # .json, folder or .zip
security-review-controller advisories import npm.zip
security-review-controller advisories import Maven.zip --ecosystem Maven # optional filter
security-review-controller advisories status
Automatic update by the controller¶
With SECREVIEW_OSV_AUTO_UPDATE=true, security-review-controller serve downloads the
all.zip export of every ecosystem and imports it, then starts again every 24 hours:
SECREVIEW_OSV_AUTO_UPDATE=true
# Optional: outgoing HTTP proxy of the controller (credentials accepted, never logged)
HTTPS_PROXY=http://user:password@proxy.example.com:3128
- Disabled by default. The MCP server itself never connects outside.
- The due time depends on the age of the database (last import, manual or automatic): a restart
or a second replica of the controller does not download again before the end of the interval
(
SECREVIEW_OSV_UPDATE_INTERVAL_HOURS, 24 by default). An empty or too old database is updated at start-up. - Proxy: the download goes through the outgoing proxy of the controller, given by the
standard variables
HTTPS_PROXY(elseHTTP_PROXY; lowercase names accepted;http://[user:password@]host:port,host:portmeaninghttp://host:port,CONNECTtunnel to the HTTPS site), except to the hosts ofNO_PROXY. The same proxy serves the OIDC login. Without these variables, the connection is direct. If the proxy inspects TLS, give its CA withSECREVIEW_OSV_CAFILE(it then replaces the system CAs). - Mirror:
SECREVIEW_OSV_URLreplaces the source (Nexus, Artifactory, internal bucket…) as long as it serves<URL>/<ecosystem>/all.zipover HTTPS. Plain HTTP, credentials in the URL and redirects to anything other than HTTPS are refused. SECREVIEW_OSV_ECOSYSTEMS(PyPI,npm,Maven,Go,crates.io,NuGetby default) limits the downloaded exports; every archive is bounded bySECREVIEW_OSV_MAX_DOWNLOAD_MB(1024 MiB), written to a temporary folder then deleted.- A failed ecosystem (network, size, invalid archive) does not prevent the others; it is logged and a new attempt takes place one hour later.
Hosts without proxy¶
NO_PROXY (or the list of the Outgoing proxy screen) gives, comma separated, the
destinations reached without the proxy:
| Entry | Example | Effect |
|---|---|---|
| Domain | corp.example or .corp.example |
The domain and all its subdomains (sso.corp.example) |
| Subdomain wildcard | *.corp.example |
Same as .corp.example |
| IP address | 10.0.0.5, [fd00::5] |
This address (a port, 10.0.0.5:8443, is ignored) |
| Everything | * |
No connection goes through the proxy |
An address only applies to a URL whose host is that IP address (https://10.0.0.5/...): a
host name is never resolved. To bypass the proxy towards an internal server reached by its name,
give its domain. Address ranges (CIDR, 10.0.0.0/8) are not supported: in NO_PROXY, such an
entry matches no host, and the Outgoing proxy screen refuses it. The loopback (localhost,
127.0.0.0/8, ::1) is always reached directly.
NO_PROXY=.corp.example,10.0.0.5
The proxy can also be chosen by an administrator in the Outgoing proxy
screen of the web interface, for the variables that are not set: HTTPS_PROXY (else
HTTP_PROXY) and NO_PROXY, when set, take precedence over the values of the screen. They
apply to the next download. The OSV database screen shows the state of the database, the history of the updates
(scheduled, CLI or web) and an Update now button (run in the background, one update at a
time), only available when SECREVIEW_OSV_AUTO_UPDATE=true.
To trigger an update on demand (or from a CronJob rather than from the controller), with the
same settings:
security-review-controller advisories update # ecosystems of SECREVIEW_OSV_ECOSYSTEMS
security-review-controller advisories update --ecosystem npm # repeatable option
# exit code: 0 = all imported, 2 = at least one ecosystem failed or invalid setting
The controller downloads at most a few hundred MiB a day (the npm export is the largest): plan
the matching space in its temporary folder (/tmp).
Scan in a pipeline (security-review-ci scan-deps)¶
In a pipeline, the pipeline client sends
the dependency files to the controller, which checks them against its advisory database and,
with --save, stores a dependency-scan report of the project. The pipeline needs neither the
database nor the package, only a ci token
(or admin) of the project:
export SECREVIEW_CONTROLLER_URL=https://secreview.example.com
export SECREVIEW_TOKEN=... # ci token restricted to my-project
security-review-ci scan-deps package-lock.json requirements.txt \
--fail-on high --require-database --project my-project --save
# exit code: 0 = ok, 1 = blocked, 2 = error
--require-databaserefuses the scan (code2, nothing stored) when the vulnerability check cannot run for a file.- With
--save, the user of a personalcitoken is the author of the report, else--authorstores the username of the developer with it (see author of a report),--repository,--branchand--committhe context; the label of the token is stored as the device. - Each file is limited to the
SECREVIEW_MAX_DEPENDENCY_CHARSof the controller, the request toSECREVIEW_MAX_SCAN_REQUEST_MB. Symbolic links are not read.
Scan from a workstation (commit hook)¶
Step-by-step setup, with the agent review: Commit hooks.
A developer runs the same security-review-ci scan-deps on their workstation, with a
personal mcp or ci token created in
My account: it reaches the projects the developer may read, and the files are sent to the
controller exactly as they are on disk. The workstation needs no Python: install the client with
uv (see Installation).
uv tool install security-review-ci
export SECREVIEW_CONTROLLER_URL=https://secreview.example.com
export SECREVIEW_TOKEN=<personal mcp or ci token>
security-review-ci scan-deps uv.lock web/package-lock.json --project my-project --require-database
# exit code: 0 = ok, 1 = blocked, 2 = error (controller unreachable, token refused...)
Commit hook (prek or pre-commit), run when a
manifest or a lockfile changes. With language: python, the hook installs security-review-ci
in its own environment: nothing else to install. prek is a single binary that also downloads
the Python of the hook (pre-commit needs a Python):
repos:
- repo: local
hooks:
- id: scan-deps
name: dependency scan
entry: security-review-ci scan-deps --skip-unconfigured --require-database --project my-project
language: python
additional_dependencies: ["security-review-ci==X.Y.Z"] # version of your controller
files: (^|/)(uv\.lock|requirements[^/]*\.txt|pyproject\.toml|package(-lock)?\.json)$
With the client already installed (uv tool install), language: system runs it from the
PATH instead.
--skip-unconfigured lets the commit through, with a warning, when SECREVIEW_CONTROLLER_URL
is not set (contributors without a controller). The two variables are read from the
environment only, never from the .env file.
Reading the result¶
The vulnerability_check field says whether the check took place:
| Value | Meaning |
|---|---|
performed |
The versions were compared with the database |
skipped_no_advisory_database |
No database imported |
skipped_no_advisories_for_ecosystem |
No advisory for this ecosystem |
Empty list ≠ safe dependencies
In the last two cases, an empty list does not mean that the dependencies are safe.
--require-database makes the pipeline fail in that case.
- A database older than 30 days is reported.
- A severity missing from the advisory is handled as
medium(severity_known: false). - Range versions (
>=2) cannot be checked: analyze the lockfile. - For an SBOM that mixes ecosystems, the check is
performedas soon as one of them has advisories; a note names the ecosystems that could not be checked. - Each component is marked
devor not. Inpyproject.toml, the extras named after development (dev,test,docs,lint...) and the[dependency-groups]aredev; the other extras (postgres...) are installed in production and stay runtime dependencies. - Hygiene rules
DEP-001toDEP-009: unpinned versions, URL / VCS, HTTP, extra index,--trusted-host, etc.