npmguard-cli 1.0.1 → 1.1.1
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/dist/api.js +7 -0
- package/dist/commands/audit.js +15 -34
- package/dist/commands/install.js +203 -0
- package/dist/contract.js +60 -0
- package/dist/index.js +10 -0
- package/dist/stream.js +72 -0
- package/dist/utils.js +56 -0
- package/dist/wallet/walletconnect.js +133 -0
- package/package.json +7 -3
package/dist/api.js
CHANGED
|
@@ -30,6 +30,13 @@ export async function startAudit(apiUrl, stripeSessionId) {
|
|
|
30
30
|
body: JSON.stringify({ stripeSessionId }),
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
export async function startAuditWithTxHash(apiUrl, packageName, version, txHash) {
|
|
34
|
+
return request(`${apiUrl}/audit/stream`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ packageName, version, txHash, chain: "base-sepolia" }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
33
40
|
export async function startAuditFree(apiUrl, packageName, version) {
|
|
34
41
|
return request(`${apiUrl}/audit/stream`, {
|
|
35
42
|
method: "POST",
|
package/dist/commands/audit.js
CHANGED
|
@@ -4,35 +4,16 @@ import qrcode from "qrcode-terminal";
|
|
|
4
4
|
import EventSource from "eventsource";
|
|
5
5
|
import * as api from "../api.js";
|
|
6
6
|
import { renderVerdict, renderFinding, renderPhase } from "../render.js";
|
|
7
|
-
|
|
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
|
-
}
|
|
7
|
+
import { parsePackageArg } from "../utils.js";
|
|
34
8
|
export async function auditCommand(pkg, opts) {
|
|
35
9
|
const apiUrl = opts.api;
|
|
10
|
+
const shouldExit = opts.exit !== false;
|
|
11
|
+
const done = (code) => {
|
|
12
|
+
if (shouldExit)
|
|
13
|
+
process.exit(code);
|
|
14
|
+
if (code !== 0)
|
|
15
|
+
throw new Error(`Audit aborted (code ${code})`);
|
|
16
|
+
};
|
|
36
17
|
// 1. Parse package name and version
|
|
37
18
|
let parsed;
|
|
38
19
|
try {
|
|
@@ -40,7 +21,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
40
21
|
}
|
|
41
22
|
catch {
|
|
42
23
|
console.error(chalk.red(`Invalid package: ${pkg}`));
|
|
43
|
-
|
|
24
|
+
return done(1);
|
|
44
25
|
}
|
|
45
26
|
console.log(chalk.bold(`Auditing ${parsed.name}`) +
|
|
46
27
|
(parsed.version ? chalk.gray(`@${parsed.version}`) : ""));
|
|
@@ -54,7 +35,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
54
35
|
renderVerdict(verdict, existing.report?.capabilities ?? [], existing.report?.proofs?.length ?? 0);
|
|
55
36
|
console.log();
|
|
56
37
|
console.log(chalk.dim(`View full report: ${apiUrl}/package/${encodeURIComponent(parsed.name)}/report`));
|
|
57
|
-
|
|
38
|
+
return done(verdict === "SAFE" ? 0 : 1);
|
|
58
39
|
}
|
|
59
40
|
// 2. Try checkout — if payments not configured (501), go straight to free audit
|
|
60
41
|
const spinner = ora("Connecting...").start();
|
|
@@ -66,7 +47,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
66
47
|
catch (err) {
|
|
67
48
|
spinner.fail("Failed to create checkout session: " +
|
|
68
49
|
(err instanceof Error ? err.message : String(err)));
|
|
69
|
-
|
|
50
|
+
return done(1);
|
|
70
51
|
}
|
|
71
52
|
if (checkoutResult.status === 501 || !checkoutResult.data) {
|
|
72
53
|
// Payments not configured — start audit directly (dev/free mode)
|
|
@@ -78,7 +59,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
78
59
|
catch (err) {
|
|
79
60
|
spinner.fail("Failed to start audit: " +
|
|
80
61
|
(err instanceof Error ? err.message : String(err)));
|
|
81
|
-
|
|
62
|
+
return done(1);
|
|
82
63
|
}
|
|
83
64
|
}
|
|
84
65
|
else {
|
|
@@ -112,7 +93,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
112
93
|
catch (err) {
|
|
113
94
|
spinner.fail("Payment polling failed: " +
|
|
114
95
|
(err instanceof Error ? err.message : String(err)));
|
|
115
|
-
|
|
96
|
+
return done(1);
|
|
116
97
|
}
|
|
117
98
|
spinner.text = "Payment confirmed. Starting audit...";
|
|
118
99
|
if (status.auditId) {
|
|
@@ -126,7 +107,7 @@ export async function auditCommand(pkg, opts) {
|
|
|
126
107
|
catch (err) {
|
|
127
108
|
spinner.fail("Failed to start audit: " +
|
|
128
109
|
(err instanceof Error ? err.message : String(err)));
|
|
129
|
-
|
|
110
|
+
return done(1);
|
|
130
111
|
}
|
|
131
112
|
}
|
|
132
113
|
}
|
|
@@ -191,5 +172,5 @@ export async function auditCommand(pkg, opts) {
|
|
|
191
172
|
}
|
|
192
173
|
};
|
|
193
174
|
});
|
|
194
|
-
|
|
175
|
+
return done(exitCode);
|
|
195
176
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { formatEther } from "viem";
|
|
5
|
+
import * as api from "../api.js";
|
|
6
|
+
import { parsePackageArg, prompt, resolveLatestVersion, detectPackageManager, } from "../utils.js";
|
|
7
|
+
import { auditCommand } from "./audit.js";
|
|
8
|
+
import { payViaWalletConnect, readAuditFee } from "../wallet/walletconnect.js";
|
|
9
|
+
import { streamAuditEvents } from "../stream.js";
|
|
10
|
+
/**
|
|
11
|
+
* Run the correct "add this package" command for the detected package manager.
|
|
12
|
+
* `npm install <pkg>` adds the package. `pnpm add <pkg>` / `yarn add <pkg>` do
|
|
13
|
+
* the same. Crucially, `yarn install <pkg>` would ignore <pkg> in yarn classic.
|
|
14
|
+
*/
|
|
15
|
+
function runInstall(packageSpec) {
|
|
16
|
+
const pm = detectPackageManager();
|
|
17
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
18
|
+
console.log(chalk.gray(`\n Running: ${pm} ${verb} ${packageSpec}\n`));
|
|
19
|
+
const res = spawnSync(pm, [verb, packageSpec], { stdio: "inherit" });
|
|
20
|
+
return res.status ?? 1;
|
|
21
|
+
}
|
|
22
|
+
function extractVerdict(report) {
|
|
23
|
+
const nested = report.report?.verdict;
|
|
24
|
+
return (nested ?? report.verdict ?? "UNKNOWN").toUpperCase();
|
|
25
|
+
}
|
|
26
|
+
function extractCapabilities(report) {
|
|
27
|
+
const nested = report.report
|
|
28
|
+
?.capabilities;
|
|
29
|
+
return nested ?? report.capabilities ?? [];
|
|
30
|
+
}
|
|
31
|
+
export async function installCommand(packageSpec, opts) {
|
|
32
|
+
const apiUrl = opts.api;
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = parsePackageArg(packageSpec);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.error(chalk.red(`Invalid package: ${packageSpec}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
let { name, version } = parsed;
|
|
42
|
+
if (!version) {
|
|
43
|
+
const spinner = ora(`Resolving latest version of ${name}...`).start();
|
|
44
|
+
const resolved = await resolveLatestVersion(name);
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
spinner.fail("Could not resolve version from npm registry.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
version = resolved;
|
|
50
|
+
spinner.succeed(`Resolved ${name}@${version}`);
|
|
51
|
+
}
|
|
52
|
+
const fullSpec = `${name}@${version}`;
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.bold(` ${fullSpec}`));
|
|
55
|
+
console.log();
|
|
56
|
+
const spinner = ora("Checking NpmGuard audit...").start();
|
|
57
|
+
let report;
|
|
58
|
+
try {
|
|
59
|
+
report = await api.getPackageReport(apiUrl, name, version);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
spinner.fail("Could not reach NpmGuard API: " +
|
|
63
|
+
(err instanceof Error ? err.message : String(err)));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
spinner.stop();
|
|
67
|
+
if (report) {
|
|
68
|
+
handleExistingReport(report, name, fullSpec, apiUrl, opts);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// No audit found — ask how to pay
|
|
72
|
+
console.log(chalk.gray(" NOT AUDITED — no NpmGuard record for this version."));
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(chalk.bold(" How do you want to pay for the audit?"));
|
|
75
|
+
console.log(" 1) Stripe (credit card)");
|
|
76
|
+
console.log(" 2) WalletConnect — ETH on Base Sepolia");
|
|
77
|
+
console.log(" 3) Install without audit (at your own risk)");
|
|
78
|
+
console.log(" 4) Cancel");
|
|
79
|
+
console.log();
|
|
80
|
+
const choice = await prompt(" Choice [1/2/3/4]: ");
|
|
81
|
+
if (choice === "1") {
|
|
82
|
+
await runStripeAuditAndInstall(fullSpec, name, version, apiUrl);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (choice === "2") {
|
|
86
|
+
await runCryptoAuditAndInstall(fullSpec, name, version, apiUrl);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (choice === "3") {
|
|
90
|
+
console.log(chalk.yellow(" Installing without audit. Proceed at your own risk."));
|
|
91
|
+
process.exit(runInstall(fullSpec));
|
|
92
|
+
}
|
|
93
|
+
console.log(chalk.gray(" Cancelled."));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
function handleExistingReport(report, name, fullSpec, apiUrl, opts) {
|
|
97
|
+
const verdict = extractVerdict(report);
|
|
98
|
+
const capabilities = extractCapabilities(report);
|
|
99
|
+
if (verdict === "SAFE") {
|
|
100
|
+
console.log(chalk.green(" ✓ SAFE — audited by NpmGuard"));
|
|
101
|
+
if (capabilities.length > 0) {
|
|
102
|
+
console.log(chalk.gray(` Capabilities: ${capabilities.join(", ")}`));
|
|
103
|
+
}
|
|
104
|
+
process.exit(runInstall(fullSpec));
|
|
105
|
+
}
|
|
106
|
+
if (verdict === "DANGEROUS" || verdict === "CRITICAL" || verdict === "WARNING") {
|
|
107
|
+
console.log(chalk.bgRed.white.bold(` ${verdict} `));
|
|
108
|
+
if (capabilities.length > 0) {
|
|
109
|
+
console.log(chalk.red(" Capabilities: ") +
|
|
110
|
+
capabilities.map((c) => chalk.yellow(c)).join(", "));
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk.dim(` Full report: ${apiUrl}/package/${encodeURIComponent(name)}/report`));
|
|
113
|
+
console.log();
|
|
114
|
+
if (opts.force) {
|
|
115
|
+
console.log(chalk.yellow(" --force passed, installing anyway..."));
|
|
116
|
+
process.exit(runInstall(fullSpec));
|
|
117
|
+
}
|
|
118
|
+
promptAndInstallIfAccepted(fullSpec, " Install anyway? This package is flagged. (y/N) ");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.yellow(` Verdict: ${verdict}`));
|
|
122
|
+
promptAndInstallIfAccepted(fullSpec, " Proceed with install? (y/N) ");
|
|
123
|
+
}
|
|
124
|
+
async function promptAndInstallIfAccepted(fullSpec, question) {
|
|
125
|
+
const answer = await prompt(chalk.red.bold(question));
|
|
126
|
+
if (answer === "y" || answer === "yes") {
|
|
127
|
+
process.exit(runInstall(fullSpec));
|
|
128
|
+
}
|
|
129
|
+
console.log(chalk.gray(" Aborted."));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
async function runStripeAuditAndInstall(fullSpec, name, version, apiUrl) {
|
|
133
|
+
try {
|
|
134
|
+
await auditCommand(fullSpec, { api: apiUrl, exit: false });
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(chalk.red("Audit failed: " + (err instanceof Error ? err.message : String(err))));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
await finalizeAfterAudit(fullSpec, name, version, apiUrl);
|
|
141
|
+
}
|
|
142
|
+
async function runCryptoAuditAndInstall(fullSpec, name, version, apiUrl) {
|
|
143
|
+
// 1. Read current fee from contract
|
|
144
|
+
let feeWei;
|
|
145
|
+
try {
|
|
146
|
+
feeWei = await readAuditFee();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
console.error(chalk.red("Could not read fee from contract: " +
|
|
150
|
+
(err instanceof Error ? err.message : String(err))));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const feeDisplay = `${formatEther(feeWei)} ETH`;
|
|
154
|
+
const confirm = await prompt(chalk.yellow(` Pay ${feeDisplay} on Base Sepolia? (y/N) `));
|
|
155
|
+
if (confirm !== "y" && confirm !== "yes") {
|
|
156
|
+
console.log(chalk.gray(" Cancelled."));
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
// 2. WalletConnect → user signs → we get txHash
|
|
160
|
+
const result = await payViaWalletConnect(name, version, feeWei, feeDisplay);
|
|
161
|
+
if (!result.paid || !result.txHash) {
|
|
162
|
+
console.log(chalk.red(" Payment failed, aborting."));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
// 3. Engine verifies txHash + returns auditId
|
|
166
|
+
const startSpinner = ora(" Starting audit on engine...").start();
|
|
167
|
+
let auditId;
|
|
168
|
+
try {
|
|
169
|
+
const res = await api.startAuditWithTxHash(apiUrl, name, version, result.txHash);
|
|
170
|
+
auditId = res.auditId;
|
|
171
|
+
startSpinner.succeed(`Audit started (id: ${auditId})`);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
startSpinner.fail("Engine rejected txHash: " +
|
|
175
|
+
(err instanceof Error ? err.message : String(err)));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.cyan(` Watch live: ${apiUrl}/audit/${auditId}`));
|
|
179
|
+
console.log();
|
|
180
|
+
// 4. Stream the audit we just paid for (do NOT call auditCommand — that
|
|
181
|
+
// would trigger a second, unpaid audit via /checkout).
|
|
182
|
+
await streamAuditEvents(apiUrl, auditId);
|
|
183
|
+
// 5. Fetch the persisted report and decide whether to install
|
|
184
|
+
await finalizeAfterAudit(fullSpec, name, version, apiUrl);
|
|
185
|
+
}
|
|
186
|
+
async function finalizeAfterAudit(fullSpec, name, version, apiUrl) {
|
|
187
|
+
const freshReport = await api.getPackageReport(apiUrl, name, version);
|
|
188
|
+
if (!freshReport) {
|
|
189
|
+
console.log(chalk.red(" Audit finished but report not found."));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const verdict = extractVerdict(freshReport);
|
|
193
|
+
if (verdict === "SAFE") {
|
|
194
|
+
console.log(chalk.green("\n ✓ SAFE — proceeding with install"));
|
|
195
|
+
process.exit(runInstall(fullSpec));
|
|
196
|
+
}
|
|
197
|
+
console.log(chalk.red(`\n Audit verdict: ${verdict}`));
|
|
198
|
+
const confirm = await prompt(chalk.red.bold(" Install anyway? (y/N) "));
|
|
199
|
+
if (confirm === "y" || confirm === "yes") {
|
|
200
|
+
process.exit(runInstall(fullSpec));
|
|
201
|
+
}
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// NpmGuardAuditRequest — deployed on Base Sepolia
|
|
2
|
+
// Deploy script: contracts/deploy.sh
|
|
3
|
+
// Source: contracts/src/NpmGuardAuditRequest.sol
|
|
4
|
+
export const AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA = "0xBF562626e4Afb883423Ec719e0270DB232bcB9eD";
|
|
5
|
+
// Base mainnet — not deployed yet
|
|
6
|
+
export const AUDIT_REQUEST_ADDRESS_BASE = "0x";
|
|
7
|
+
export const BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
8
|
+
export const BASE_CHAIN_ID = 8453;
|
|
9
|
+
export const AUDIT_REQUEST_ABI = [
|
|
10
|
+
{
|
|
11
|
+
type: "constructor",
|
|
12
|
+
inputs: [{ name: "_auditFee", type: "uint256" }],
|
|
13
|
+
stateMutability: "nonpayable",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: "function",
|
|
17
|
+
name: "requestAudit",
|
|
18
|
+
inputs: [
|
|
19
|
+
{ name: "packageName", type: "string" },
|
|
20
|
+
{ name: "version", type: "string" },
|
|
21
|
+
],
|
|
22
|
+
outputs: [],
|
|
23
|
+
stateMutability: "payable",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "function",
|
|
27
|
+
name: "isRequested",
|
|
28
|
+
inputs: [
|
|
29
|
+
{ name: "packageName", type: "string" },
|
|
30
|
+
{ name: "version", type: "string" },
|
|
31
|
+
],
|
|
32
|
+
outputs: [{ name: "", type: "bool" }],
|
|
33
|
+
stateMutability: "view",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "function",
|
|
37
|
+
name: "auditFee",
|
|
38
|
+
inputs: [],
|
|
39
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
40
|
+
stateMutability: "view",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "function",
|
|
44
|
+
name: "owner",
|
|
45
|
+
inputs: [],
|
|
46
|
+
outputs: [{ name: "", type: "address" }],
|
|
47
|
+
stateMutability: "view",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "event",
|
|
51
|
+
name: "AuditRequested",
|
|
52
|
+
inputs: [
|
|
53
|
+
{ name: "packageName", type: "string", indexed: false },
|
|
54
|
+
{ name: "version", type: "string", indexed: false },
|
|
55
|
+
{ name: "requester", type: "address", indexed: true },
|
|
56
|
+
{ name: "feePaid", type: "uint256", indexed: false },
|
|
57
|
+
],
|
|
58
|
+
anonymous: false,
|
|
59
|
+
},
|
|
60
|
+
];
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { auditCommand } from "./commands/audit.js";
|
|
4
4
|
import { checkCommand } from "./commands/check.js";
|
|
5
|
+
import { installCommand } from "./commands/install.js";
|
|
5
6
|
const program = new Command();
|
|
6
7
|
program
|
|
7
8
|
.name("npmguard")
|
|
@@ -16,6 +17,15 @@ program
|
|
|
16
17
|
const apiUrl = program.opts().api;
|
|
17
18
|
await auditCommand(pkg, { api: apiUrl });
|
|
18
19
|
});
|
|
20
|
+
program
|
|
21
|
+
.command("install")
|
|
22
|
+
.description("Install an npm package with NpmGuard security check")
|
|
23
|
+
.argument("<package>", "Package name, optionally with version (e.g. express@4.18.0)")
|
|
24
|
+
.option("-f, --force", "Install even if the package is flagged as dangerous")
|
|
25
|
+
.action(async (pkg, cmdOpts) => {
|
|
26
|
+
const apiUrl = program.opts().api;
|
|
27
|
+
await installCommand(pkg, { api: apiUrl, force: cmdOpts.force });
|
|
28
|
+
});
|
|
19
29
|
program
|
|
20
30
|
.command("check")
|
|
21
31
|
.description("Check all dependencies of a project against existing audits")
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import EventSource from "eventsource";
|
|
4
|
+
import { renderVerdict, renderFinding, renderPhase } from "./render.js";
|
|
5
|
+
/**
|
|
6
|
+
* Connect to the engine SSE stream for an existing audit and render events
|
|
7
|
+
* until a verdict or error arrives. Does not exit the process.
|
|
8
|
+
*/
|
|
9
|
+
export async function streamAuditEvents(apiUrl, auditId) {
|
|
10
|
+
const eventsUrl = `${apiUrl}/audit/${encodeURIComponent(auditId)}/events`;
|
|
11
|
+
const es = new EventSource(eventsUrl);
|
|
12
|
+
const spinner = ora("Audit in progress...").start();
|
|
13
|
+
let verdict = "UNKNOWN";
|
|
14
|
+
let exitCode = 0;
|
|
15
|
+
await new Promise((resolve) => {
|
|
16
|
+
es.addEventListener("phase_started", (event) => {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(event.data);
|
|
19
|
+
spinner.text = renderPhase(data.phase ?? data.name ?? "");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
es.addEventListener("finding_discovered", (event) => {
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(event.data);
|
|
28
|
+
const finding = data.finding ?? data;
|
|
29
|
+
spinner.stop();
|
|
30
|
+
renderFinding(finding);
|
|
31
|
+
spinner.start();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
es.addEventListener("verdict_reached", (event) => {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(event.data);
|
|
40
|
+
spinner.stop();
|
|
41
|
+
verdict = (data.verdict ?? "UNKNOWN").toString().toUpperCase();
|
|
42
|
+
renderVerdict(verdict, data.capabilities ?? [], data.proofCount ?? data.findings?.length ?? 0);
|
|
43
|
+
exitCode = verdict === "SAFE" ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
spinner.stop();
|
|
47
|
+
}
|
|
48
|
+
es.close();
|
|
49
|
+
resolve();
|
|
50
|
+
});
|
|
51
|
+
es.addEventListener("audit_error", (event) => {
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(event.data);
|
|
54
|
+
spinner.fail(chalk.red("Audit error: " + (data.error ?? event.data)));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
spinner.fail(chalk.red("Audit error: " + event.data));
|
|
58
|
+
}
|
|
59
|
+
exitCode = 1;
|
|
60
|
+
es.close();
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
es.onerror = () => {
|
|
64
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
65
|
+
spinner.stop();
|
|
66
|
+
es.close();
|
|
67
|
+
resolve();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
return { verdict, exitCode };
|
|
72
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export function parsePackageArg(pkg) {
|
|
5
|
+
if (pkg.startsWith("@")) {
|
|
6
|
+
const slashIndex = pkg.indexOf("/");
|
|
7
|
+
if (slashIndex === -1) {
|
|
8
|
+
throw new Error(`Invalid scoped package name: ${pkg}`);
|
|
9
|
+
}
|
|
10
|
+
const rest = pkg.slice(slashIndex + 1);
|
|
11
|
+
const atIndex = rest.lastIndexOf("@");
|
|
12
|
+
if (atIndex > 0) {
|
|
13
|
+
return {
|
|
14
|
+
name: pkg.slice(0, slashIndex + 1 + atIndex),
|
|
15
|
+
version: rest.slice(atIndex + 1),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return { name: pkg };
|
|
19
|
+
}
|
|
20
|
+
const atIndex = pkg.lastIndexOf("@");
|
|
21
|
+
if (atIndex > 0) {
|
|
22
|
+
return {
|
|
23
|
+
name: pkg.slice(0, atIndex),
|
|
24
|
+
version: pkg.slice(atIndex + 1),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { name: pkg };
|
|
28
|
+
}
|
|
29
|
+
export function prompt(question) {
|
|
30
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
rl.question(question, (answer) => {
|
|
33
|
+
rl.close();
|
|
34
|
+
resolve(answer.trim().toLowerCase());
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function resolveLatestVersion(packageName) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
41
|
+
if (!res.ok)
|
|
42
|
+
return null;
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
return data.version ?? null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function detectPackageManager(cwd = process.cwd()) {
|
|
51
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
52
|
+
return "pnpm";
|
|
53
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
54
|
+
return "yarn";
|
|
55
|
+
return "npm";
|
|
56
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import qrcode from "qrcode-terminal";
|
|
4
|
+
import { SignClient } from "@walletconnect/sign-client";
|
|
5
|
+
import { createPublicClient, http, encodeFunctionData } from "viem";
|
|
6
|
+
import { baseSepolia } from "viem/chains";
|
|
7
|
+
import { AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA, AUDIT_REQUEST_ABI, BASE_SEPOLIA_CHAIN_ID, } from "../contract.js";
|
|
8
|
+
const WALLETCONNECT_PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID ?? "d5eb170c427570e15ac00ae53acc93ba";
|
|
9
|
+
function generateQrCode(text) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
qrcode.generate(text, { small: true }, (code) => {
|
|
12
|
+
console.log(code);
|
|
13
|
+
resolve();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function payViaWalletConnect(packageName, version, feeWei, feeDisplay) {
|
|
18
|
+
const calldata = encodeFunctionData({
|
|
19
|
+
abi: AUDIT_REQUEST_ABI,
|
|
20
|
+
functionName: "requestAudit",
|
|
21
|
+
args: [packageName, version],
|
|
22
|
+
});
|
|
23
|
+
const publicClient = createPublicClient({
|
|
24
|
+
chain: baseSepolia,
|
|
25
|
+
transport: http(),
|
|
26
|
+
});
|
|
27
|
+
let signClient = null;
|
|
28
|
+
// WalletConnect throws late "No matching key" errors when sessions clean up
|
|
29
|
+
const wcErrorHandler = (err) => {
|
|
30
|
+
if (err?.message?.includes("No matching key"))
|
|
31
|
+
return;
|
|
32
|
+
console.error(err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
};
|
|
35
|
+
process.on("uncaughtException", wcErrorHandler);
|
|
36
|
+
try {
|
|
37
|
+
const initSpinner = ora(" Connecting to WalletConnect...").start();
|
|
38
|
+
signClient = await SignClient.init({
|
|
39
|
+
projectId: WALLETCONNECT_PROJECT_ID,
|
|
40
|
+
metadata: {
|
|
41
|
+
name: "NpmGuard",
|
|
42
|
+
description: "NPM package security audit",
|
|
43
|
+
url: "https://npmguard.com",
|
|
44
|
+
icons: [],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
initSpinner.stop();
|
|
48
|
+
const { uri, approval } = await signClient.connect({
|
|
49
|
+
requiredNamespaces: {
|
|
50
|
+
eip155: {
|
|
51
|
+
methods: ["eth_sendTransaction"],
|
|
52
|
+
chains: [`eip155:${BASE_SEPOLIA_CHAIN_ID}`],
|
|
53
|
+
events: ["chainChanged", "accountsChanged"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (!uri) {
|
|
58
|
+
console.log(chalk.red(" Failed to generate WalletConnect URI"));
|
|
59
|
+
return { paid: false };
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(chalk.cyan(" Scan with your wallet to connect:"));
|
|
63
|
+
console.log();
|
|
64
|
+
await generateQrCode(uri);
|
|
65
|
+
console.log();
|
|
66
|
+
const pairSpinner = ora(" Waiting for wallet connection...").start();
|
|
67
|
+
const session = await approval();
|
|
68
|
+
const accounts = session.namespaces.eip155?.accounts ?? [];
|
|
69
|
+
const baseAccount = accounts.find((a) => a.startsWith(`eip155:${BASE_SEPOLIA_CHAIN_ID}:`));
|
|
70
|
+
const sender = baseAccount
|
|
71
|
+
? baseAccount.split(":")[2]
|
|
72
|
+
: accounts[0]?.split(":")[2];
|
|
73
|
+
if (!sender) {
|
|
74
|
+
pairSpinner.fail("Wallet did not approve any accounts");
|
|
75
|
+
return { paid: false };
|
|
76
|
+
}
|
|
77
|
+
pairSpinner.succeed(`Connected: ${sender.slice(0, 6)}...${sender.slice(-4)}`);
|
|
78
|
+
console.log(chalk.cyan(` Confirm the ${feeDisplay} transaction in your wallet (Base Sepolia)...`));
|
|
79
|
+
const txHash = (await signClient.request({
|
|
80
|
+
topic: session.topic,
|
|
81
|
+
chainId: `eip155:${BASE_SEPOLIA_CHAIN_ID}`,
|
|
82
|
+
request: {
|
|
83
|
+
method: "eth_sendTransaction",
|
|
84
|
+
params: [
|
|
85
|
+
{
|
|
86
|
+
from: sender,
|
|
87
|
+
to: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
|
|
88
|
+
data: calldata,
|
|
89
|
+
value: "0x" + feeWei.toString(16),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
const confirmSpinner = ora(" Waiting for on-chain confirmation...").start();
|
|
95
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
96
|
+
hash: txHash,
|
|
97
|
+
});
|
|
98
|
+
if (receipt.status === "success") {
|
|
99
|
+
confirmSpinner.succeed("Payment confirmed on-chain");
|
|
100
|
+
console.log(chalk.gray(` Tx: https://sepolia.basescan.org/tx/${txHash}`));
|
|
101
|
+
console.log();
|
|
102
|
+
return { paid: true, txHash, sender };
|
|
103
|
+
}
|
|
104
|
+
confirmSpinner.fail("Transaction reverted");
|
|
105
|
+
return { paid: false };
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
if (msg.includes("rejected") || msg.includes("denied")) {
|
|
110
|
+
console.log(chalk.yellow(" Transaction rejected by user."));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(chalk.red(` WalletConnect error: ${msg}`));
|
|
114
|
+
}
|
|
115
|
+
console.log();
|
|
116
|
+
return { paid: false };
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
signClient = null;
|
|
120
|
+
setTimeout(() => process.off("uncaughtException", wcErrorHandler), 5000);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function readAuditFee() {
|
|
124
|
+
const publicClient = createPublicClient({
|
|
125
|
+
chain: baseSepolia,
|
|
126
|
+
transport: http(),
|
|
127
|
+
});
|
|
128
|
+
return (await publicClient.readContract({
|
|
129
|
+
address: AUDIT_REQUEST_ADDRESS_BASE_SEPOLIA,
|
|
130
|
+
abi: AUDIT_REQUEST_ABI,
|
|
131
|
+
functionName: "auditFee",
|
|
132
|
+
}));
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npmguard-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "NpmGuard CLI — check npm packages against NpmGuard security audits",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
-
"files": [
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
8
10
|
"bin": {
|
|
9
11
|
"npmguard": "./dist/index.js"
|
|
10
12
|
},
|
|
@@ -19,11 +21,13 @@
|
|
|
19
21
|
],
|
|
20
22
|
"license": "MIT",
|
|
21
23
|
"dependencies": {
|
|
24
|
+
"@walletconnect/sign-client": "^2.23.9",
|
|
22
25
|
"chalk": "^5.3.0",
|
|
23
26
|
"commander": "^12.1.0",
|
|
24
27
|
"eventsource": "^2.0.2",
|
|
25
28
|
"ora": "^8.1.0",
|
|
26
|
-
"qrcode-terminal": "^0.12.0"
|
|
29
|
+
"qrcode-terminal": "^0.12.0",
|
|
30
|
+
"viem": "^2.47.17"
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
29
33
|
"@types/node": "^22.0.0",
|