Tutorial
From first scan to a CI gate on a real project, with representative output from the workflow.
Last updated July 30, 2026View source (v0.21.0)
A start-to-finish walkthrough on a real project: scan it, understand a finding, cut the list down to what matters, accept the existing debt, and wire the gate into CI. Every command below was run against the pinned repository shown, and the output you see is what it printed.
Where Getting Started introduces each feature on its own, this page chains them into the workflow you'd actually deploy.
Prerequisites
bomlyon yourPATH(Installation) — verify withbomly versiongit- Network access for the enrichment steps (
--enrichis the explicit opt-in — see Network and Privacy)
We'll use a deliberately vulnerable demo project maintained by the Bomly org, pinned to a tag so the dependency graph you scan matches this page. The outputs below were captured with Bomly 0.20.2 on 2026-07-30; graph numbers are stable, but advisory counts and IDs come from live enrichment data and may drift as services add or withdraw records:
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 fan out into 136 packages — the other 122 are transitive, and they're where most surprises live. 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)
The chain reads bottom-up: removing minimist@0.0.10 means dealing with optimist, which means dealing with the direct dependency to. That's the fact you need before filing any upgrade PR. (explain reference)
Step 3 — Enrich with 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 coordinates — 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: 25 gating vulnerability findings, plus 6 license warnings (the [WARNING] UNKNOWN-… rows — packages whose license couldn't be determined) that inform but never gate. Twenty-five gating findings is too many to fix in one sitting, which is exactly the situation 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
The two --fail-on constraints AND together: a vulnerability finding now gates only when it is high-or-above and reachable. On this project that cuts the gating vulnerability findings from 25 to 10 (the report shows 16 rows — the 6 license warnings are unaffected by reachability). 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, and at package precision "unreachable" means "no import path found", not "safe" — read Reachability before making this your only gate.
Step 6 — Accept the existing debt
A gate you can't turn on because of old findings protects nothing. Baselines record 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 — their policy status becomes suppressed instead of gating. Commit .bomly/baseline.json so CI and teammates share the same accepted set, and use bomly baseline update / prune as debt gets paid down. (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.
(The --fail-on high threshold filters the rendered findings too — drop it to also see this bump's three medium-severity persisted advisories.)
Read this carefully, because it shows both halves of how diff gating works:
- The other 22 vulnerable packages from Step 3 are absent. Diff audits only the packages the change touched — untouched debt elsewhere in the repo never blocks a PR.
- The exit code is still
2. The lodash bump persisted three high-severity findings: 4.17.15 still ships them, so the gate correctly refuses to call this upgrade done. The way out is the bump the fix suggestion points at — or an accepted baseline. - The baseline from Step 6 didn't apply here because diff materializes each side from git: every side uses the baseline committed at that ref. Commit
.bomly/baseline.jsonand PRs that leave accepted findings untouched pass.
Step 8 — Put it in CI
The turnkey form for GitHub pull requests is the Guard action — it runs the diff from Step 7 against the PR's merge base and posts the result as a check and an updatable 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
Prefer the CLI directly (any CI system), 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
Recipes for GitLab, Jenkins, Azure DevOps, and CircleCI live in CI Integration; Guard's full input reference in Bomly Guard.
What you built
An inventory you can query (scan, explain), an enrichment-backed policy gate with a reachability-aware triage path, a committed baseline that separates old debt from new risk, and a PR-level diff gate in CI. From here:
- Use Cases — more goal-shaped recipes (licenses, typosquats, containers, SBOMs)
- Output Formats — JSON, SARIF, and SBOM artifacts from the same runs
- Interactive TUI — explore the same graph with
bomly scan --interactive