npmguard-cli 0.5.5 → 1.0.0

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,155 +1,9 @@
1
- # npmguard-cli
1
+ # NpmGuard CLI
2
2
 
3
- CLI that checks npm packages against on-chain audit records before installing. Published on npm as [`npmguard-cli`](https://www.npmjs.com/package/npmguard-cli).
3
+ > **TODO** CLI will talk to the NpmGuard API server (not ENS/IPFS). Not yet implemented.
4
4
 
5
- ## `npmguard-cli check`
5
+ Planned features:
6
6
 
7
- Scans your `package.json`, queries the npm registry for available updates, then resolves each package version against ENS on Sepolia to find audit records.
8
-
9
- ```bash
10
- npx npmguard-cli check --path /your/project
11
- ```
12
-
13
- ```mermaid
14
- flowchart LR
15
- A[package.json] -->|read deps| B[npm registry]
16
- B -->|latest version| C{ENS Sepolia}
17
- C -->|resolve 1-7-9.axios.npmguard.eth| D[Text Records]
18
- D -->|verdict, score, capabilities, CIDs| E[Terminal output]
19
- ```
20
-
21
- What happens:
22
-
23
- 1. Reads `package.json` and extracts all dependencies
24
- 2. For each dependency, fetches `https://registry.npmjs.org/{package}/latest` to get the latest version
25
- 3. Constructs the ENS name: `{version}.{package}.npmguard.eth` (e.g. `1-7-9.axios.npmguard.eth`)
26
- 4. Resolves these ENS text records on Sepolia via a public RPC:
27
- - `npmguard.verdict` — SAFE / CRITICAL
28
- - `npmguard.score` — 0 to 100
29
- - `npmguard.capabilities` — network, filesystem, process_spawn, etc.
30
- - `npmguard.report_cid` — IPFS CID of the full audit report
31
- - `npmguard.source_cid` — IPFS CID of the audited source code
32
- 5. Displays a table with verdict and capabilities per package
33
- 6. Lists clickable IPFS links to the full audit reports
34
-
35
- Example output:
36
-
37
- ```
38
- NpmGuard Dependency Audit
39
-
40
- ┌─────────┬───────────┬────────┬──────────────┬──────────────┐
41
- │ Package │ Installed │ Latest │ Verdict │ Capabilities │
42
- ├─────────┼───────────┼────────┼──────────────┼──────────────┤
43
- │ axios │ 1.6.0 │ 1.7.9 │ SAFE (96) │ network │
44
- ├─────────┼───────────┼────────┼──────────────┼──────────────┤
45
- │ lodash │ 4.17.21 │ 4.18.1 │ CRITICAL (12)│ network, ... │
46
- ├─────────┼───────────┼────────┼──────────────┼──────────────┤
47
- │ chalk │ 5.6.2 │ 5.6.2 │ NOT AUDITED │ - │
48
- └─────────┴───────────┴────────┴──────────────┴──────────────┘
49
-
50
- 1 safe | 1 critical | 1 not audited
51
-
52
- axios@1.7.9 report: https://gateway.pinata.cloud/ipfs/bafkrei...
53
- lodash@4.18.1 report: https://gateway.pinata.cloud/ipfs/bafkrei...
54
- ```
55
-
56
- ## `npmguard-cli install`
57
-
58
- Installs a package from IPFS instead of the npm registry when an audit record exists on ENS.
59
-
60
- ```bash
61
- npx npmguard-cli install axios
62
- npx npmguard-cli install axios@1.7.9
63
- ```
64
-
65
- ```mermaid
66
- flowchart TD
67
- A[npmguard-cli install axios] --> B{ENS Sepolia}
68
- B -->|resolve 1-7-9.axios.npmguard.eth| C[Get verdict + sourceCid]
69
- C --> D{Verdict?}
70
- D -->|SAFE / WARNING| E[npm install from IPFS gateway]
71
- D -->|CRITICAL| F[Installation blocked]
72
- F -->|--force| E
73
- E -->|tarball from gateway.pinata.cloud/ipfs/sourceCid| G[node_modules/]
74
- ```
75
-
76
- What happens:
77
-
78
- 1. If no version specified, fetches the latest version from npm registry
79
- 2. Resolves `{version}.{package}.npmguard.eth` on Sepolia ENS
80
- 3. Reads the `npmguard.source_cid` text record — the IPFS CID of the audited source tarball
81
- 4. Runs `npm install https://gateway.pinata.cloud/ipfs/{sourceCid}`
82
- 5. npm downloads the tarball directly from IPFS and installs it
83
-
84
- The result in `package-lock.json`:
85
-
86
- ```json
87
- "node_modules/axios": {
88
- "version": "1.7.9",
89
- "resolved": "https://gateway.pinata.cloud/ipfs/bafkrei...",
90
- "integrity": "sha512-..."
91
- }
92
- ```
93
-
94
- The `resolved` field points to IPFS, not npm. Anyone running `npm install` on this project gets the exact same IPFS-verified code.
95
-
96
- If the package is flagged **CRITICAL**, installation is blocked:
97
-
98
- ```
99
- lodash@4.18.1
100
-
101
- CRITICAL (score: 12)
102
- Capabilities: network, filesystem, process_spawn
103
- Report: https://gateway.pinata.cloud/ipfs/bafkrei...
104
-
105
- Installation blocked. This package has critical security issues.
106
- Use --force to install anyway.
107
- ```
108
-
109
- If no audit record exists on ENS, falls back to standard `npm install`.
110
-
111
- ## ENS structure
112
-
113
- ```mermaid
114
- graph TD
115
- A[npmguard.eth] --> B[axios.npmguard.eth]
116
- A --> C[lodash.npmguard.eth]
117
- B --> D[1-7-9.axios.npmguard.eth]
118
- B --> E[1-8-0.axios.npmguard.eth]
119
- D --> F["npmguard.verdict = safe<br/>npmguard.score = 96<br/>npmguard.capabilities = network<br/>npmguard.report_cid = bafkrei...<br/>npmguard.source_cid = bafkrei..."]
120
- E --> G["npmguard.verdict = critical<br/>npmguard.score = 66"]
121
- ```
122
-
123
- ## Project structure
124
-
125
- ```
126
- cli/
127
- ├── src/
128
- │ ├── index.ts # Entry point — commander setup
129
- │ ├── audit-source.ts # AuditSource interface
130
- │ ├── ens-source.ts # Reads ENS text records on Sepolia via viem
131
- │ ├── mock-source.ts # Mock data for testing without ENS
132
- │ ├── scanner.ts # Reads package.json + fetches npm for updates
133
- │ └── commands/
134
- │ ├── check.ts # npmguard-cli check
135
- │ └── install.ts # npmguard-cli install
136
- ├── reports/ # Sample audit reports (uploaded to IPFS)
137
- ├── package.json
138
- └── tsconfig.json
139
- ```
140
-
141
- ## Development
142
-
143
- ```bash
144
- cd cli && npm install
145
-
146
- # Run locally
147
- npx tsx src/index.ts check --path /tmp/test-project
148
- npx tsx src/index.ts install axios@1.7.9
149
-
150
- # Build
151
- npm run build
152
-
153
- # Publish
154
- npm publish
155
- ```
7
+ - `npmguard check <package>` run a security audit via the API
8
+ - `npmguard install <package>` — audit-then-install workflow
9
+ - Machine-readable JSON output for CI/CD integration
package/dist/api.js ADDED
@@ -0,0 +1,66 @@
1
+ async function request(url, options) {
2
+ const res = await fetch(url, options);
3
+ if (!res.ok) {
4
+ const body = await res.text().catch(() => "");
5
+ throw new Error(`HTTP ${res.status}: ${body || res.statusText}`);
6
+ }
7
+ return res.json();
8
+ }
9
+ export async function checkout(apiUrl, packageName, version) {
10
+ const body = { packageName };
11
+ if (version)
12
+ body.version = version;
13
+ return request(`${apiUrl}/checkout`, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify(body),
17
+ });
18
+ }
19
+ export async function pollCheckoutStatus(apiUrl, sessionId) {
20
+ return request(`${apiUrl}/checkout/${encodeURIComponent(sessionId)}/status`);
21
+ }
22
+ export async function startAudit(apiUrl, stripeSessionId) {
23
+ return request(`${apiUrl}/audit/stream`, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json" },
26
+ body: JSON.stringify({ stripeSessionId }),
27
+ });
28
+ }
29
+ export async function startAuditFree(apiUrl, packageName, version) {
30
+ return request(`${apiUrl}/audit/stream`, {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ packageName, ...(version && { version }) }),
34
+ });
35
+ }
36
+ export async function checkoutRaw(apiUrl, packageName, version) {
37
+ const body = { packageName };
38
+ if (version)
39
+ body.version = version;
40
+ const res = await fetch(`${apiUrl}/checkout`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify(body),
44
+ });
45
+ if (res.status === 501) {
46
+ return { status: 501, data: null };
47
+ }
48
+ if (!res.ok) {
49
+ const text = await res.text().catch(() => "");
50
+ throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
51
+ }
52
+ return { status: res.status, data: (await res.json()) };
53
+ }
54
+ export async function getPackageReport(apiUrl, packageName, version) {
55
+ const query = version ? `?version=${encodeURIComponent(version)}` : "";
56
+ const url = `${apiUrl}/package/${encodeURIComponent(packageName)}/report${query}`;
57
+ try {
58
+ return await request(url);
59
+ }
60
+ catch (err) {
61
+ if (err instanceof Error && err.message.startsWith("HTTP 404")) {
62
+ return null;
63
+ }
64
+ throw err;
65
+ }
66
+ }
@@ -0,0 +1,195 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import qrcode from "qrcode-terminal";
4
+ import EventSource from "eventsource";
5
+ import * as api from "../api.js";
6
+ import { renderVerdict, renderFinding, renderPhase } from "../render.js";
7
+ function parsePackageArg(pkg) {
8
+ // Handle scoped packages: @scope/name@version
9
+ if (pkg.startsWith("@")) {
10
+ const slashIndex = pkg.indexOf("/");
11
+ if (slashIndex === -1) {
12
+ throw new Error(`Invalid scoped package name: ${pkg}`);
13
+ }
14
+ const rest = pkg.slice(slashIndex + 1);
15
+ const atIndex = rest.lastIndexOf("@");
16
+ if (atIndex > 0) {
17
+ return {
18
+ name: pkg.slice(0, slashIndex + 1 + atIndex),
19
+ version: rest.slice(atIndex + 1),
20
+ };
21
+ }
22
+ return { name: pkg };
23
+ }
24
+ // Handle unscoped packages: name@version
25
+ const atIndex = pkg.lastIndexOf("@");
26
+ if (atIndex > 0) {
27
+ return {
28
+ name: pkg.slice(0, atIndex),
29
+ version: pkg.slice(atIndex + 1),
30
+ };
31
+ }
32
+ return { name: pkg };
33
+ }
34
+ export async function auditCommand(pkg, opts) {
35
+ const apiUrl = opts.api;
36
+ // 1. Parse package name and version
37
+ let parsed;
38
+ try {
39
+ parsed = parsePackageArg(pkg);
40
+ }
41
+ catch {
42
+ console.error(chalk.red(`Invalid package: ${pkg}`));
43
+ process.exit(1);
44
+ }
45
+ console.log(chalk.bold(`Auditing ${parsed.name}`) +
46
+ (parsed.version ? chalk.gray(`@${parsed.version}`) : ""));
47
+ console.log();
48
+ // 1b. Check if already audited
49
+ const existing = await api.getPackageReport(apiUrl, parsed.name, parsed.version);
50
+ if (existing) {
51
+ console.log(chalk.yellow("This package has already been audited."));
52
+ console.log();
53
+ const verdict = existing.report?.verdict ?? existing.verdict ?? "UNKNOWN";
54
+ renderVerdict(verdict, existing.report?.capabilities ?? [], existing.report?.proofs?.length ?? 0);
55
+ console.log();
56
+ console.log(chalk.dim(`View full report: ${apiUrl}/package/${encodeURIComponent(parsed.name)}/report`));
57
+ process.exit(verdict === "SAFE" ? 0 : 1);
58
+ }
59
+ // 2. Try checkout — if payments not configured (501), go straight to free audit
60
+ const spinner = ora("Connecting...").start();
61
+ let auditId;
62
+ let checkoutResult;
63
+ try {
64
+ checkoutResult = await api.checkoutRaw(apiUrl, parsed.name, parsed.version);
65
+ }
66
+ catch (err) {
67
+ spinner.fail("Failed to create checkout session: " +
68
+ (err instanceof Error ? err.message : String(err)));
69
+ process.exit(1);
70
+ }
71
+ if (checkoutResult.status === 501 || !checkoutResult.data) {
72
+ // Payments not configured — start audit directly (dev/free mode)
73
+ spinner.text = "Starting audit (no payment required)...";
74
+ try {
75
+ const auditRes = await api.startAuditFree(apiUrl, parsed.name, parsed.version);
76
+ auditId = auditRes.auditId;
77
+ }
78
+ catch (err) {
79
+ spinner.fail("Failed to start audit: " +
80
+ (err instanceof Error ? err.message : String(err)));
81
+ process.exit(1);
82
+ }
83
+ }
84
+ else {
85
+ // Payment required — show URL + QR + poll
86
+ spinner.stop();
87
+ const link = chalk.blue.underline(checkoutResult.data.url);
88
+ console.log(chalk.bold("Pay to start the audit:"));
89
+ console.log(link);
90
+ console.log();
91
+ qrcode.generate(checkoutResult.data.url, { small: true });
92
+ console.log();
93
+ spinner.start("Waiting for payment...");
94
+ let status;
95
+ try {
96
+ status = await new Promise((resolve, reject) => {
97
+ const interval = setInterval(async () => {
98
+ try {
99
+ const s = await api.pollCheckoutStatus(apiUrl, checkoutResult.data.sessionId);
100
+ if (s.paid) {
101
+ clearInterval(interval);
102
+ resolve(s);
103
+ }
104
+ }
105
+ catch (err) {
106
+ clearInterval(interval);
107
+ reject(err);
108
+ }
109
+ }, 3000);
110
+ });
111
+ }
112
+ catch (err) {
113
+ spinner.fail("Payment polling failed: " +
114
+ (err instanceof Error ? err.message : String(err)));
115
+ process.exit(1);
116
+ }
117
+ spinner.text = "Payment confirmed. Starting audit...";
118
+ if (status.auditId) {
119
+ auditId = status.auditId;
120
+ }
121
+ else {
122
+ try {
123
+ const auditRes = await api.startAudit(apiUrl, checkoutResult.data.sessionId);
124
+ auditId = auditRes.auditId;
125
+ }
126
+ catch (err) {
127
+ spinner.fail("Failed to start audit: " +
128
+ (err instanceof Error ? err.message : String(err)));
129
+ process.exit(1);
130
+ }
131
+ }
132
+ }
133
+ spinner.text = "Audit in progress...";
134
+ // 9. Connect to SSE
135
+ const eventsUrl = `${apiUrl}/audit/${encodeURIComponent(auditId)}/events`;
136
+ const es = new EventSource(eventsUrl);
137
+ let exitCode = 0;
138
+ await new Promise((resolve) => {
139
+ es.addEventListener("phase_started", (event) => {
140
+ try {
141
+ const data = JSON.parse(event.data);
142
+ spinner.text = renderPhase(data.phase ?? data.name ?? "");
143
+ }
144
+ catch {
145
+ // ignore parse errors
146
+ }
147
+ });
148
+ es.addEventListener("finding_discovered", (event) => {
149
+ try {
150
+ const data = JSON.parse(event.data);
151
+ const finding = data.finding ?? data;
152
+ spinner.stop();
153
+ renderFinding(finding);
154
+ spinner.start();
155
+ }
156
+ catch {
157
+ // ignore parse errors
158
+ }
159
+ });
160
+ es.addEventListener("verdict_reached", (event) => {
161
+ try {
162
+ const data = JSON.parse(event.data);
163
+ spinner.stop();
164
+ renderVerdict(data.verdict ?? "UNKNOWN", data.capabilities ?? [], data.proofCount ?? data.findings?.length ?? 0);
165
+ exitCode = data.verdict?.toUpperCase() === "SAFE" ? 0 : 1;
166
+ }
167
+ catch {
168
+ spinner.stop();
169
+ }
170
+ es.close();
171
+ resolve();
172
+ });
173
+ es.addEventListener("audit_error", (event) => {
174
+ try {
175
+ const data = JSON.parse(event.data);
176
+ spinner.fail(chalk.red("Audit error: " + (data.error ?? event.data)));
177
+ }
178
+ catch {
179
+ spinner.fail(chalk.red("Audit error: " + event.data));
180
+ }
181
+ exitCode = 1;
182
+ es.close();
183
+ resolve();
184
+ });
185
+ es.onerror = () => {
186
+ // EventSource will auto-reconnect; if it closes, we resolve
187
+ if (es.readyState === EventSource.CLOSED) {
188
+ spinner.stop();
189
+ es.close();
190
+ resolve();
191
+ }
192
+ };
193
+ });
194
+ process.exit(exitCode);
195
+ }
@@ -1,130 +1,81 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
1
3
  import chalk from "chalk";
