lockhawk 0.2.4 β†’ 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,15 @@
1
- <div align="center">
2
-
3
1
  # πŸ›‘οΈ lockhawk
4
2
 
5
- ### Fast, free, and accurate npm dependency vulnerability scanning β€” for your machine **and** your pipeline.
3
+ **Fast, free, and accurate vulnerability scanning for your npm dependencies β€” on your machine and in your pipeline.**
6
4
 
7
5
  [![npm](https://img.shields.io/npm/v/lockhawk.svg)](https://www.npmjs.com/package/lockhawk)
8
6
  [![CI](https://github.com/lockhawk/lockhawk/actions/workflows/ci.yml/badge.svg)](https://github.com/lockhawk/lockhawk/actions/workflows/ci.yml)
9
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/lockhawk/lockhawk/blob/main/LICENSE)
10
8
  [![Data: OSV.dev](https://img.shields.io/badge/data-OSV.dev-4285F4.svg)](https://osv.dev)
11
9
 
12
- </div>
13
-
14
- ---
10
+ `lockhawk` reads your lockfile (`package-lock.json`, `yarn.lock`, or `pnpm-lock.yaml`), builds the full dependency tree including every transitive dependency, and checks each package against the free [OSV.dev](https://osv.dev) vulnerability database. You get a clear terminal report, a polished HTML dashboard, and structured output (JSON, SARIF, JUnit) for CI.
15
11
 
16
- `lockhawk` reads your lockfile (`package-lock.json`, `yarn.lock`, or `pnpm-lock.yaml`), builds the
17
- full dependency tree β€” including transitive dependencies β€” and checks every package against the
18
- free [OSV.dev](https://osv.dev) vulnerability database. It produces a beautiful, self-contained
19
- HTML dashboard and machine-readable reports so you can understand and prioritise fixes.
20
- **No account, no API key, no usage limits.**
12
+ **No account. No API key. No usage limits.**
21
13
 
22
14
  ```console
23
15
  $ npx lockhawk scan
@@ -36,59 +28,51 @@ Found 3 vulnerabilities (1 critical, 1 high, 1 medium) Β· 3 fixable
36
28
  Data from OSV.dev Β· database: offline Β· scanned in 184ms
37
29
  ```
38
30
 
39
- ## πŸš€ Quick start
31
+ ## Quick start
40
32
 
41
33
  ```bash
42
- # One-off, no install
34
+ # Run once, no install needed
43
35
  npx lockhawk scan
44
36
 
45
- # Fail the build on high+ severity and emit SARIF for the GitHub Security tab
37
+ # Fail the build on high or critical findings and write SARIF for the GitHub Security tab
46
38
  npx lockhawk scan --format sarif --output scan.sarif --fail-on high
47
39
 
48
- # Generate the standalone HTML dashboard
40
+ # Generate a standalone HTML dashboard
49
41
  npx lockhawk scan --format html --output report.html
50
42
 
51
- # Explore findings in an interactive local dashboard
43
+ # Explore findings in an interactive dashboard in your browser
52
44
  npx lockhawk serve
53
45
  ```
54
46
 
55
- Install it globally or as a dev dependency:
47
+ Prefer to install it?
56
48
 
57
49
  ```bash
58
50
  npm install -g lockhawk # global `lockhawk` command
59
- npm install --save-dev lockhawk # for use in package scripts / CI
51
+ npm install --save-dev lockhawk # for use in package scripts and CI
60
52
  ```
61
53
 
62
- ## ✨ Features
63
-
64
- - **⚑ Fast & non-intrusive** β€” a warm scan does **zero network I/O** and finishes in well under a
65
- second. The vulnerability database is cached on disk (and downloadable for fully offline CI),
66
- lookups are batched, and the scanner is **fail-open**: a transient network blip never breaks
67
- your build.
68
- - **πŸ†“ Free forever** β€” powered by Google's [OSV.dev](https://osv.dev). No API key, no
69
- rate-limited account, no seat licensing.
70
- - **🎯 Accurate** β€” canonical OSV range matching with `semver`, **CVSS v3 _and_ v4** scoring,
71
- withdrawn-advisory handling, and alias de-duplication.
72
- - **πŸ“¦ Every package manager** β€” npm (`package-lock` v1/v2/v3 + shrinkwrap), Yarn (classic &
73
- Berry), and pnpm (v5/v6/v9), including workspaces.
74
- - **πŸ“Š Beautiful analysis** β€” a self-contained HTML dashboard you can open anywhere or attach as
75
- a CI artifact, plus `lockhawk serve` for interactive drill-down with dependency-path tracing.
76
- - **πŸ”Œ Built for CI/CD** β€” first-class **SARIF** (GitHub Security tab), **JUnit** (Azure DevOps /
77
- GitLab test dashboards), JSON, and a stable exit-code contract for build gating.
78
-
79
- ## 🧭 CLI
54
+ ## Why lockhawk
55
+
56
+ - **⚑ Fast and quiet.** A warm scan does zero network requests and finishes in well under a second. The vulnerability database is cached on disk (and you can download it for fully offline CI), and lookups are batched. The scanner also fails open: a temporary network problem never breaks your build.
57
+ - **πŸ†“ Free forever.** Powered by Google's [OSV.dev](https://osv.dev). There is no API key, no rate-limited account, and no per-seat license.
58
+ - **🎯 Accurate.** Canonical OSV range matching with `semver`, CVSS v3 and v4 scoring, correct handling of withdrawn advisories, and de-duplication of aliased advisories.
59
+ - **πŸ“¦ Every package manager.** npm (`package-lock` v1, v2, v3, and shrinkwrap), Yarn (classic and Berry), and pnpm (v5, v6, v9), including workspaces.
60
+ - **πŸ“Š Clear results.** A standalone HTML dashboard (one file, works offline) that you can open anywhere or attach as a CI artifact, plus `lockhawk serve` to drill into findings interactively.
61
+ - **πŸ”Œ Built for CI.** First-class SARIF (GitHub Security tab), JUnit (Azure DevOps and GitLab test dashboards), JSON, and a stable exit-code contract for gating builds.
62
+
63
+ ## CLI reference
80
64
 
81
65
  ```
82
- lockhawk scan [path] Scan a project (default command)
66
+ lockhawk scan [path] Scan a project (this is the default command)
83
67
  -f, --format <fmt> table | json | sarif | html | junit (default: table)
84
68
  -o, --output <file> write the report to a file
85
- --severity-threshold <level> minimum severity to report
86
- --fail-on <level> minimum severity for a non-zero exit (default: high)
87
- --offline | --online force the offline DB or live OSV.dev queries
88
- --strict-network fail on network errors instead of degrading
69
+ --severity-threshold <level> minimum severity to include in the report
70
+ --fail-on <level> minimum severity that causes a non-zero exit (default: high)
71
+ --offline | --online force the offline database or live OSV.dev queries
72
+ --strict-network fail on network errors instead of degrading gracefully
89
73
  --prod-only ignore dev dependencies
90
74
  --ignore <ids...> suppress specific advisory ids
91
- --ignore-file <path> a .lockhawkignore file (ids, optional expiry date)
75
+ --ignore-file <path> a .lockhawkignore file (ids, with an optional expiry date)
92
76
  --cache-dir <dir> | --cache-ttl <hours> | --no-cache | --concurrency <n>
93
77
 
94
78
  lockhawk report -i result.json -f html -o report.html Re-render a saved result
@@ -98,21 +82,21 @@ lockhawk db update | status | path Manage the offline OSV
98
82
 
99
83
  ### Exit codes
100
84
 
101
- | Code | Meaning |
102
- | ---- | --------------------------------------------- |
103
- | `0` | Clean β€” no finding at or above `--fail-on` |
104
- | `1` | At least one finding at or above `--fail-on` |
105
- | `2` | Usage error (e.g. no lockfile found) |
106
- | `3` | Internal error |
107
- | `4` | Network error while `--strict-network` is set |
85
+ | Code | Meaning |
86
+ | ---- | ---------------------------------------------- |
87
+ | `0` | Clean. No finding at or above `--fail-on`. |
88
+ | `1` | At least one finding at or above `--fail-on`. |
89
+ | `2` | Usage error, for example no lockfile found. |
90
+ | `3` | Internal error. |
91
+ | `4` | Network error while `--strict-network` is set. |
108
92
 
109
- Plain network failures never fail the build unless you opt in with `--strict-network`.
93
+ Network failures never fail the build unless you opt in with `--strict-network`.
110
94
 
111
- ## πŸ” CI/CD
95
+ ## Continuous integration
112
96
 
113
- Runs in any pipeline without slowing it down β€” warm the cached DB once, then scan offline in well
114
- under a second. Full recipes for **GitHub Actions**, **Azure DevOps**, and **GitLab CI** are in
115
- [docs/ci-cd.md](https://github.com/lockhawk/lockhawk/blob/main/docs/ci-cd.md).
97
+ lockhawk runs in any pipeline without slowing it down. Warm the cached database once, then scan offline in well under a second. The snippets below are the short version; full, copy-paste recipes for all three platforms are in [docs/ci-cd.md](https://github.com/lockhawk/lockhawk/blob/main/docs/ci-cd.md).
98
+
99
+ **GitHub Actions** (the bundled action):
116
100
 
117
101
  ```yaml
118
102
  permissions: { contents: read, security-events: write }
@@ -122,9 +106,38 @@ steps:
122
106
  with: { fail-on: high }
123
107
  ```
124
108
 
125
- ## βš™οΈ Configuration
109
+ **Azure DevOps** (findings render natively in the Tests tab via JUnit):
110
+
111
+ ```yaml
112
+ - script: npx lockhawk db update
113
+ displayName: Warm OSV database
114
+ - script: npx lockhawk scan --offline --format junit --output lockhawk.junit.xml --fail-on none
115
+ displayName: Scan dependencies
116
+ - task: PublishTestResults@2
117
+ condition: always()
118
+ inputs:
119
+ testResultsFormat: JUnit
120
+ testResultsFiles: lockhawk.junit.xml
121
+ failTaskOnFailedTests: true # fail the pipeline when there are findings
122
+ ```
123
+
124
+ **GitLab CI** (JUnit surfaces in the pipeline and merge-request test widgets):
126
125
 
127
- Add a `.lockhawkrc`, `lockhawk.config.js`, or a `"lockhawk"` key in `package.json`:
126
+ ```yaml
127
+ dependency_scan:
128
+ image: node:22
129
+ script:
130
+ - npx lockhawk db update
131
+ - npx lockhawk scan --offline --format junit --output scan.junit.xml --fail-on high
132
+ artifacts:
133
+ when: always
134
+ reports:
135
+ junit: scan.junit.xml
136
+ ```
137
+
138
+ ## Configuration
139
+
140
+ Add a `.lockhawkrc`, a `lockhawk.config.js`, or a `"lockhawk"` key in `package.json`:
128
141
 
129
142
  ```json
130
143
  {
@@ -135,10 +148,11 @@ Add a `.lockhawkrc`, `lockhawk.config.js`, or a `"lockhawk"` key in `package.jso
135
148
  }
136
149
  ```
137
150
 
138
- ## 🧩 Programmatic API
151
+ `failOn` is a severity gate: the scan exits non-zero when any finding is at or above that level. With `"failOn": "high"`, a single high or critical finding fails the build, while `"critical"` tolerates highs and `"none"` never fails on findings (report-only). CLI flags always override config-file values.
152
+
153
+ ## Programmatic API
139
154
 
140
- To embed the scanner in your own tooling, use the engine package
141
- [`@lockhawk/core`](https://www.npmjs.com/package/@lockhawk/core):
155
+ To embed the scanner in your own tooling, use the engine package [`@lockhawk/core`](https://www.npmjs.com/package/@lockhawk/core):
142
156
 
143
157
  ```ts
144
158
  import { scan } from '@lockhawk/core';
@@ -147,11 +161,10 @@ const result = await scan({ path: '.', mode: 'auto', failOn: 'high' });
147
161
  console.log(result.summary, result.findings);
148
162
  ```
149
163
 
150
- ## πŸ“š Full documentation
164
+ ## Full documentation
151
165
 
152
- Full docs, CI/CD recipes, configuration reference, and the contributing guide live in the main
153
- repository: **https://github.com/lockhawk/lockhawk**
166
+ Full docs, CI recipes, the configuration reference, and the contributing guide live in the main repository: **https://github.com/lockhawk/lockhawk**
154
167
 
155
- ## πŸ“„ License
168
+ ## License
156
169
 
157
- [MIT](https://github.com/lockhawk/lockhawk/blob/main/LICENSE) β€” free to use, modify, and distribute.
170
+ [MIT](https://github.com/lockhawk/lockhawk/blob/main/LICENSE). Free to use, modify, and distribute.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import{toHtml as $,toJson as b,toJunit as w,toSarifString as k}from"@lockhawk/core";import t from"chalk";import f from"cli-table3";var o={critical:n=>t.bgRed.white.bold(` ${n} `),high:n=>t.hex("#d9822b").bold(n),medium:n=>t.yellow(n),low:n=>t.blue(n),unknown:n=>t.gray(n),none:n=>t.green(n)};function d(n){let e=[],{summary:l,target:r,stats:h,database:u}=n;e.push(t.bold(`
3
+ lockhawk`)+t.dim(` \xB7 ${r.root.name}${r.root.version?`@${r.root.version}`:""} \xB7 ${r.manager} \xB7 ${h.totalPackages} packages`));for(let i of u.warnings)e.push(t.yellow(`\u26A0 ${i}`));if(n.findings.length===0)return e.push(t.green.bold(`
4
+ \u2713 No known vulnerabilities found.
5
+ `)),e.push(t.dim(c(n))),e.join(`
6
+ `);let s=new f({head:["Severity","Package","Advisory","Fixed in","Path"].map(i=>t.dim(i)),style:{head:[],border:[]},colWidths:[12,26,22,12,40],wordWrap:!0});for(let i of n.findings){let a=i.severity,p=`${a.level}${a.score?` ${a.score}`:""}`,g=i.dependencyPaths[0]?.join(" \u203A ")??i.packageName;s.push([o[a.level](p),`${i.packageName}
7
+ ${t.dim(i.version)}${i.direct?t.dim(" (direct)"):""}`,i.id,i.fixedVersions[0]??t.dim("none"),t.dim(g)])}return e.push(""),e.push(s.toString()),e.push(""),e.push(m(l)),e.push(t.dim(c(n))),e.join(`
8
+ `)}function m(n){let e=[];return n.critical&&e.push(o.critical(`${n.critical} critical`)),n.high&&e.push(o.high(`${n.high} high`)),n.medium&&e.push(o.medium(`${n.medium} medium`)),n.low&&e.push(o.low(`${n.low} low`)),n.unknown&&e.push(o.unknown(`${n.unknown} unknown`)),t.bold(`Found ${n.total} ${n.total===1?"vulnerability":"vulnerabilities"}`)+(e.length?` (${e.join(", ")})`:"")+t.dim(` \xB7 ${n.fixable} fixable`)}function c(n){let e=n.database.ageHours!==void 0?`, ${n.database.ageHours}h old`:"";return`
9
+ Data from OSV.dev \xB7 database: ${n.database.source}${e} \xB7 scanned in ${n.stats.durationMs??0}ms`}async function y(n,e){switch(e){case"json":return b(n);case"sarif":return k(n);case"junit":return w(n);case"html":return $(n);default:return d(n)}}export{y as a};
10
+ //# sourceMappingURL=chunk-HC4RCFHY.js.map
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import p from"ora";import{offlineDbDir as d,readOfflineMeta as u,resolveCacheDir as n,updateOfflineDatabase as f}from"@lockhawk/core";async function b(s){let a=n(s.cacheDir),t=!!process.stderr.isTTY&&!process.env.CI?p({stream:process.stderr,text:"Updating database\u2026"}).start():void 0;try{let{status:r,meta:o}=await f({cacheDir:a,nowIso:new Date().toISOString(),force:s.force,onProgress:i=>{t?t.text=i:process.stderr.write(`${i}
3
+ `)}}),c=`${r==="updated"?"Updated":"Already up to date"} \u2014 ${o.recordCount} advisories across ${o.packageCount} packages.`;t?t.succeed(c):process.stderr.write(`${c}
4
+ `)}catch(r){let o=r instanceof Error?r.message:String(r);t?t.fail(`Database update failed: ${o}`):process.stderr.write(`Database update failed: ${o}
5
+ `),process.exitCode=3}}async function $(s){let a=n(s.cacheDir),e=await u(a);if(!e){process.stdout.write("No offline database found. Run `lockhawk db update` to create one.\n");return}let t=Math.round((Date.now()-Date.parse(e.lastUpdated))/36e5);process.stdout.write([`Ecosystem: ${e.ecosystem}`,`Source: ${e.source}`,`Advisories: ${e.recordCount}`,`Packages: ${e.packageCount}`,`Last updated: ${e.lastUpdated} (${t}h ago)`,`Location: ${d(a)}`].join(`
6
+ `)+`
7
+ `)}function m(s){process.stdout.write(`${d(n(s.cacheDir))}
8
+ `)}export{m as runDbPath,$ as runDbStatus,b as runDbUpdate};
9
+ //# sourceMappingURL=db-FFGMJJW7.js.map
package/dist/index.js CHANGED
@@ -1,289 +1,4 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/index.ts
4
- import { Command, Option } from "commander";
5
-
6
- // src/commands/scan.ts
7
- import { writeFileSync } from "fs";
8
- import { join, resolve } from "path";
9
- import ora from "ora";
10
- import { LockfileError, scan, shouldFail } from "@lockhawk/core";
11
-
12
- // src/report/render.ts
13
- import { toHtml, toJson, toJunit, toSarifString } from "@lockhawk/core";
14
-
15
- // src/report/table.ts
16
- import chalk from "chalk";
17
- import Table from "cli-table3";
18
- var paint = {
19
- critical: (s) => chalk.bgRed.white.bold(` ${s} `),
20
- high: (s) => chalk.hex("#d9822b").bold(s),
21
- medium: (s) => chalk.yellow(s),
22
- low: (s) => chalk.blue(s),
23
- unknown: (s) => chalk.gray(s),
24
- none: (s) => chalk.green(s)
25
- };
26
- function renderTable(result) {
27
- const lines = [];
28
- const { summary, target, stats, database } = result;
29
- lines.push(
30
- chalk.bold(`
31
- lockhawk`) + chalk.dim(
32
- ` \xB7 ${target.root.name}${target.root.version ? `@${target.root.version}` : ""} \xB7 ${target.manager} \xB7 ${stats.totalPackages} packages`
33
- )
34
- );
35
- for (const warning of database.warnings) lines.push(chalk.yellow(`\u26A0 ${warning}`));
36
- if (result.findings.length === 0) {
37
- lines.push(chalk.green.bold("\n\u2713 No known vulnerabilities found.\n"));
38
- lines.push(chalk.dim(footer(result)));
39
- return lines.join("\n");
40
- }
41
- const table = new Table({
42
- head: ["Severity", "Package", "Advisory", "Fixed in", "Path"].map((h) => chalk.dim(h)),
43
- style: { head: [], border: [] },
44
- colWidths: [12, 26, 22, 12, 40],
45
- wordWrap: true
46
- });
47
- for (const f of result.findings) {
48
- const sev = f.severity;
49
- const sevText = `${sev.level}${sev.score ? ` ${sev.score}` : ""}`;
50
- const path = f.dependencyPaths[0]?.join(" \u203A ") ?? f.packageName;
51
- table.push([
52
- paint[sev.level](sevText),
53
- `${f.packageName}
54
- ${chalk.dim(f.version)}${f.direct ? chalk.dim(" (direct)") : ""}`,
55
- f.id,
56
- f.fixedVersions[0] ?? chalk.dim("none"),
57
- chalk.dim(path)
58
- ]);
59
- }
60
- lines.push("");
61
- lines.push(table.toString());
62
- lines.push("");
63
- lines.push(summaryLine(summary));
64
- lines.push(chalk.dim(footer(result)));
65
- return lines.join("\n");
66
- }
67
- function summaryLine(summary) {
68
- const parts = [];
69
- if (summary.critical) parts.push(paint.critical(`${summary.critical} critical`));
70
- if (summary.high) parts.push(paint.high(`${summary.high} high`));
71
- if (summary.medium) parts.push(paint.medium(`${summary.medium} medium`));
72
- if (summary.low) parts.push(paint.low(`${summary.low} low`));
73
- if (summary.unknown) parts.push(paint.unknown(`${summary.unknown} unknown`));
74
- return chalk.bold(
75
- `Found ${summary.total} ${summary.total === 1 ? "vulnerability" : "vulnerabilities"}`
76
- ) + (parts.length ? ` (${parts.join(", ")})` : "") + chalk.dim(` \xB7 ${summary.fixable} fixable`);
77
- }
78
- function footer(result) {
79
- const age = result.database.ageHours !== void 0 ? `, ${result.database.ageHours}h old` : "";
80
- return `
81
- Data from OSV.dev \xB7 database: ${result.database.source}${age} \xB7 scanned in ${result.stats.durationMs ?? 0}ms`;
82
- }
83
-
84
- // src/report/render.ts
85
- async function renderResult(result, format) {
86
- switch (format) {
87
- case "json":
88
- return toJson(result);
89
- case "sarif":
90
- return toSarifString(result);
91
- case "junit":
92
- return toJunit(result);
93
- case "html":
94
- return toHtml(result);
95
- default:
96
- return renderTable(result);
97
- }
98
- }
99
-
100
- // src/config.ts
101
- import { existsSync, readFileSync } from "fs";
102
- import { cosmiconfigSync } from "cosmiconfig";
103
- function loadFileConfig(cwd) {
104
- try {
105
- const result = cosmiconfigSync("lockhawk").search(cwd);
106
- return result?.config ?? {};
107
- } catch {
108
- return {};
109
- }
110
- }
111
- function readIgnoreFile(path, now = /* @__PURE__ */ new Date()) {
112
- if (!existsSync(path)) return [];
113
- const ids = [];
114
- for (const raw of readFileSync(path, "utf8").split(/\r?\n/)) {
115
- const line = raw.replace(/#.*$/, "").trim();
116
- if (!line) continue;
117
- const [id, expiry] = line.split(/\s+/);
118
- if (!id) continue;
119
- if (expiry) {
120
- const when = Date.parse(expiry);
121
- if (!Number.isNaN(when) && when < now.getTime()) continue;
122
- }
123
- ids.push(id);
124
- }
125
- return ids;
126
- }
127
-
128
- // src/commands/scan.ts
129
- var EXIT = { OK: 0, FINDINGS: 1, USAGE: 2, INTERNAL: 3, NETWORK: 4 };
130
- async function runScan(pathArg, cli) {
131
- const dir = resolve(pathArg || ".");
132
- const fileConfig = loadFileConfig(dir);
133
- const mode = cli.offline ? "offline" : cli.online ? "online" : fileConfig.mode ?? "auto";
134
- const format = cli.format ?? "table";
135
- const failOn = cli.failOn ?? fileConfig.failOn ?? "high";
136
- const ignore = [
137
- ...cli.ignore ?? [],
138
- ...fileConfig.ignore ?? [],
139
- ...readIgnoreFile(cli.ignoreFile ?? join(dir, ".lockhawkignore"))
140
- ];
141
- const useSpinner = format === "table" && Boolean(process.stderr.isTTY) && !process.env.CI;
142
- const spinner = useSpinner ? ora({ stream: process.stderr, text: "Scanning\u2026" }).start() : void 0;
143
- const options = {
144
- path: dir,
145
- mode,
146
- cacheDir: cli.cacheDir,
147
- cacheTtlHours: cli.cacheTtl,
148
- noCache: cli.cache === false,
149
- concurrency: cli.concurrency,
150
- prodOnly: cli.prodOnly ?? fileConfig.prodOnly,
151
- ignore,
152
- strictNetwork: cli.strictNetwork,
153
- severityThreshold: cli.severityThreshold ?? fileConfig.severityThreshold,
154
- onProgress: (message) => {
155
- if (spinner) spinner.text = message;
156
- }
157
- };
158
- let result;
159
- try {
160
- result = await scan(options);
161
- } catch (err) {
162
- spinner?.stop();
163
- const message = err instanceof Error ? err.message : String(err);
164
- process.stderr.write(`lockhawk: ${message}
165
- `);
166
- if (err instanceof LockfileError) process.exitCode = EXIT.USAGE;
167
- else process.exitCode = cli.strictNetwork ? EXIT.NETWORK : EXIT.INTERNAL;
168
- return;
169
- }
170
- spinner?.stop();
171
- const rendered = await renderResult(result, format);
172
- if (cli.output) {
173
- writeFileSync(cli.output, rendered.endsWith("\n") ? rendered : `${rendered}
174
- `);
175
- process.stderr.write(`Report written to ${cli.output}
176
- ${briefSummary(result)}
177
- `);
178
- } else {
179
- process.stdout.write(`${rendered}
180
- `);
181
- }
182
- process.exitCode = shouldFail(result.summary, failOn) ? EXIT.FINDINGS : EXIT.OK;
183
- }
184
- function briefSummary(result) {
185
- const s = result.summary;
186
- if (s.total === 0) return "\u2713 No known vulnerabilities found.";
187
- const parts = ["critical", "high", "medium", "low", "unknown"].filter((level) => s[level] > 0).map((level) => `${s[level]} ${level}`);
188
- return `Found ${s.total} ${s.total === 1 ? "vulnerability" : "vulnerabilities"} (${parts.join(", ")}).`;
189
- }
190
-
191
- // src/commands/report.ts
192
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
193
- async function runReport(opts) {
194
- let result;
195
- try {
196
- result = JSON.parse(readFileSync2(opts.input, "utf8"));
197
- } catch (err) {
198
- process.stderr.write(
199
- `lockhawk: could not read ${opts.input}: ${err instanceof Error ? err.message : err}
200
- `
201
- );
202
- process.exitCode = 2;
203
- return;
204
- }
205
- const rendered = await renderResult(result, opts.format ?? "html");
206
- if (opts.output) {
207
- writeFileSync2(opts.output, rendered.endsWith("\n") ? rendered : `${rendered}
208
- `);
209
- process.stderr.write(`Report written to ${opts.output}
210
- `);
211
- } else {
212
- process.stdout.write(`${rendered}
213
- `);
214
- }
215
- }
216
-
217
- // src/commands/serve.ts
218
- import { createServer } from "http";
219
- import { readFileSync as readFileSync3 } from "fs";
220
- import { resolve as resolve2 } from "path";
221
- import open from "open";
222
- import { scan as scan2, toHtml as toHtml2, toJson as toJson2 } from "@lockhawk/core";
223
- async function runServe(pathArg, opts) {
224
- let result;
225
- if (opts.input) {
226
- result = JSON.parse(readFileSync3(opts.input, "utf8"));
227
- } else {
228
- const mode = opts.offline ? "offline" : opts.online ? "online" : "auto";
229
- process.stderr.write("Scanning\u2026\n");
230
- result = await scan2({ path: resolve2(pathArg || "."), mode });
231
- }
232
- const html = toHtml2(result);
233
- const json = toJson2(result);
234
- const server = createServer((req, res) => {
235
- if (req.url && req.url.startsWith("/api/result")) {
236
- res.setHeader("content-type", "application/json");
237
- res.end(json);
238
- return;
239
- }
240
- res.setHeader("content-type", "text/html; charset=utf-8");
241
- res.end(html);
242
- });
243
- const port = opts.port ?? 7777;
244
- server.listen(port, "127.0.0.1", () => {
245
- const url = `http://localhost:${port}`;
246
- process.stderr.write(
247
- `
248
- lockhawk dashboard running at ${url}
249
- ${summaryLine2(result)}
250
- Press Ctrl+C to stop.
251
- `
252
- );
253
- if (opts.open !== false) void open(url).catch(() => void 0);
254
- });
255
- }
256
- function summaryLine2(result) {
257
- const s = result.summary;
258
- return s.total === 0 ? "\u2713 No known vulnerabilities found." : `${s.total} findings \u2014 ${s.critical} critical, ${s.high} high, ${s.medium} medium, ${s.low} low.`;
259
- }
260
-
261
- // src/index.ts
262
- var program = new Command();
263
- program.name("lockhawk").description(
264
- "Fast, free, accurate npm dependency vulnerability scanner (local + CI/CD), powered by OSV.dev"
265
- ).version("0.1.0");
266
- var severityChoices = ["none", "low", "medium", "high", "critical"];
267
- program.command("scan", { isDefault: true }).description("Scan a project for vulnerable dependencies").argument("[path]", "project directory to scan", ".").addOption(
268
- new Option("-f, --format <format>", "output format").choices(["table", "json", "sarif", "html", "junit"]).default("table")
269
- ).option("-o, --output <file>", "write the report to a file instead of stdout").addOption(
270
- new Option("--severity-threshold <level>", "minimum severity to report").choices(
271
- severityChoices
272
- )
273
- ).addOption(
274
- new Option("--fail-on <level>", "minimum severity that causes a non-zero exit").choices(severityChoices).default("high")
275
- ).option("--offline", "use only the local offline database (no network)").option("--online", "force live OSV.dev queries").option("--strict-network", "fail the scan on network errors instead of degrading gracefully").option("--prod-only", "ignore dev dependencies").option("--ignore <ids...>", "advisory ids to suppress").option("--ignore-file <path>", "path to a .lockhawkignore file").option("--cache-dir <dir>", "override the cache directory").option("--cache-ttl <hours>", "cache freshness window in hours", (v) => Number(v)).option("--no-cache", "bypass the on-disk cache").option("--concurrency <n>", "max concurrent OSV requests", (v) => Number(v)).action((path, opts) => runScan(path, opts));
276
- var db = program.command("db").description("Manage the offline OSV database");
277
- db.command("update").description("Download or refresh the offline npm advisory database").option("--cache-dir <dir>", "override the cache directory").option("--force", "ignore conditional caching and re-download").action((opts) => import("./db-3KKJNIY5.js").then((m) => m.runDbUpdate(opts)));
278
- db.command("status").description("Show offline database freshness").option("--cache-dir <dir>", "override the cache directory").action((opts) => import("./db-3KKJNIY5.js").then((m) => m.runDbStatus(opts)));
279
- db.command("path").description("Print the offline database directory").option("--cache-dir <dir>", "override the cache directory").action((opts) => import("./db-3KKJNIY5.js").then((m) => m.runDbPath(opts)));
280
- program.command("report").description("Re-render a saved JSON result into another format").requiredOption("-i, --input <file>", "path to a JSON scan result").addOption(
281
- new Option("-f, --format <format>", "output format").choices(["table", "json", "sarif", "html", "junit"]).default("html")
282
- ).option("-o, --output <file>", "write the report to a file instead of stdout").action((opts) => runReport(opts));
283
- program.command("serve").description("Run a scan and open an interactive dashboard locally").argument("[path]", "project directory to scan", ".").option("-i, --input <file>", "serve a saved JSON result instead of scanning").option("-p, --port <port>", "port to listen on", (v) => Number(v), 7777).option("--no-open", "do not open the browser automatically").option("--offline", "use only the local offline database").option("--online", "force live OSV.dev queries").action((path, opts) => runServe(path, opts));
284
- program.parseAsync().catch((err) => {
285
- process.stderr.write(`lockhawk: ${err instanceof Error ? err.message : String(err)}
286
- `);
287
- process.exitCode = 3;
288
- });
2
+ import{Command as c,Option as n}from"commander";var t=new c;t.name("lockhawk").description("Fast, free, accurate npm dependency vulnerability scanner (local + CI/CD), powered by OSV.dev").version("0.2.5");var a=["none","low","medium","high","critical"];t.command("scan",{isDefault:!0}).description("Scan a project for vulnerable dependencies").argument("[path]","project directory to scan",".").addOption(new n("-f, --format <format>","output format").choices(["table","json","sarif","html","junit"]).default("table")).option("-o, --output <file>","write the report to a file instead of stdout").addOption(new n("--severity-threshold <level>","minimum severity to report").choices(a)).addOption(new n("--fail-on <level>","minimum severity that causes a non-zero exit").choices(a).default("high")).option("--offline","use only the local offline database (no network)").option("--online","force live OSV.dev queries").option("--strict-network","fail the scan on network errors instead of degrading gracefully").option("--prod-only","ignore dev dependencies").option("--ignore <ids...>","advisory ids to suppress").option("--ignore-file <path>","path to a .lockhawkignore file").option("--cache-dir <dir>","override the cache directory").option("--cache-ttl <hours>","cache freshness window in hours",e=>Number(e)).option("--no-cache","bypass the on-disk cache").option("--concurrency <n>","max concurrent OSV requests",e=>Number(e)).action((e,o)=>import("./scan-ESCA2VN5.js").then(i=>i.runScan(e,o)));var r=t.command("db").description("Manage the offline OSV database");r.command("update").description("Download or refresh the offline npm advisory database").option("--cache-dir <dir>","override the cache directory").option("--force","ignore conditional caching and re-download").action(e=>import("./db-FFGMJJW7.js").then(o=>o.runDbUpdate(e)));r.command("status").description("Show offline database freshness").option("--cache-dir <dir>","override the cache directory").action(e=>import("./db-FFGMJJW7.js").then(o=>o.runDbStatus(e)));r.command("path").description("Print the offline database directory").option("--cache-dir <dir>","override the cache directory").action(e=>import("./db-FFGMJJW7.js").then(o=>o.runDbPath(e)));t.command("report").description("Re-render a saved JSON result into another format").requiredOption("-i, --input <file>","path to a JSON scan result").addOption(new n("-f, --format <format>","output format").choices(["table","json","sarif","html","junit"]).default("html")).option("-o, --output <file>","write the report to a file instead of stdout").action(e=>import("./report-I7PES65V.js").then(o=>o.runReport(e)));t.command("serve").description("Run a scan and open an interactive dashboard locally").argument("[path]","project directory to scan",".").option("-i, --input <file>","serve a saved JSON result instead of scanning").option("-p, --port <port>","port to listen on",e=>Number(e),7777).option("--no-open","do not open the browser automatically").option("--offline","use only the local offline database").option("--online","force live OSV.dev queries").action((e,o)=>import("./serve-3DOIOCFP.js").then(i=>i.runServe(e,o)));t.parseAsync().catch(e=>{process.stderr.write(`lockhawk: ${e instanceof Error?e.message:String(e)}
3
+ `),process.exitCode=3});
289
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import{a as n}from"./chunk-HC4RCFHY.js";import{readFileSync as i,writeFileSync as s}from"fs";async function a(r){let o;try{o=JSON.parse(i(r.input,"utf8"))}catch(e){process.stderr.write(`lockhawk: could not read ${r.input}: ${e instanceof Error?e.message:e}
3
+ `),process.exitCode=2;return}let t=await n(o,r.format??"html");r.output?(s(r.output,t.endsWith(`
4
+ `)?t:`${t}
5
+ `),process.stderr.write(`Report written to ${r.output}
6
+ `)):process.stdout.write(`${t}
7
+ `)}export{a as runReport};
8
+ //# sourceMappingURL=report-I7PES65V.js.map
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import{a as m}from"./chunk-HC4RCFHY.js";import{writeFileSync as N}from"fs";import{join as O,resolve as b}from"path";import k from"ora";import{LockfileError as v,scan as T,shouldFail as x}from"@lockhawk/core";import{existsSync as S,readFileSync as w}from"fs";import{cosmiconfigSync as F}from"cosmiconfig";function d(o){try{return F("lockhawk").search(o)?.config??{}}catch{return{}}}function g(o,e=new Date){if(!S(o))return[];let t=[];for(let r of w(o,"utf8").split(/\r?\n/)){let c=r.replace(/#.*$/,"").trim();if(!c)continue;let[i,a]=c.split(/\s+/);if(i){if(a){let l=Date.parse(a);if(!Number.isNaN(l)&&l<e.getTime())continue}t.push(i)}}return t}var s={OK:0,FINDINGS:1,USAGE:2,INTERNAL:3,NETWORK:4};async function L(o,e){let t=b(o||"."),r=d(t),c=e.offline?"offline":e.online?"online":r.mode??"auto",i=e.format??"table",a=e.failOn??r.failOn??"high",l=[...e.ignore??[],...r.ignore??[],...g(e.ignoreFile??O(t,".lockhawkignore"))],u=i==="table"&&!!process.stderr.isTTY&&!process.env.CI?k({stream:process.stderr,text:"Scanning\u2026"}).start():void 0,h={path:t,mode:c,cacheDir:e.cacheDir,cacheTtlHours:e.cacheTtl,noCache:e.cache===!1,concurrency:e.concurrency,prodOnly:e.prodOnly??r.prodOnly,ignore:l,strictNetwork:e.strictNetwork,severityThreshold:e.severityThreshold??r.severityThreshold,onProgress:n=>{u&&(u.text=n)}},f;try{f=await T(h)}catch(n){u?.stop();let y=n instanceof Error?n.message:String(n);process.stderr.write(`lockhawk: ${y}
3
+ `),n instanceof v?process.exitCode=s.USAGE:process.exitCode=e.strictNetwork?s.NETWORK:s.INTERNAL;return}u?.stop();let p=await m(f,i);e.output?(N(e.output,p.endsWith(`
4
+ `)?p:`${p}
5
+ `),process.stderr.write(`Report written to ${e.output}
6
+ ${C(f)}
7
+ `)):process.stdout.write(`${p}
8
+ `),process.exitCode=x(f.summary,a)?s.FINDINGS:s.OK}function C(o){let e=o.summary;if(e.total===0)return"\u2713 No known vulnerabilities found.";let t=["critical","high","medium","low","unknown"].filter(r=>e[r]>0).map(r=>`${e[r]} ${r}`);return`Found ${e.total} ${e.total===1?"vulnerability":"vulnerabilities"} (${t.join(", ")}).`}export{s as EXIT,L as runScan};
9
+ //# sourceMappingURL=scan-ESCA2VN5.js.map
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import{createServer as c}from"http";import{readFileSync as l}from"fs";import{resolve as p}from"path";import u from"open";import{scan as d,toHtml as m,toJson as f}from"@lockhawk/core";async function H(t,e){let o;if(e.input)o=JSON.parse(l(e.input,"utf8"));else{let r=e.offline?"offline":e.online?"online":"auto";process.stderr.write(`Scanning\u2026
3
+ `),o=await d({path:p(t||"."),mode:r})}let n=m(o),i=f(o),a=c(h(n,i)),s=e.port??7777;a.listen(s,"127.0.0.1",()=>{let r=`http://localhost:${s}`;process.stderr.write(`
4
+ lockhawk dashboard running at ${r}
5
+ ${S(o)}
6
+ Press Ctrl+C to stop.
7
+ `),e.open!==!1&&u(r).catch(()=>{})})}function h(t,e){return(o,n)=>{if(n.setHeader("cache-control","no-store, max-age=0"),n.setHeader("x-content-type-options","nosniff"),!g(o.headers?.host)){n.statusCode=403,n.end("Forbidden");return}if(((o.url??"").split("?")[0]??"")==="/api/result"){n.setHeader("content-type","application/json"),n.end(e);return}n.setHeader("content-type","text/html; charset=utf-8"),n.end(t)}}function g(t){if(!t)return!1;let e=t.replace(/:\d+$/,"").toLowerCase();return e==="localhost"||e==="127.0.0.1"||e==="[::1]"||e==="::1"}function S(t){let e=t.summary;return e.total===0?"\u2713 No known vulnerabilities found.":`${e.total} findings \u2014 ${e.critical} critical, ${e.high} high, ${e.medium} medium, ${e.low} low.`}export{h as createDashboardHandler,g as isLoopbackHost,H as runServe};
8
+ //# sourceMappingURL=serve-3DOIOCFP.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lockhawk",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Fast, free, accurate npm dependency vulnerability scanner for local + CI/CD, powered by OSV.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "lockhawk": "./dist/index.js"
19
19
  },
20
20
  "files": [
21
- "dist"
21
+ "dist/**/*.js"
22
22
  ],
23
23
  "keywords": [
24
24
  "security",
@@ -57,7 +57,7 @@
57
57
  "cosmiconfig": "^9.0.0",
58
58
  "open": "^10.1.0",
59
59
  "ora": "^8.1.1",
60
- "@lockhawk/core": "0.2.4"
60
+ "@lockhawk/core": "0.2.6"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^22.10.2",
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/commands/db.ts
4
- import ora from "ora";
5
- import {
6
- offlineDbDir,
7
- readOfflineMeta,
8
- resolveCacheDir,
9
- updateOfflineDatabase
10
- } from "@lockhawk/core";
11
- async function runDbUpdate(opts) {
12
- const cacheDir = resolveCacheDir(opts.cacheDir);
13
- const useSpinner = Boolean(process.stderr.isTTY) && !process.env.CI;
14
- const spinner = useSpinner ? ora({ stream: process.stderr, text: "Updating database\u2026" }).start() : void 0;
15
- try {
16
- const { status, meta } = await updateOfflineDatabase({
17
- cacheDir,
18
- nowIso: (/* @__PURE__ */ new Date()).toISOString(),
19
- force: opts.force,
20
- onProgress: (message) => {
21
- if (spinner) spinner.text = message;
22
- else process.stderr.write(`${message}
23
- `);
24
- }
25
- });
26
- const summary = `${status === "updated" ? "Updated" : "Already up to date"} \u2014 ${meta.recordCount} advisories across ${meta.packageCount} packages.`;
27
- if (spinner) spinner.succeed(summary);
28
- else process.stderr.write(`${summary}
29
- `);
30
- } catch (err) {
31
- const message = err instanceof Error ? err.message : String(err);
32
- if (spinner) spinner.fail(`Database update failed: ${message}`);
33
- else process.stderr.write(`Database update failed: ${message}
34
- `);
35
- process.exitCode = 3;
36
- }
37
- }
38
- async function runDbStatus(opts) {
39
- const cacheDir = resolveCacheDir(opts.cacheDir);
40
- const meta = await readOfflineMeta(cacheDir);
41
- if (!meta) {
42
- process.stdout.write("No offline database found. Run `lockhawk db update` to create one.\n");
43
- return;
44
- }
45
- const ageHours = Math.round((Date.now() - Date.parse(meta.lastUpdated)) / 36e5);
46
- process.stdout.write(
47
- [
48
- `Ecosystem: ${meta.ecosystem}`,
49
- `Source: ${meta.source}`,
50
- `Advisories: ${meta.recordCount}`,
51
- `Packages: ${meta.packageCount}`,
52
- `Last updated: ${meta.lastUpdated} (${ageHours}h ago)`,
53
- `Location: ${offlineDbDir(cacheDir)}`
54
- ].join("\n") + "\n"
55
- );
56
- }
57
- function runDbPath(opts) {
58
- process.stdout.write(`${offlineDbDir(resolveCacheDir(opts.cacheDir))}
59
- `);
60
- }
61
- export {
62
- runDbPath,
63
- runDbStatus,
64
- runDbUpdate
65
- };
66
- //# sourceMappingURL=db-3KKJNIY5.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/db.ts"],"sourcesContent":["import ora from 'ora';\nimport {\n offlineDbDir,\n readOfflineMeta,\n resolveCacheDir,\n updateOfflineDatabase,\n} from '@lockhawk/core';\n\ninterface DbOptions {\n cacheDir?: string;\n force?: boolean;\n}\n\n/** `db update` β€” download/refresh the offline OSV database. */\nexport async function runDbUpdate(opts: DbOptions): Promise<void> {\n const cacheDir = resolveCacheDir(opts.cacheDir);\n const useSpinner = Boolean(process.stderr.isTTY) && !process.env.CI;\n const spinner = useSpinner\n ? ora({ stream: process.stderr, text: 'Updating database…' }).start()\n : undefined;\n try {\n const { status, meta } = await updateOfflineDatabase({\n cacheDir,\n nowIso: new Date().toISOString(),\n force: opts.force,\n onProgress: (message) => {\n if (spinner) spinner.text = message;\n else process.stderr.write(`${message}\\n`);\n },\n });\n const summary = `${status === 'updated' ? 'Updated' : 'Already up to date'} β€” ${meta.recordCount} advisories across ${meta.packageCount} packages.`;\n if (spinner) spinner.succeed(summary);\n else process.stderr.write(`${summary}\\n`);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (spinner) spinner.fail(`Database update failed: ${message}`);\n else process.stderr.write(`Database update failed: ${message}\\n`);\n process.exitCode = 3;\n }\n}\n\n/** `db status` β€” report freshness of the offline database. */\nexport async function runDbStatus(opts: DbOptions): Promise<void> {\n const cacheDir = resolveCacheDir(opts.cacheDir);\n const meta = await readOfflineMeta(cacheDir);\n if (!meta) {\n process.stdout.write('No offline database found. Run `lockhawk db update` to create one.\\n');\n return;\n }\n const ageHours = Math.round((Date.now() - Date.parse(meta.lastUpdated)) / 3_600_000);\n process.stdout.write(\n [\n `Ecosystem: ${meta.ecosystem}`,\n `Source: ${meta.source}`,\n `Advisories: ${meta.recordCount}`,\n `Packages: ${meta.packageCount}`,\n `Last updated: ${meta.lastUpdated} (${ageHours}h ago)`,\n `Location: ${offlineDbDir(cacheDir)}`,\n ].join('\\n') + '\\n',\n );\n}\n\n/** `db path` β€” print the offline database directory. */\nexport function runDbPath(opts: DbOptions): void {\n process.stdout.write(`${offlineDbDir(resolveCacheDir(opts.cacheDir))}\\n`);\n}\n"],"mappings":";;;AAAA,OAAO,SAAS;AAChB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,eAAsB,YAAY,MAAgC;AAChE,QAAM,WAAW,gBAAgB,KAAK,QAAQ;AAC9C,QAAM,aAAa,QAAQ,QAAQ,OAAO,KAAK,KAAK,CAAC,QAAQ,IAAI;AACjE,QAAM,UAAU,aACZ,IAAI,EAAE,QAAQ,QAAQ,QAAQ,MAAM,0BAAqB,CAAC,EAAE,MAAM,IAClE;AACJ,MAAI;AACF,UAAM,EAAE,QAAQ,KAAK,IAAI,MAAM,sBAAsB;AAAA,MACnD;AAAA,MACA,SAAQ,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC/B,OAAO,KAAK;AAAA,MACZ,YAAY,CAAC,YAAY;AACvB,YAAI,QAAS,SAAQ,OAAO;AAAA,YACvB,SAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AAAA,MAC1C;AAAA,IACF,CAAC;AACD,UAAM,UAAU,GAAG,WAAW,YAAY,YAAY,oBAAoB,WAAM,KAAK,WAAW,sBAAsB,KAAK,YAAY;AACvI,QAAI,QAAS,SAAQ,QAAQ,OAAO;AAAA,QAC/B,SAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AAAA,EAC1C,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,QAAI,QAAS,SAAQ,KAAK,2BAA2B,OAAO,EAAE;AAAA,QACzD,SAAQ,OAAO,MAAM,2BAA2B,OAAO;AAAA,CAAI;AAChE,YAAQ,WAAW;AAAA,EACrB;AACF;AAGA,eAAsB,YAAY,MAAgC;AAChE,QAAM,WAAW,gBAAgB,KAAK,QAAQ;AAC9C,QAAM,OAAO,MAAM,gBAAgB,QAAQ;AAC3C,MAAI,CAAC,MAAM;AACT,YAAQ,OAAO,MAAM,sEAAsE;AAC3F;AAAA,EACF;AACA,QAAM,WAAW,KAAK,OAAO,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,WAAW,KAAK,IAAS;AACnF,UAAQ,OAAO;AAAA,IACb;AAAA,MACE,iBAAiB,KAAK,SAAS;AAAA,MAC/B,iBAAiB,KAAK,MAAM;AAAA,MAC5B,iBAAiB,KAAK,WAAW;AAAA,MACjC,iBAAiB,KAAK,YAAY;AAAA,MAClC,iBAAiB,KAAK,WAAW,KAAK,QAAQ;AAAA,MAC9C,iBAAiB,aAAa,QAAQ,CAAC;AAAA,IACzC,EAAE,KAAK,IAAI,IAAI;AAAA,EACjB;AACF;AAGO,SAAS,UAAU,MAAuB;AAC/C,UAAQ,OAAO,MAAM,GAAG,aAAa,gBAAgB,KAAK,QAAQ,CAAC,CAAC;AAAA,CAAI;AAC1E;","names":[]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/scan.ts","../src/report/render.ts","../src/report/table.ts","../src/config.ts","../src/commands/report.ts","../src/commands/serve.ts"],"sourcesContent":["import { Command, Option } from 'commander';\nimport { runScan } from './commands/scan.js';\nimport type { ScanCliOptions } from './commands/scan.js';\nimport type { Format } from './report/render.js';\nimport { runReport } from './commands/report.js';\nimport { runServe } from './commands/serve.js';\n\nconst program = new Command();\n\nprogram\n .name('lockhawk')\n .description(\n 'Fast, free, accurate npm dependency vulnerability scanner (local + CI/CD), powered by OSV.dev',\n )\n .version('0.1.0');\n\nconst severityChoices = ['none', 'low', 'medium', 'high', 'critical'];\n\nprogram\n .command('scan', { isDefault: true })\n .description('Scan a project for vulnerable dependencies')\n .argument('[path]', 'project directory to scan', '.')\n .addOption(\n new Option('-f, --format <format>', 'output format')\n .choices(['table', 'json', 'sarif', 'html', 'junit'])\n .default('table'),\n )\n .option('-o, --output <file>', 'write the report to a file instead of stdout')\n .addOption(\n new Option('--severity-threshold <level>', 'minimum severity to report').choices(\n severityChoices,\n ),\n )\n .addOption(\n new Option('--fail-on <level>', 'minimum severity that causes a non-zero exit')\n .choices(severityChoices)\n .default('high'),\n )\n .option('--offline', 'use only the local offline database (no network)')\n .option('--online', 'force live OSV.dev queries')\n .option('--strict-network', 'fail the scan on network errors instead of degrading gracefully')\n .option('--prod-only', 'ignore dev dependencies')\n .option('--ignore <ids...>', 'advisory ids to suppress')\n .option('--ignore-file <path>', 'path to a .lockhawkignore file')\n .option('--cache-dir <dir>', 'override the cache directory')\n .option('--cache-ttl <hours>', 'cache freshness window in hours', (v) => Number(v))\n .option('--no-cache', 'bypass the on-disk cache')\n .option('--concurrency <n>', 'max concurrent OSV requests', (v) => Number(v))\n .action((path: string, opts: ScanCliOptions & { format: Format }) => runScan(path, opts));\n\nconst db = program.command('db').description('Manage the offline OSV database');\ndb.command('update')\n .description('Download or refresh the offline npm advisory database')\n .option('--cache-dir <dir>', 'override the cache directory')\n .option('--force', 'ignore conditional caching and re-download')\n .action((opts) => import('./commands/db.js').then((m) => m.runDbUpdate(opts)));\ndb.command('status')\n .description('Show offline database freshness')\n .option('--cache-dir <dir>', 'override the cache directory')\n .action((opts) => import('./commands/db.js').then((m) => m.runDbStatus(opts)));\ndb.command('path')\n .description('Print the offline database directory')\n .option('--cache-dir <dir>', 'override the cache directory')\n .action((opts) => import('./commands/db.js').then((m) => m.runDbPath(opts)));\n\nprogram\n .command('report')\n .description('Re-render a saved JSON result into another format')\n .requiredOption('-i, --input <file>', 'path to a JSON scan result')\n .addOption(\n new Option('-f, --format <format>', 'output format')\n .choices(['table', 'json', 'sarif', 'html', 'junit'])\n .default('html'),\n )\n .option('-o, --output <file>', 'write the report to a file instead of stdout')\n .action((opts) => runReport(opts));\n\nprogram\n .command('serve')\n .description('Run a scan and open an interactive dashboard locally')\n .argument('[path]', 'project directory to scan', '.')\n .option('-i, --input <file>', 'serve a saved JSON result instead of scanning')\n .option('-p, --port <port>', 'port to listen on', (v) => Number(v), 7777)\n .option('--no-open', 'do not open the browser automatically')\n .option('--offline', 'use only the local offline database')\n .option('--online', 'force live OSV.dev queries')\n .action((path: string, opts) => runServe(path, opts));\n\nprogram.parseAsync().catch((err) => {\n process.stderr.write(`lockhawk: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = 3;\n});\n","import { writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport ora from 'ora';\nimport { LockfileError, scan, shouldFail } from '@lockhawk/core';\nimport type { ScanOptions, ScanResult, Severity, SourceMode } from '@lockhawk/core';\nimport { renderResult } from '../report/render.js';\nimport type { Format } from '../report/render.js';\nimport { loadFileConfig, readIgnoreFile } from '../config.js';\n\nexport interface ScanCliOptions {\n format?: Format;\n output?: string;\n severityThreshold?: Severity;\n failOn?: Severity;\n offline?: boolean;\n online?: boolean;\n strictNetwork?: boolean;\n prodOnly?: boolean;\n ignore?: string[];\n ignoreFile?: string;\n cacheDir?: string;\n cacheTtl?: number;\n cache?: boolean; // commander sets false for --no-cache\n concurrency?: number;\n}\n\n/** Stable exit-code contract (documented for CI). */\nexport const EXIT = { OK: 0, FINDINGS: 1, USAGE: 2, INTERNAL: 3, NETWORK: 4 } as const;\n\nexport async function runScan(pathArg: string, cli: ScanCliOptions): Promise<void> {\n const dir = resolve(pathArg || '.');\n const fileConfig = loadFileConfig(dir);\n\n const mode: SourceMode = cli.offline\n ? 'offline'\n : cli.online\n ? 'online'\n : (fileConfig.mode ?? 'auto');\n const format: Format = cli.format ?? 'table';\n const failOn: Severity = cli.failOn ?? fileConfig.failOn ?? 'high';\n\n const ignore = [\n ...(cli.ignore ?? []),\n ...(fileConfig.ignore ?? []),\n ...readIgnoreFile(cli.ignoreFile ?? join(dir, '.lockhawkignore')),\n ];\n\n const useSpinner = format === 'table' && Boolean(process.stderr.isTTY) && !process.env.CI;\n const spinner = useSpinner\n ? ora({ stream: process.stderr, text: 'Scanning…' }).start()\n : undefined;\n\n const options: ScanOptions = {\n path: dir,\n mode,\n cacheDir: cli.cacheDir,\n cacheTtlHours: cli.cacheTtl,\n noCache: cli.cache === false,\n concurrency: cli.concurrency,\n prodOnly: cli.prodOnly ?? fileConfig.prodOnly,\n ignore,\n strictNetwork: cli.strictNetwork,\n severityThreshold: cli.severityThreshold ?? fileConfig.severityThreshold,\n onProgress: (message) => {\n if (spinner) spinner.text = message;\n },\n };\n\n let result: ScanResult;\n try {\n result = await scan(options);\n } catch (err) {\n spinner?.stop();\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`lockhawk: ${message}\\n`);\n if (err instanceof LockfileError) process.exitCode = EXIT.USAGE;\n else process.exitCode = cli.strictNetwork ? EXIT.NETWORK : EXIT.INTERNAL;\n return;\n }\n spinner?.stop();\n\n const rendered = await renderResult(result, format);\n if (cli.output) {\n writeFileSync(cli.output, rendered.endsWith('\\n') ? rendered : `${rendered}\\n`);\n process.stderr.write(`Report written to ${cli.output}\\n${briefSummary(result)}\\n`);\n } else {\n process.stdout.write(`${rendered}\\n`);\n }\n\n process.exitCode = shouldFail(result.summary, failOn) ? EXIT.FINDINGS : EXIT.OK;\n}\n\nfunction briefSummary(result: ScanResult): string {\n const s = result.summary;\n if (s.total === 0) return 'βœ“ No known vulnerabilities found.';\n const parts = (['critical', 'high', 'medium', 'low', 'unknown'] as const)\n .filter((level) => s[level] > 0)\n .map((level) => `${s[level]} ${level}`);\n return `Found ${s.total} ${s.total === 1 ? 'vulnerability' : 'vulnerabilities'} (${parts.join(', ')}).`;\n}\n","import { toHtml, toJson, toJunit, toSarifString } from '@lockhawk/core';\nimport type { ScanResult } from '@lockhawk/core';\nimport { renderTable } from './table.js';\n\nexport type Format = 'table' | 'json' | 'sarif' | 'html' | 'junit';\n\n/** Render a scan result into the requested output format. */\nexport async function renderResult(result: ScanResult, format: Format): Promise<string> {\n switch (format) {\n case 'json':\n return toJson(result);\n case 'sarif':\n return toSarifString(result);\n case 'junit':\n return toJunit(result);\n case 'html':\n return toHtml(result);\n default:\n return renderTable(result);\n }\n}\n","import chalk from 'chalk';\nimport Table from 'cli-table3';\nimport type { ScanResult, Severity } from '@lockhawk/core';\n\nconst paint: Record<Severity, (s: string) => string> = {\n critical: (s) => chalk.bgRed.white.bold(` ${s} `),\n high: (s) => chalk.hex('#d9822b').bold(s),\n medium: (s) => chalk.yellow(s),\n low: (s) => chalk.blue(s),\n unknown: (s) => chalk.gray(s),\n none: (s) => chalk.green(s),\n};\n\n/** Render a scan result as a colorized terminal report. */\nexport function renderTable(result: ScanResult): string {\n const lines: string[] = [];\n const { summary, target, stats, database } = result;\n\n lines.push(\n chalk.bold(`\\nlockhawk`) +\n chalk.dim(\n ` Β· ${target.root.name}${target.root.version ? `@${target.root.version}` : ''} Β· ${target.manager} Β· ${stats.totalPackages} packages`,\n ),\n );\n\n for (const warning of database.warnings) lines.push(chalk.yellow(`⚠ ${warning}`));\n\n if (result.findings.length === 0) {\n lines.push(chalk.green.bold('\\nβœ“ No known vulnerabilities found.\\n'));\n lines.push(chalk.dim(footer(result)));\n return lines.join('\\n');\n }\n\n const table = new Table({\n head: ['Severity', 'Package', 'Advisory', 'Fixed in', 'Path'].map((h) => chalk.dim(h)),\n style: { head: [], border: [] },\n colWidths: [12, 26, 22, 12, 40],\n wordWrap: true,\n });\n\n for (const f of result.findings) {\n const sev = f.severity;\n const sevText = `${sev.level}${sev.score ? ` ${sev.score}` : ''}`;\n const path = f.dependencyPaths[0]?.join(' β€Ί ') ?? f.packageName;\n table.push([\n paint[sev.level](sevText),\n `${f.packageName}\\n${chalk.dim(f.version)}${f.direct ? chalk.dim(' (direct)') : ''}`,\n f.id,\n f.fixedVersions[0] ?? chalk.dim('none'),\n chalk.dim(path),\n ]);\n }\n\n lines.push('');\n lines.push(table.toString());\n lines.push('');\n lines.push(summaryLine(summary));\n lines.push(chalk.dim(footer(result)));\n return lines.join('\\n');\n}\n\nfunction summaryLine(summary: ScanResult['summary']): string {\n const parts: string[] = [];\n if (summary.critical) parts.push(paint.critical(`${summary.critical} critical`));\n if (summary.high) parts.push(paint.high(`${summary.high} high`));\n if (summary.medium) parts.push(paint.medium(`${summary.medium} medium`));\n if (summary.low) parts.push(paint.low(`${summary.low} low`));\n if (summary.unknown) parts.push(paint.unknown(`${summary.unknown} unknown`));\n return (\n chalk.bold(\n `Found ${summary.total} ${summary.total === 1 ? 'vulnerability' : 'vulnerabilities'}`,\n ) +\n (parts.length ? ` (${parts.join(', ')})` : '') +\n chalk.dim(` Β· ${summary.fixable} fixable`)\n );\n}\n\nfunction footer(result: ScanResult): string {\n const age = result.database.ageHours !== undefined ? `, ${result.database.ageHours}h old` : '';\n return `\\nData from OSV.dev Β· database: ${result.database.source}${age} Β· scanned in ${result.stats.durationMs ?? 0}ms`;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { cosmiconfigSync } from 'cosmiconfig';\nimport type { Severity, SourceMode } from '@lockhawk/core';\n\n/** Configuration resolvable from a config file (`.lockhawkrc`, `package.json#lockhawk`, …). */\nexport interface FileConfig {\n mode?: SourceMode;\n failOn?: Severity;\n severityThreshold?: Severity;\n prodOnly?: boolean;\n ignore?: string[];\n concurrency?: number;\n cacheTtlHours?: number;\n}\n\n/** Load a config file by searching upward from `cwd`. Returns `{}` if none found. */\nexport function loadFileConfig(cwd: string): FileConfig {\n try {\n const result = cosmiconfigSync('lockhawk').search(cwd);\n return (result?.config as FileConfig) ?? {};\n } catch {\n return {};\n }\n}\n\n/**\n * Parse a `.lockhawkignore` file. Each non-comment line is an advisory id with an\n * optional expiry date (`GHSA-xxxx 2026-12-31`); expired entries are dropped so\n * suppressions don't silently outlive their review.\n */\nexport function readIgnoreFile(path: string, now: Date = new Date()): string[] {\n if (!existsSync(path)) return [];\n const ids: string[] = [];\n for (const raw of readFileSync(path, 'utf8').split(/\\r?\\n/)) {\n const line = raw.replace(/#.*$/, '').trim();\n if (!line) continue;\n const [id, expiry] = line.split(/\\s+/);\n if (!id) continue;\n if (expiry) {\n const when = Date.parse(expiry);\n if (!Number.isNaN(when) && when < now.getTime()) continue; // expired β†’ no longer ignored\n }\n ids.push(id);\n }\n return ids;\n}\n","import { readFileSync, writeFileSync } from 'node:fs';\nimport type { ScanResult } from '@lockhawk/core';\nimport { renderResult } from '../report/render.js';\nimport type { Format } from '../report/render.js';\n\ninterface ReportOptions {\n input: string;\n format?: Format;\n output?: string;\n}\n\n/** `report` β€” re-render a previously saved JSON result into another format. */\nexport async function runReport(opts: ReportOptions): Promise<void> {\n let result: ScanResult;\n try {\n result = JSON.parse(readFileSync(opts.input, 'utf8')) as ScanResult;\n } catch (err) {\n process.stderr.write(\n `lockhawk: could not read ${opts.input}: ${err instanceof Error ? err.message : err}\\n`,\n );\n process.exitCode = 2;\n return;\n }\n\n const rendered = await renderResult(result, opts.format ?? 'html');\n if (opts.output) {\n writeFileSync(opts.output, rendered.endsWith('\\n') ? rendered : `${rendered}\\n`);\n process.stderr.write(`Report written to ${opts.output}\\n`);\n } else {\n process.stdout.write(`${rendered}\\n`);\n }\n}\n","import { createServer } from 'node:http';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport open from 'open';\nimport { scan, toHtml, toJson } from '@lockhawk/core';\nimport type { ScanResult, SourceMode } from '@lockhawk/core';\n\ninterface ServeOptions {\n input?: string;\n port?: number;\n open?: boolean; // commander sets false for --no-open\n offline?: boolean;\n online?: boolean;\n}\n\n/** `serve` β€” run a scan (or load a saved result) and serve the dashboard locally. */\nexport async function runServe(pathArg: string, opts: ServeOptions): Promise<void> {\n let result: ScanResult;\n if (opts.input) {\n result = JSON.parse(readFileSync(opts.input, 'utf8')) as ScanResult;\n } else {\n const mode: SourceMode = opts.offline ? 'offline' : opts.online ? 'online' : 'auto';\n process.stderr.write('Scanning…\\n');\n result = await scan({ path: resolve(pathArg || '.'), mode });\n }\n\n const html = toHtml(result);\n const json = toJson(result);\n\n const server = createServer((req, res) => {\n if (req.url && req.url.startsWith('/api/result')) {\n res.setHeader('content-type', 'application/json');\n res.end(json);\n return;\n }\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(html);\n });\n\n const port = opts.port ?? 7777;\n // Bind to loopback only β€” the dashboard embeds the full dependency tree and\n // scan result, which should not be exposed to the local network.\n server.listen(port, '127.0.0.1', () => {\n const url = `http://localhost:${port}`;\n process.stderr.write(\n `\\nlockhawk dashboard running at ${url}\\n${summaryLine(result)}\\nPress Ctrl+C to stop.\\n`,\n );\n if (opts.open !== false) void open(url).catch(() => undefined);\n });\n}\n\nfunction summaryLine(result: ScanResult): string {\n const s = result.summary;\n return s.total === 0\n ? 'βœ“ No known vulnerabilities found.'\n : `${s.total} findings β€” ${s.critical} critical, ${s.high} high, ${s.medium} medium, ${s.low} low.`;\n}\n"],"mappings":"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","names":["readFileSync","writeFileSync","readFileSync","writeFileSync","readFileSync","resolve","scan","toHtml","toJson","summaryLine"]}