Auditing dependencies
Fabr has two commands that report on the packages a target depends on, without building it.
fabr audit checks them against the OSV database of security advisories, and
fabr sbom writes them out as a CycloneDX software bill of materials.
Both work on npm and Maven packages alike, and both read the same dependency tree that
fabr deps prints: the one your target’s build would use.
| You want to | Section |
|---|---|
| Find advisories that affect your dependencies | Running fabr audit |
| Understand the report | Reading the report |
| Fix what it found | Acting on an advisory |
| Use a mirror or an internal advisory database | Choosing the advisory database |
| Produce an SBOM | Writing a bill of materials |
| Run either in CI | In CI |
Compared with the tools you know
Section titled “Compared with the tools you know”npm audit: checks the tree recorded in package-lock.json against the GitHub Advisory
Database, and npm audit fix updates the lockfile. fabr: checks the versions fabr selects for
the build, which follow from your build files alone, and changes nothing; you raise a version by
editing the build file. Fabr selects the
lowest version that satisfies every requirement,
not the newest, so a dependency’s dependencies are often older than the ones a fresh
npm install would give you, and fabr audit reports advisories that npm audit on a new
lockfile wouldn’t. The report is about what you actually build.
Dependabot: GitHub reads your manifests and lockfiles on its servers, raises alerts, and can open pull requests that update them. fabr: you run the check yourself, locally or in CI, and the result can go to GitHub code scanning as SARIF. Nothing opens pull requests.
OWASP Dependency-Check: scans jar files and other artifacts and matches them against the NVD
by CPE, which needs a local copy of the NVD data. fabr: names each package exactly, by its
package URL (pkg:maven/org.apache.commons/commons-text@1.9),
and asks the OSV API about it; there are no files to scan and no database to download.
cyclonedx-npm, the CycloneDX Maven plugin: read an installed node_modules tree or a Maven
build. fabr: writes the bill of materials from the resolved tree, without downloading any
package, and the same sources always give the same document.
What fabr audit checks, and what it leaves out:
- Checked: every package from npm or a Maven repository among what a target’s build requires, directly or through other packages, at the exact version selected.
- Checked as the package it changes: a patched package, at the
version it was made from. An advisory its
fixesnames is reported as fixed by the patch. - Not checked: other packages built in your project, which have no published version to ask about (what they depend on is checked); the tools fabr runs during a build, such as the TypeScript compiler, a bundler or a test runner, which aren’t part of the target’s dependencies; and the Node.js release and the JDK.
- No fixing. The report names the versions that fix each advisory; you choose one and edit the build file.
- No ignore list or severity threshold. Every advisory found is reported, and any one of them makes the command fail, except one a patched package declares it fixes.
- Advisories are read live. Each run asks the database afresh and caches nothing, so the same commit can pass one day and fail the next, when a new advisory is published.
Running fabr audit
Section titled “Running fabr audit”Name the targets whose dependencies you want checked:
fabr audit appA name can be anything fabr deps takes:
- A target, or several:
fabr audit app worker. Each is checked separately and the report lists one path per target that reaches an affected package. - A property listing targets, such as
SHIPPED = app worker;:fabr audit SHIPPED. - A package from a registry, with no target declared, to check it before you add it:
fabr audit @npm:lodash:4.17.21,fabr audit @mvn:org.apache.commons:commons-text:1.10.0. This works outside a project too. - A catalog member:
fabr audit @pkg:react.
A target is checked as its build resolves it: a js_package’s deps, provided_deps and the
packages they bring in, but not its test_deps. To check what its tests use as well, require the
test operation on the name:
fabr audit 'app<BUILD_OPERATION=test>'A target type with no build of its own is read under the one operation it has: a java_program
or a js_script under run.
For Java, a jar_package is checked against its compile classpath, which leaves out the
runtime-scope dependencies of the packages it uses. Audit what you deploy instead: a
java_program, war_package or java_installation is checked against its runtime classpath, the
jars it actually runs with. See
Compile and runtime classpaths.
fabr audit resolves versions exactly as a build would, reading only registry metadata, so it
builds nothing and downloads no packages. The command-line reference has
its options.
Reading the report
Section titled “Reading the report”The report goes to stdout; fabr’s own progress lines go to stderr, and -q leaves them out. For a
target with deps = @npm:async:2.6.3 @npm:minimist:1.2.8;:
GHSA-35jh-r3h4-6jhm (CVE-2021-23337) HIGH Command Injection in lodash lodash@4.17.14, fixed in 4.17.21 app > async@2.6.3 > lodash@4.17.14 https://nvd.nist.gov/vuln/detail/CVE-2021-23337
GHSA-fwr7-v2mv-hh25 (CVE-2021-43138) HIGH Prototype Pollution in async async@2.6.3, fixed in 3.2.2, 2.6.4 app > async@2.6.3 https://nvd.nist.gov/vuln/detail/CVE-2021-43138
…
6 advisories affect 2 of 3 packages checkedEach advisory is one block, the most severe first:
- The heading is the advisory’s OSV id with its aliases (usually the CVE), the database’s
severity rating where it gives one (
CRITICAL,HIGH,MODERATE,LOW), and its summary. - Each affected package follows at the version your tree has, with the versions the advisory
says fix it, highest first. An advisory fixed on several release lines lists a version for each
(
fixed in 3.2.2, 2.6.4): choose the one on the line you want to stay on. - The path shows how the target reaches the package, one per target named.
(+2)at the end of a path means the tree reaches the package two more ways, such as through another package that depends on it or your own requirement on it. - The link is the advisory’s published page.
The last line counts the advisories and the packages checked. With nothing found it reads
No known advisories affect the 3 packages checked.
A Maven package is named by its group and artifact:
GHSA-599f-7c49-w659 (CVE-2022-42889) CRITICAL Arbitrary code execution in Apache Commons Text org.apache.commons/commons-text@1.9, fixed in 1.10.0 greeter > org.apache.commons/commons-text@1.9 https://nvd.nist.gov/vuln/detail/CVE-2022-42889Exit status
Section titled “Exit status”fabr audit exits with 0 when no advisory affects any package checked, and with 1 when one does.
An advisory a patched package fixes doesn’t count.
It also exits with 1 when the audit couldn’t run: an unknown name, a dependency that can’t be
resolved, or an advisory database that can’t be reached, each reported on stderr with an error:
line. The command-line reference lists every status.
SARIF for code scanning
Section titled “SARIF for code scanning”--format sarif writes the report as SARIF 2.1.0,
the format GitHub code scanning and other code-scanning services read:
fabr audit --format sarif app > audit.sarifEach advisory is a rule, with its id, summary, link, aliases and a security-severity score
derived from its rating, and each affected package is a result: error for a critical or high
advisory, warning for a moderate or unrated one, note for a low one. A result is placed at the
line of your build file whose reference brings the package in, even when the package arrives
through another one, so the alert for lodash in the example above points at the deps line that
names async. A package you name on the command line, such as @npm:lodash:4.17.15, has no
line to point at, so its result has no location. The file paths are relative to your project’s root directory, the one holding
PROJECT.fabr. Each result’s properties carry the package URL, the fixed versions and the paths.
Choosing the advisory database
Section titled “Choosing the advisory database”The advisories come from the server the ADVISORIES property names, https://api.osv.dev unless
you set it. Any server answering the OSV API works, such
as a mirror or an internal database. Set it in your project:
ADVISORIES = https://osv.internal.example.com;or for one run:
fabr -DADVISORIES=https://osv.internal.example.com audit appActing on an advisory
Section titled “Acting on an advisory”The remedy is a later version of the affected package. Fabr chooses one version of each package
for the whole tree, the highest of the minimum versions anything asks for, so raising the minimum
anywhere raises it everywhere. Pick a version from the report’s fixed in list, then run
fabr audit again to confirm.
| The affected package is | Do this |
|---|---|
| Your own direct dependency | Raise the version in deps: @npm:minimist:1.2.0 becomes @npm:minimist:1.2.8. |
| A dependency of a dependency | First see whether a later release of the package you depend on requires a fixed version. If not, require the fixed version yourself, as below. |
| Brought in through a catalog | Add the fixed version to the catalog, and every target using it gets it. |
| Held back by a range that excludes the fix | Force it with !, as below. |
| Not fixed in any release | Patch it, as below, or remove the dependency. |
Requiring a fixed version yourself
Section titled “Requiring a fixed version yourself”Add a requirement with the fixed version as its minimum. For a package that async brings in:
js_package app { srcs = src:**/*.ts; deps = @npm:async:2.6.4 @npm:lodash:^4.18.1;}fabr deps app shows the result: lodash:^4.17.14 -> 4.18.1 under async. The requirement also
makes lodash a dependency of app in its published package.json. To raise it without that,
use a catalog where the target already takes the dependency from one:
catalog @pkg { deps = @npm:async:2.6.4 @npm:lodash:4.18.1;}
js_package app { srcs = src:**/*.ts; deps = @pkg:async;}A ? marker doesn’t help here: @npm:lodash:4.18.1? permits that version without asking for it,
so it raises nothing.
Maven works the same way: a plain version in a requirement is a minimum.
java_installation libs { deps = @mvn:org.apache.commons:commons-text:1.10.0 @mvn:org.apache.commons:commons-lang3:3.18.0; # raises commons-text's 3.12.0}A BOM raises every package it manages that the tree uses, which suits a family of artifacts released together:
deps = @mvn:org.apache.logging.log4j:log4j-bom:2.25.5 @mvn:org.apache.logging.log4j:log4j-core:2.14.1; # selects 2.25.5See How versions are chosen and BOMs.
Forcing a version
Section titled “Forcing a version”When a package’s range excludes the fixed version (a dependency asking for ~4.17.15 when the fix
is 4.18.0), raising the minimum gives a version conflict instead. Force the version with !,
which replaces every requirement on the package, like npm’s overrides:
deps = @npm:async:2.6.4 @npm:lodash:4.18.1!;A ! needs an exact version. Use it only when you know the forced version works with the package
whose range it overrides. See
When versions conflict and, for Maven,
its section.
Patching a package
Section titled “Patching a package”Where no fixed release exists, a patched target
replaces the package everywhere in the build with your own copy. fabr audit still checks a
patched package, at the version it was made from, so the advisory is still reported. To say that
the patch fixes it, list the advisory in the target’s fixes, by its OSV id or any of its aliases:
patched lodash { srcs = @npm:lodash:4.17.15; patches = patches/lodash-template.patch; fixes = GHSA-35jh-r3h4-6jhm;}The report then shows the package as fixed by the patch, and that advisory no longer makes the command fail:
GHSA-35jh-r3h4-6jhm (CVE-2021-23337) HIGH Command Injection in lodash lodash@4.17.15, fixed by 'lodash' app > async@2.6.3 > lodash@4.17.15 https://nvd.nist.gov/vuln/detail/CVE-2021-23337
No known advisories affect the 3 packages checked; 1 advisory is fixed by patched packagesEvery other advisory affecting the original version is reported as usual, against
lodash@4.17.15 as patched by 'lodash'. Fabr doesn’t check that the patch actually fixes what
fixes lists; that is your claim. An id in fixes that names no advisory affecting the package
gets a warning on stderr. That happens when srcs has moved to a version the advisory doesn’t
affect, or when the id is misspelt. So does a fixes on a patched target whose srcs isn’t one
registry package (vendored files, say): there is no published version for an advisory to affect.
Remove the patch once a fixed release is out.
In SARIF, a fixed package’s result carries a suppressions entry of kind inSource. In a bill of
materials, a patched package’s component has a pedigree: the original package as its ancestor,
and a patch that resolves each advisory in fixes.
Writing a bill of materials
Section titled “Writing a bill of materials”fabr sbom writes a CycloneDX 1.6 bill of materials of one
target or package to stdout, as JSON:
fabr sbom app > app.cdx.jsonfabr sbom @mvn:org.apache.commons:commons-text:1.10.0 > commons-text.cdx.jsonThe packages it lists are the ones fabr audit checks for the same name, read the same way, so a
<BUILD_OPERATION=…> on the name, and the Java classpath, apply here too.
It takes exactly one name.
The document holds:
- The component described, under
metadata.component: the target, as anapplication, named as the target is, with theversionit declares. A registry package named on the command line is described as alibrary, by its package URL. - A component for every package among its dependencies, direct or not, each a
librarynamed by its package URL (pkg:npm/async@2.6.3,pkg:maven/org.apache.commons/commons-lang3@3.11), with its Maven group or npm scope asgroup. A package built in your project is a component by its name, and its version where the target declares one. A patched package also has apedigreenaming the package it was made from and the advisories itsfixeslists. - Its licence, as its registry publishes it: npm’s
licensefield, which is usually an SPDX id ("id": "MIT") or expression, and Maven’s POM<licenses>, taken from the nearest parent POM that declares one, which is usually a name and URL ("name": "Apache License, Version 2.0"). - Its digest, as its registry publishes it: npm’s
dist.integrity, usually SHA-512, and the.sha1Maven publishes beside the jar. - The dependency graph, under
dependencies: what each component depends on directly. - The tool: fabr and its version, under
metadata.tools.
{ "type": "library", "bom-ref": "pkg:npm/lodash@4.17.14", "name": "lodash", "version": "4.17.14", "purl": "pkg:npm/lodash@4.17.14", "licenses": [{ "license": { "id": "MIT" } }], "hashes": [{ "alg": "SHA-512", "content": "9a62986d6dc62ee2…" }]}Licences and digests come from each package’s registry metadata for its exact version, which fabr reads the first time and then keeps in its cache, since a published version’s metadata never changes. No package is downloaded.
The document carries no timestamp and no serial number, so the same sources and the same fabr
version write the same bytes every time. You can commit it, or compare it between releases with
diff.
Run fabr audit in its own job, or on a schedule, so that a newly published advisory doesn’t
block an unrelated merge; upload its SARIF to your code-scanning service; and keep fabr sbom’s
output with each release. Continuous integration has the
GitHub Actions steps.
Troubleshooting
Section titled “Troubleshooting”| You see | Cause | Fix |
|---|---|---|
| An advisory for a version older than npm would install | Fabr selects the lowest version that satisfies every requirement, not the newest. | Require the fixed version; see Acting on an advisory. |
The advisory is still reported after adding @npm:x:1.2.3? |
A ? permits a version without requiring it. |
Write the requirement without ?: @npm:x:^1.2.3. |
No packages to audit for 'app<BUILD_OPERATION=test>' |
The target declares no tests, so it has nothing to test. |
Audit the target without the operation, or add its tests. |
No packages to audit |
The name has no registry packages among its dependencies. | Check with fabr deps <name> that you named the right target. |
error:https://osv.example.com/v1/querybatch answered 404: … |
ADVISORIES names something that isn’t an OSV API server, or has a path on the end. |
Set ADVISORIES to the server’s root URL, such as https://api.osv.dev. |
error:getaddrinfo ENOTFOUND osv.example.com |
The advisory server can’t be reached. | Check the URL and the network; behind a proxy, set HTTPS_PROXY. |
The 'sbom' command takes a single target |
fabr sbom describes one thing. |
Run it once per target, or name a property listing them with fabr audit instead. |
error: 'app:**' selects files of 'app' |
Both commands take a target or package, not a selection of its files. | Name the target: app. |
Option '--format' is not valid for the 'sbom' command |
fabr sbom writes only CycloneDX JSON. |
Leave --format out. |