lockhawk 0.2.5 โ 0.2.7
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 +77 -64
- package/dist/chunk-IJMHOVBV.js +3 -0
- package/dist/chunk-OIXS6N3H.js +10 -0
- package/dist/db-FFGMJJW7.js +9 -0
- package/dist/index.js +2 -291
- package/dist/report-O5PRSFAL.js +8 -0
- package/dist/report-shell.html +58 -0
- package/dist/scan-X3WQY53T.js +9 -0
- package/dist/serve-WIIUVGR4.js +8 -0
- package/package.json +13 -6
- package/dist/db-3KKJNIY5.js +0 -66
- package/dist/db-3KKJNIY5.js.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
1
|
# ๐ก๏ธ lockhawk
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
**Fast, free, and accurate vulnerability scanning for your npm dependencies โ on your machine and in your pipeline.**
|
|
6
4
|
|
|
7
5
|
[](https://www.npmjs.com/package/lockhawk)
|
|
8
6
|
[](https://github.com/lockhawk/lockhawk/actions/workflows/ci.yml)
|
|
9
7
|
[](https://github.com/lockhawk/lockhawk/blob/main/LICENSE)
|
|
10
8
|
[](https://osv.dev)
|
|
11
9
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
31
|
+
## Quick start
|
|
40
32
|
|
|
41
33
|
```bash
|
|
42
|
-
#
|
|
34
|
+
# Run once, no install needed
|
|
43
35
|
npx lockhawk scan
|
|
44
36
|
|
|
45
|
-
# Fail the build on high
|
|
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
|
|
40
|
+
# Generate a standalone HTML dashboard
|
|
49
41
|
npx lockhawk scan --format html --output report.html
|
|
50
42
|
|
|
51
|
-
# Explore findings in an interactive
|
|
43
|
+
# Explore findings in an interactive dashboard in your browser
|
|
52
44
|
npx lockhawk serve
|
|
53
45
|
```
|
|
54
46
|
|
|
55
|
-
|
|
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
|
|
51
|
+
npm install --save-dev lockhawk # for use in package scripts and CI
|
|
60
52
|
```
|
|
61
53
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
- **โก Fast
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
87
|
-
--offline | --online force the offline
|
|
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
|
|
104
|
-
| `1` | At least one finding at or above `--fail-on
|
|
105
|
-
| `2` | Usage error
|
|
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
|
-
|
|
93
|
+
Network failures never fail the build unless you opt in with `--strict-network`.
|
|
110
94
|
|
|
111
|
-
##
|
|
95
|
+
## Continuous integration
|
|
112
96
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
164
|
+
## Full documentation
|
|
151
165
|
|
|
152
|
-
Full docs, CI
|
|
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
|
-
##
|
|
168
|
+
## License
|
|
156
169
|
|
|
157
|
-
[MIT](https://github.com/lockhawk/lockhawk/blob/main/LICENSE)
|
|
170
|
+
[MIT](https://github.com/lockhawk/lockhawk/blob/main/LICENSE). Free to use, modify, and distribute.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{readFile as n}from"fs/promises";import{fileURLToPath as t}from"url";import{dirname as i,join as o}from"path";var e;async function u(){if(e!==void 0)return e??void 0;try{let r=i(t(import.meta.url));e=await n(o(r,"report-shell.html"),"utf8")}catch{e=null}return e??void 0}export{u as a};
|
|
3
|
+
//# sourceMappingURL=chunk-IJMHOVBV.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as c}from"./chunk-IJMHOVBV.js";import{toHtml as b,toJson as w,toJunit as k,toSarifString as v}from"@lockhawk/core";import t from"chalk";import m from"cli-table3";var i={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 l(n){let e=[],{summary:h,target:r,stats:u,database:p}=n;e.push(t.bold(`
|
|
3
|
+
lockhawk`)+t.dim(` \xB7 ${r.root.name}${r.root.version?`@${r.root.version}`:""} \xB7 ${r.manager} \xB7 ${u.totalPackages} packages`));for(let o of p.warnings)e.push(t.yellow(`\u26A0 ${o}`));if(n.findings.length===0)return e.push(t.green.bold(`
|
|
4
|
+
\u2713 No known vulnerabilities found.
|
|
5
|
+
`)),e.push(t.dim(d(n))),e.join(`
|
|
6
|
+
`);let s=new m({head:["Severity","Package","Advisory","Fixed in","Path"].map(o=>t.dim(o)),style:{head:[],border:[]},colWidths:[12,26,22,12,40],wordWrap:!0});for(let o of n.findings){let a=o.severity,f=`${a.level}${a.score?` ${a.score}`:""}`,g=o.dependencyPaths[0]?.join(" \u203A ")??o.packageName;s.push([i[a.level](f),`${o.packageName}
|
|
7
|
+
${t.dim(o.version)}${o.direct?t.dim(" (direct)"):""}`,o.id,o.fixedVersions[0]??t.dim("none"),t.dim(g)])}return e.push(""),e.push(s.toString()),e.push(""),e.push($(h)),e.push(t.dim(d(n))),e.join(`
|
|
8
|
+
`)}function $(n){let e=[];return n.critical&&e.push(i.critical(`${n.critical} critical`)),n.high&&e.push(i.high(`${n.high} high`)),n.medium&&e.push(i.medium(`${n.medium} medium`)),n.low&&e.push(i.low(`${n.low} low`)),n.unknown&&e.push(i.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 d(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 F(n,e){switch(e){case"json":return w(n);case"sarif":return v(n);case"junit":return k(n);case"html":return b(n,await c());default:return l(n)}}export{F as a};
|
|
10
|
+
//# sourceMappingURL=chunk-OIXS6N3H.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,293 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
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(createDashboardHandler(html, json));
|
|
235
|
-
const port = opts.port ?? 7777;
|
|
236
|
-
server.listen(port, "127.0.0.1", () => {
|
|
237
|
-
const url = `http://localhost:${port}`;
|
|
238
|
-
process.stderr.write(
|
|
239
|
-
`
|
|
240
|
-
lockhawk dashboard running at ${url}
|
|
241
|
-
${summaryLine2(result)}
|
|
242
|
-
Press Ctrl+C to stop.
|
|
243
|
-
`
|
|
244
|
-
);
|
|
245
|
-
if (opts.open !== false) void open(url).catch(() => void 0);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
function createDashboardHandler(html, json) {
|
|
249
|
-
return (req, res) => {
|
|
250
|
-
res.setHeader("cache-control", "no-store, max-age=0");
|
|
251
|
-
if (req.url && req.url.startsWith("/api/result")) {
|
|
252
|
-
res.setHeader("content-type", "application/json");
|
|
253
|
-
res.end(json);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
257
|
-
res.end(html);
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function summaryLine2(result) {
|
|
261
|
-
const s = result.summary;
|
|
262
|
-
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.`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// src/index.ts
|
|
266
|
-
var program = new Command();
|
|
267
|
-
program.name("lockhawk").description(
|
|
268
|
-
"Fast, free, accurate npm dependency vulnerability scanner (local + CI/CD), powered by OSV.dev"
|
|
269
|
-
).version("0.1.0");
|
|
270
|
-
var severityChoices = ["none", "low", "medium", "high", "critical"];
|
|
271
|
-
program.command("scan", { isDefault: true }).description("Scan a project for vulnerable dependencies").argument("[path]", "project directory to scan", ".").addOption(
|
|
272
|
-
new Option("-f, --format <format>", "output format").choices(["table", "json", "sarif", "html", "junit"]).default("table")
|
|
273
|
-
).option("-o, --output <file>", "write the report to a file instead of stdout").addOption(
|
|
274
|
-
new Option("--severity-threshold <level>", "minimum severity to report").choices(
|
|
275
|
-
severityChoices
|
|
276
|
-
)
|
|
277
|
-
).addOption(
|
|
278
|
-
new Option("--fail-on <level>", "minimum severity that causes a non-zero exit").choices(severityChoices).default("high")
|
|
279
|
-
).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));
|
|
280
|
-
var db = program.command("db").description("Manage the offline OSV database");
|
|
281
|
-
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)));
|
|
282
|
-
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)));
|
|
283
|
-
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)));
|
|
284
|
-
program.command("report").description("Re-render a saved JSON result into another format").requiredOption("-i, --input <file>", "path to a JSON scan result").addOption(
|
|
285
|
-
new Option("-f, --format <format>", "output format").choices(["table", "json", "sarif", "html", "junit"]).default("html")
|
|
286
|
-
).option("-o, --output <file>", "write the report to a file instead of stdout").action((opts) => runReport(opts));
|
|
287
|
-
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));
|
|
288
|
-
program.parseAsync().catch((err) => {
|
|
289
|
-
process.stderr.write(`lockhawk: ${err instanceof Error ? err.message : String(err)}
|
|
290
|
-
`);
|
|
291
|
-
process.exitCode = 3;
|
|
292
|
-
});
|
|
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.6");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-X3WQY53T.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-O5PRSFAL.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-WIIUVGR4.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});
|
|
293
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as n}from"./chunk-OIXS6N3H.js";import"./chunk-IJMHOVBV.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-O5PRSFAL.js.map
|