Tutorial
From first scan to a CI gate on a real project, with representative output from the workflow.
Last updated October 25, 2026View source (v0.30.0)
This walkthrough takes a real project from a first scan to a pull request gate in CI: scan it, understand a finding, cut the list down to what matters, accept the existing findings, and wire the gate into CI. Every command was run against the pinned repository shown, and the output is what it printed.
Getting started shows each feature on its own. This page combines them into the workflow you would deploy.
Prerequisites
bomlyon yourPATH(Installation); check withbomly versiongit- Network access for the enrichment steps (Network and privacy lists what
--enrichcontacts)
The project is a deliberately vulnerable demo maintained by the Bomly organization, pinned to a tag so the dependency graph you scan matches this page. The outputs were captured with Bomly 0.20.2 on 2026-07-30. The package counts are stable, but advisory counts and IDs may differ on your run because vulnerability databases change:
git clone --depth 1 --branch v1.0.0 https://github.com/bomly-dev/example-javascript-npm
cd example-javascript-npm
Step 1: inventory the project
bomly scan
✓ 136 packages in 1 manifest (14 direct, 122 transitive · runtime 43, dev 93)
Top-level dependencies
NAME VERSION LICENSE SCOPE VULNS
algo-httpserv 1.1.1 - runtime -
js-yaml 3.13.0 - runtime -
larvitbase 3.1.3 - runtime -
larvitfs 2.3.1 - runtime -
larvitreqparser 0.2.1 - runtime -
larvitrouter 3.0.2 - runtime -
larvitutils 2.3.0 - runtime -
lodash 4.17.15 - runtime -
marked 0.3.19 - runtime -
mocha 7.2.0 - development -
node-yaml-config 0.0.5 - runtime -
semver 5.7.1 - runtime -
to 0.2.9 - runtime -
url 0.11.0 - runtime -
Fourteen direct dependencies pull in 136 packages. The other 122 are transitive, and most problems come from those. No network call happened yet: the license and vulnerability columns stay empty until you enrich.
Step 2: ask why a package is there
Pick a package you never installed on purpose. minimist is a classic:
bomly explain minimist
ecosystem npm
manager npm
package minimist@0.0.10
scope runtime
direct no
introduced by:
example-javascript-vulnerable-methods@0.0.1
└─ to@0.2.9
└─ optimist@0.6.1
└─ minimist@0.0.10 [analyzed] (transitive)
Read the chain from the bottom: to remove minimist@0.0.10 you have to deal with optimist, and for that with the direct dependency to. That is the fact you need before you file an upgrade PR. (explain reference)
Step 3: add vulnerability and license data
bomly scan --enrich
✓ 136 packages in 1 manifest (14 direct, 122 transitive · runtime 43, dev 93)
✓ Enriched via Grype, deps.dev License Matcher
✓ 22 fix suggestions for 22 of 23 vulnerable packages.
Run again with --format json to see remediation details.
Top-level dependencies
NAME VERSION LICENSE SCOPE VULNS
algo-httpserv 1.1.1 MIT runtime 1H
js-yaml 3.13.0 MIT runtime 1H 1M
...
Twenty-three vulnerable packages, and a fix exists for all but one. Enrichment sends package names and versions, never your code, to the services listed in Matchers.
Step 4: turn data into a gate
bomly scan --enrich --audit --fail-on high
echo $?
Findings (31 rows: 25 gating vulnerabilities + 6 license warnings — first ten shown)
Findings
[CRITICAL] GHSA-cf4h-3jhx-xvhq underscore@1.9.2
[CRITICAL] GHSA-xvch-5gv4-984h minimist@0.0.10
[CRITICAL] GHSA-xvch-5gv4-984h minimist@1.2.5
[HIGH] GHSA-23c5-xmqv-rm74 minimatch@3.0.4
[HIGH] GHSA-35jh-r3h4-6jhm lodash@4.17.15
[HIGH] GHSA-3ppc-4f35-3m26 minimatch@3.0.4
[HIGH] GHSA-5v2h-r2cx-5xgj marked@0.3.19
[HIGH] GHSA-8j8c-7jfh-h6hx js-yaml@3.13.0
[HIGH] GHSA-cgjv-rghq-qhgp algo-httpserv@1.1.1
[HIGH] GHSA-p6mc-m468-83gw lodash@4.17.15
The exit code is 2: at least one finding matched --fail-on high. The report lists 31 findings of two kinds. Twenty-five are vulnerability findings that gate. Six are license warnings ([WARNING] UNKNOWN-… rows further down, for packages whose license could not be determined) that inform but never gate. Twenty-five gating findings is too many to fix at once, which is what the next two steps are for. (Auditors, Exit codes)
Step 5: triage by reachability (experimental)
Which of those 25 gating findings live in code this app actually imports?
bomly scan --enrich --audit --analyze --fail-on high --fail-on reachable
Both conditions must hold: a vulnerability finding now gates only when it is high or above and reachable. On this project that cuts the gating findings from 25 to 10 (the report shows 16 rows, because the 6 license warnings are unaffected). For example, minimist@1.2.5, pulled in by the dev-only mocha toolchain, drops out, while minimist@0.0.10 on the runtime path stays.
Reachability is experimental. For these analyzers "unreachable" means "no import path found", not "safe". Read Reachability before you make this your only gate.
Step 6: accept the existing findings
A gate you cannot turn on because of old findings protects nothing. A baseline records today's findings so the gate fails only on new ones:
bomly baseline create --enrich
bomly scan --enrich --audit --fail-on high
echo $? # 0
The findings are still reported, but their policy status becomes suppressed instead of gating. Commit .bomly/baseline.json so CI and teammates share the same accepted set. Use bomly baseline update and prune as you fix findings. This tutorial does not commit the file yet, which matters in the next step. (Finding baselines)
Step 7: review changes, not snapshots
For pull requests, compare two states and audit only what changed. The demo repo has two tags to compare:
bomly diff --url https://github.com/bomly-dev/example-javascript-npm \
--base v0.9.0 --head v1.0.0 --enrich --audit --fail-on high
echo $? # 2
Version changed (1)
~ lodash 4.17.14 → 4.17.15
3 finding(s) persisted.
persisted [HIGH] GHSA-35jh-r3h4-6jhm lodash@4.17.15
persisted [HIGH] GHSA-p6mc-m468-83gw lodash@4.17.15
persisted [HIGH] GHSA-r5fr-rjxr-66jc lodash@4.17.15
✓ 1 fix suggestion for 1 of 1 vulnerable package.
Run again with --format json to see remediation details.
(--fail-on high also filters the findings shown. Drop it to see this bump's three medium-severity persisted advisories as well.)
This output shows the two halves of diff gating:
- The other 22 vulnerable packages from Step 3 are absent. Diff audits only the packages the change touched, so existing findings elsewhere in the repository never block a PR.
- The exit code is still
2. The lodash bump kept three high-severity findings alive: 4.17.15 still ships them, so the gate refuses to call this upgrade done. The way out is the version the fix suggestion names, or an accepted baseline. - The baseline from Step 6 did not apply, because you never committed it. Diff reads each side from Git, so each side uses the baseline committed at that ref. Commit
.bomly/baseline.json, and PRs that leave accepted findings untouched pass.
Step 8: put it in CI
For GitHub pull requests, the Guard action runs the diff from Step 7 against the PR's merge base and posts the result as a check and a PR comment:
# .github/workflows/bomly.yml
on: pull_request
jobs:
bomly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: bomly-dev/bomly-guard@v1
with:
fail-on: high
On any CI system you can call the CLI directly, here with SARIF for the security tab:
- name: Bomly gate
run: bomly diff --base origin/main --head HEAD --enrich --audit --fail-on high -o sarif=bomly.sarif
- name: Upload SARIF
if: success() || failure()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bomly.sarif
CI integration has recipes for GitLab, Jenkins, Azure DevOps, and CircleCI. Bomly Guard has the action's inputs.
What you built
- An inventory you can query with
scanandexplain. - A policy gate on enriched data, with reachability to triage it.
- A baseline that separates old findings from new risk.
- A PR-level diff gate in CI.
From here:
- Use cases — recipes for licenses, typosquats, containers, and SBOMs
- Output formats — JSON, SARIF, and SBOM output from the same runs
- Interactive TUI — explore the same graph with
bomly scan --interactive