2
- import Table from "cli-table3";
3
- import ora from "ora";
4
- import { scanProject } from "../scanner.js";
5
- const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs";
6
- function verdictLabel(verdict, score) {
7
- switch (verdict) {
8
- case "SAFE":
9
- return chalk.green(`SAFE (${score})`);
10
- case "WARNING":
11
- return chalk.yellow(`WARNING (${score})`);
12
- case "CRITICAL":
13
- return chalk.red(`CRITICAL (${score})`);
14
- case "DANGEROUS":
15
- return chalk.red.bold(`DANGEROUS (${score})`);
16
- default:
17
- return chalk.gray("UNKNOWN");
4
+ import * as api from "../api.js";
5
+ export async function checkCommand(opts) {
6
+ const apiUrl = opts.api;
7
+ const pkgPath = resolve(opts.path, "package.json");
8
+ // 1. Read package.json
9
+ let pkgJson;
10
+ try {
11
+ const raw = await readFile(pkgPath, "utf-8");
12
+ pkgJson = JSON.parse(raw);
18
13
  }
19
- }
20
- function capsLabel(caps) {
21
- if (caps.length === 0)
22
- return chalk.gray("none");
23
- return caps
24
- .map((c) => {
25
- if (["process_spawn", "binary_download", "eval_usage", "obfuscated_code"].includes(c)) {
26
- return chalk.red(c);
27
- }
28
- if (["filesystem"].includes(c)) {
29
- return chalk.yellow(c);
14
+ catch (err) {
15
+ console.error(chalk.red(`Failed to read ${pkgPath}: `) +
16
+ (err instanceof Error ? err.message : String(err)));
17
+ process.exit(1);
18
+ }
19
+ // 2. Collect dependency names
20
+ const deps = new Set([
21
+ ...Object.keys(pkgJson.dependencies ?? {}),
22
+ ...Object.keys(pkgJson.devDependencies ?? {}),
23
+ ]);
24
+ if (deps.size === 0) {
25
+ console.log(chalk.yellow("No dependencies found in package.json."));
26
+ return;
27
+ }
28
+ console.log(chalk.bold(`Checking ${deps.size} dependencies...\n`));
29
+ // 3. Check each dependency
30
+ const results = [];
31
+ for (const name of deps) {
32
+ const versionSpec = pkgJson.dependencies?.[name] ?? pkgJson.devDependencies?.[name] ?? "";
33
+ try {
34
+ const report = await api.getPackageReport(apiUrl, name);
35
+ results.push({
36
+ name,
37
+ version: report?.version ?? versionSpec,
38
+ verdict: report?.verdict ?? null,
39
+ });
30
40
  }
31
- return chalk.gray(c);
32
- })
33
- .join(", ");
34
- }
35
- function shortCid(cid) {
36
- return cid.slice(0, 8) + "..." + cid.slice(-4);
37
- }
38
- export async function checkCommand(projectPath, auditSource) {
39
- const spinner = ora("Scanning project dependencies...").start();
40
- const deps = await scanProject(projectPath);
41
- spinner.text = `Found ${deps.length} dependencies. Checking audits...`;
42
- const table = new Table({
43
- head: [
44
- chalk.bold("Package"),
45
- chalk.bold("Installed"),
46
- chalk.bold("Latest"),
47
- chalk.bold("Verdict"),
48
- chalk.bold("Capabilities"),
49
- ],
50
- style: { head: [], border: [] },
51
- });
52
- let safeCount = 0;
53
- let warningCount = 0;
54
- let criticalCount = 0;
55
- let notAuditedCount = 0;
56
- const links = [];
57
- for (const dep of deps) {
58
- const versionToCheck = dep.latest ?? dep.installed;
59
- let audit;
60
- if (dep.installed.startsWith("http")) {
61
- // IPFS/URL installs: audit against the latest registry version
62
- audit = await auditSource.getAudit(dep.name, versionToCheck);
41
+ catch {
42
+ results.push({ name, version: versionSpec, verdict: null });
63
43
  }
64
- else if (!dep.hasUpdate) {
65
- audit = await auditSource.getAudit(dep.name, dep.installed);
44
+ }
45
+ // 4. Print summary table
46
+ const nameWidth = Math.max(12, ...results.map((r) => r.name.length)) + 2;
47
+ const versionWidth = Math.max(10, ...results.map((r) => r.version.length)) + 2;
48
+ const header = chalk.bold("Package".padEnd(nameWidth)) +
49
+ chalk.bold("Version".padEnd(versionWidth)) +
50
+ chalk.bold("Verdict");
51
+ console.log(header);
52
+ console.log("─".repeat(nameWidth + versionWidth + 12));
53
+ for (const r of results) {
54
+ const nameCol = r.name.padEnd(nameWidth);
55
+ const versionCol = r.version.padEnd(versionWidth);
56
+ let verdictCol;
57
+ if (r.verdict === null) {
58
+ verdictCol = chalk.gray("not audited");
66
59
  }
67
- else {
68
- audit = await auditSource.getAudit(dep.name, versionToCheck);
60
+ else if (r.verdict.toUpperCase() === "SAFE") {
61
+ verdictCol = chalk.green("SAFE");
69
62
  }
70
- let verdictCol;
71
- let capsCol;
72
- if (audit) {
73
- verdictCol = verdictLabel(audit.verdict, audit.score);
74
- capsCol = capsLabel(audit.capabilities);
75
- if (audit.verdict === "SAFE")
76
- safeCount++;
77
- else if (audit.verdict === "WARNING")
78
- warningCount++;
79
- else if (audit.verdict === "CRITICAL")
80
- criticalCount++;
81
- if (audit.reportCid) {
82
- links.push(` ${dep.name}@${versionToCheck} report: ${IPFS_GATEWAY}/${audit.reportCid}`);
83
- }
63
+ else if (r.verdict.toUpperCase() === "DANGEROUS") {
64
+ verdictCol = chalk.red("DANGEROUS");
84
65
  }
85
66
  else {
86
- verdictCol = chalk.gray("NOT AUDITED");
87
- capsCol = chalk.gray("-");
88
- notAuditedCount++;
89
- }
90
- // Truncate long IPFS URLs so the table stays readable
91
- const installedCol = dep.installed.startsWith("http")
92
- ? chalk.yellow("IPFS " + shortCid(dep.installed.split("/").pop()))
93
- : dep.installed;
94
- table.push([
95
- dep.name,
96
- installedCol,
97
- dep.latest ?? dep.installed,
98
- verdictCol,
99
- capsCol,
100
- ]);
101
- }
102
- spinner.stop();
103
- console.log();
104
- console.log(chalk.bold("NpmGuard Dependency Audit"));
105
- console.log();
106
- console.log(table.toString());
107
- console.log();
108
- // Summary
109
- const parts = [];
110
- if (safeCount > 0)
111
- parts.push(chalk.green(`${safeCount} safe`));
112
- if (warningCount > 0)
113
- parts.push(chalk.yellow(`${warningCount} warnings`));
114
- if (criticalCount > 0)
115
- parts.push(chalk.red(`${criticalCount} critical`));
116
- if (notAuditedCount > 0)
117
- parts.push(chalk.gray(`${notAuditedCount} not audited`));
118
- console.log(` ${parts.join(" | ")}`);
119
- if (links.length > 0) {
120
- console.log();
121
- for (const link of links) {
122
- console.log(chalk.gray(link));
67
+ verdictCol = chalk.yellow(r.verdict);
123
68
  }
124
- }
125
- if (criticalCount > 0) {
126
- console.log();
127
- console.log(chalk.red.bold(` ${criticalCount} package(s) flagged as CRITICAL — do not update without reviewing the report.`));
69
+ console.log(nameCol + versionCol + verdictCol);
128
70
  }
129
71
  console.log();
72
+ // Count stats
73
+ const safe = results.filter((r) => r.verdict?.toUpperCase() === "SAFE").length;
74
+ const dangerous = results.filter((r) => r.verdict?.toUpperCase() === "DANGEROUS").length;
75
+ const notAudited = results.filter((r) => r.verdict === null).length;
76
+ console.log(chalk.green(`${safe} safe`) +
77
+ " | " +
78
+ chalk.red(`${dangerous} dangerous`) +
79
+ " | " +
80
+ chalk.gray(`${notAudited} not audited`));
130
81
  }
package/dist/index.js CHANGED
@@ -1,37 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import "dotenv/config";
3
2
  import { Command } from "commander";
4
- import { resolve } from "node:path";
5
- import { ENSAuditSource } from "./ens-source.js";
6
- import { MockAuditSource } from "./mock-source.js";
3
+ import { auditCommand } from "./commands/audit.js";
7
4
  import { checkCommand } from "./commands/check.js";
8
- import { installCommand } from "./commands/install.js";
9
5
  const program = new Command();
10
6
  program
11
7
  .name("npmguard")
12
- .description("Check npm packages against NpmGuard security audits")
13
- .version("0.1.0");
8
+ .description("NpmGuard CLI — audit npm packages for security issues")
9
+ .version("1.0.0")
10
+ .option("--api <url>", "NpmGuard engine API URL", process.env.NPMGUARD_API_URL ?? "https://npmguard.com");
14
11
  program
15
- .command("check")
16
- .description("Scan project dependencies and check audit status for available updates")
17
- .option("-p, --path <path>", "Path to project directory", ".")
18
- .option("--mock", "Use mock data instead of ENS")
19
- .action(async (opts) => {
20
- const auditSource = opts.mock
21
- ? new MockAuditSource()
22
- : new ENSAuditSource();
23
- const projectPath = resolve(opts.path);
24
- await checkCommand(projectPath, auditSource);
12
+ .command("audit")
13
+ .description("Pay for and run a security audit on an npm package")
14
+ .argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
15
+ .action(async (pkg) => {
16
+ const apiUrl = program.opts().api;
17
+ await auditCommand(pkg, { api: apiUrl });
25
18
  });
26
19
  program
27
- .command("install <package>")
28
- .description("Install a package after checking its NpmGuard audit status")
29
- .option("--force", "Install even if flagged as CRITICAL")
30
- .option("--mock", "Use mock data instead of ENS")
31
- .action(async (pkg, opts) => {
32
- const auditSource = opts.mock
33
- ? new MockAuditSource()
34
- : new ENSAuditSource();
35
- await installCommand(pkg, auditSource, opts.force ?? false);
20
+ .command("check")
21
+ .description("Check all dependencies of a project against existing audits")
22
+ .option("--path <dir>", "Path to project directory", ".")
23
+ .action(async (cmdOpts) => {
24
+ const apiUrl = program.opts().api;
25
+ await checkCommand({ path: cmdOpts.path, api: apiUrl });
36
26
  });
37
27
  program.parse();