npmguard-cli 0.3.1 → 0.5.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 +155 -0
- package/dist/commands/install.js +320 -44
- package/dist/contract.d.ts +121 -0
- package/dist/contract.js +92 -0
- package/dist/ens-source.js +34 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/package.json +4 -1
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# npmguard-cli
|
|
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).
|
|
4
|
+
|
|
5
|
+
## `npmguard-cli check`
|
|
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
|
+
```
|
package/dist/commands/install.js
CHANGED
|
@@ -1,17 +1,188 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
+
import qrcode from "qrcode-terminal";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
3
5
|
import { execSync } from "node:child_process";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { createPublicClient, createWalletClient, http, formatEther, encodeFunctionData, } from "viem";
|
|
7
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
8
|
+
import { defineChain } from "viem";
|
|
9
|
+
import { SignClient } from "@walletconnect/sign-client";
|
|
10
|
+
import { AUDIT_REQUEST_ADDRESS_0G, AUDIT_REQUEST_ABI, } from "../contract.js";
|
|
11
|
+
const ogGalileo = defineChain({
|
|
12
|
+
id: 16602,
|
|
13
|
+
name: "0G-Galileo-Testnet",
|
|
14
|
+
nativeCurrency: { name: "0G", symbol: "0G", decimals: 18 },
|
|
15
|
+
rpcUrls: { default: { http: ["https://evmrpc-testnet.0g.ai"] } },
|
|
16
|
+
blockExplorers: { default: { name: "0G Explorer", url: "https://chainscan-galileo.0g.ai" } },
|
|
17
|
+
testnet: true,
|
|
18
|
+
});
|
|
7
19
|
const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
21
|
+
const DEFAULT_AUDIT_API_URL = "http://209.38.42.28:8000/audit";
|
|
22
|
+
const WALLETCONNECT_PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID ?? "d5eb170c427570e15ac00ae53acc93ba";
|
|
23
|
+
const OG_RPC = "https://evmrpc-testnet.0g.ai";
|
|
24
|
+
const BLOCK_EXPLORER = "https://chainscan-galileo.0g.ai";
|
|
25
|
+
function prompt(question) {
|
|
26
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question(question, (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer.trim().toLowerCase());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function generateQrCode(text) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
qrcode.generate(text, { small: true }, (code) => {
|
|
37
|
+
console.log(code);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function askInstallWithoutAudit(packageSpec) {
|
|
43
|
+
const answer = await prompt(chalk.white(` Install from npm without audit? (y/n) `));
|
|
44
|
+
if (answer === "y" || answer === "yes") {
|
|
45
|
+
console.log();
|
|
46
|
+
execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function requestAuditOnChain(packageName, version, privateKey) {
|
|
50
|
+
const account = privateKeyToAccount(privateKey);
|
|
51
|
+
const rpcUrl = OG_RPC;
|
|
52
|
+
const publicClient = createPublicClient({
|
|
53
|
+
chain: ogGalileo,
|
|
54
|
+
transport: http(rpcUrl),
|
|
55
|
+
});
|
|
56
|
+
const walletClient = createWalletClient({
|
|
57
|
+
account,
|
|
58
|
+
chain: ogGalileo,
|
|
59
|
+
transport: http(rpcUrl),
|
|
60
|
+
});
|
|
61
|
+
const auditFee = await publicClient.readContract({
|
|
62
|
+
address: AUDIT_REQUEST_ADDRESS_0G,
|
|
63
|
+
abi: AUDIT_REQUEST_ABI,
|
|
64
|
+
functionName: "auditFee",
|
|
65
|
+
});
|
|
66
|
+
const hash = await walletClient.writeContract({
|
|
67
|
+
address: AUDIT_REQUEST_ADDRESS_0G,
|
|
68
|
+
abi: AUDIT_REQUEST_ABI,
|
|
69
|
+
functionName: "requestAudit",
|
|
70
|
+
args: [packageName, version],
|
|
71
|
+
value: auditFee,
|
|
72
|
+
});
|
|
73
|
+
await publicClient.waitForTransactionReceipt({ hash });
|
|
74
|
+
return hash;
|
|
75
|
+
}
|
|
76
|
+
async function payViaWalletConnect(packageName, version, feeWei, feeDisplay) {
|
|
77
|
+
const calldata = encodeFunctionData({
|
|
78
|
+
abi: AUDIT_REQUEST_ABI,
|
|
79
|
+
functionName: "requestAudit",
|
|
80
|
+
args: [packageName, version],
|
|
81
|
+
});
|
|
82
|
+
let signClient = null;
|
|
83
|
+
// WalletConnect throws unhandled errors when MetaMask sends
|
|
84
|
+
// session events after the session is cleaned up — suppress them
|
|
85
|
+
const wcErrorHandler = (err) => {
|
|
86
|
+
if (err?.message?.includes("No matching key"))
|
|
87
|
+
return;
|
|
88
|
+
console.error(err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
};
|
|
91
|
+
process.on("uncaughtException", wcErrorHandler);
|
|
92
|
+
try {
|
|
93
|
+
const initSpinner = ora(" Connecting to WalletConnect...").start();
|
|
94
|
+
signClient = await SignClient.init({
|
|
95
|
+
projectId: WALLETCONNECT_PROJECT_ID,
|
|
96
|
+
metadata: {
|
|
97
|
+
name: "NpmGuard",
|
|
98
|
+
description: "NPM package security audit",
|
|
99
|
+
url: "https://npmguard.dev",
|
|
100
|
+
icons: [],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
initSpinner.stop();
|
|
104
|
+
const { uri, approval } = await signClient.connect({
|
|
105
|
+
requiredNamespaces: {
|
|
106
|
+
eip155: {
|
|
107
|
+
methods: ["eth_sendTransaction"],
|
|
108
|
+
chains: ["eip155:16602"],
|
|
109
|
+
events: ["chainChanged", "accountsChanged"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (!uri) {
|
|
114
|
+
console.log(chalk.red(" Failed to generate WalletConnect URI"));
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(chalk.cyan(` Scan with your wallet to connect:`));
|
|
119
|
+
console.log();
|
|
120
|
+
await generateQrCode(uri);
|
|
121
|
+
console.log();
|
|
122
|
+
const pairSpinner = ora(" Waiting for wallet connection...").start();
|
|
123
|
+
const session = await approval();
|
|
124
|
+
// Find the 0G Galileo account in approved namespaces
|
|
125
|
+
const accounts = session.namespaces.eip155?.accounts ?? [];
|
|
126
|
+
const ogAccount = accounts.find((a) => a.startsWith("eip155:16602:"));
|
|
127
|
+
const account = ogAccount
|
|
128
|
+
? ogAccount.split(":")[2]
|
|
129
|
+
: accounts[0]?.split(":")[2];
|
|
130
|
+
if (!account) {
|
|
131
|
+
pairSpinner.fail("Wallet did not approve any accounts");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
pairSpinner.succeed(`Connected: ${account.slice(0, 6)}...${account.slice(-4)}`);
|
|
135
|
+
console.log(chalk.cyan(` Confirm the ${feeDisplay} transaction in your wallet...`));
|
|
136
|
+
const txHash = await signClient.request({
|
|
137
|
+
topic: session.topic,
|
|
138
|
+
chainId: "eip155:16602",
|
|
139
|
+
request: {
|
|
140
|
+
method: "eth_sendTransaction",
|
|
141
|
+
params: [
|
|
142
|
+
{
|
|
143
|
+
from: account,
|
|
144
|
+
to: AUDIT_REQUEST_ADDRESS_0G,
|
|
145
|
+
data: calldata,
|
|
146
|
+
value: "0x" + feeWei.toString(16),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const confirmSpinner = ora(" Waiting for on-chain confirmation...").start();
|
|
152
|
+
const ogClient = createPublicClient({
|
|
153
|
+
chain: ogGalileo,
|
|
154
|
+
transport: http(OG_RPC),
|
|
155
|
+
});
|
|
156
|
+
const receipt = await ogClient.waitForTransactionReceipt({
|
|
157
|
+
hash: txHash,
|
|
158
|
+
});
|
|
159
|
+
if (receipt.status === "success") {
|
|
160
|
+
confirmSpinner.succeed("Payment confirmed on-chain!");
|
|
161
|
+
console.log(chalk.gray(` Tx: ${BLOCK_EXPLORER}/tx/${txHash}`));
|
|
162
|
+
console.log();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
confirmSpinner.fail("Transaction reverted");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const msg = err.message ?? String(err);
|
|
172
|
+
if (msg.includes("rejected") || msg.includes("denied")) {
|
|
173
|
+
console.log(chalk.yellow(" Transaction rejected by user."));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log(chalk.red(` WalletConnect error: ${msg}`));
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
signClient = null;
|
|
183
|
+
// Remove handler after a delay to catch late WC events
|
|
184
|
+
setTimeout(() => process.off("uncaughtException", wcErrorHandler), 5000);
|
|
12
185
|
}
|
|
13
|
-
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
14
|
-
await writeFile(dest, buffer);
|
|
15
186
|
}
|
|
16
187
|
export async function installCommand(packageSpec, auditSource, force = false) {
|
|
17
188
|
const atIndex = packageSpec.lastIndexOf("@");
|
|
@@ -46,16 +217,144 @@ export async function installCommand(packageSpec, auditSource, force = false) {
|
|
|
46
217
|
console.log();
|
|
47
218
|
console.log(chalk.bold(` ${packageName}@${requestedVersion}`));
|
|
48
219
|
console.log();
|
|
49
|
-
// No audit found
|
|
220
|
+
// ─── No audit found ───────────────────────────────────────────────
|
|
50
221
|
if (!audit) {
|
|
51
222
|
console.log(chalk.gray(` NOT AUDITED — no NpmGuard record found for this version.`));
|
|
52
223
|
console.log();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
224
|
+
const privateKey = process.env.NPMGUARD_PRIVATE_KEY;
|
|
225
|
+
const contractDeployed = AUDIT_REQUEST_ADDRESS_0G !== ZERO_ADDRESS;
|
|
226
|
+
if (contractDeployed) {
|
|
227
|
+
const publicClient = createPublicClient({
|
|
228
|
+
chain: ogGalileo,
|
|
229
|
+
transport: http(OG_RPC),
|
|
230
|
+
});
|
|
231
|
+
// Check if user already paid (previous attempt where audit engine failed)
|
|
232
|
+
let alreadyPaid = false;
|
|
233
|
+
try {
|
|
234
|
+
alreadyPaid = (await publicClient.readContract({
|
|
235
|
+
address: AUDIT_REQUEST_ADDRESS_0G,
|
|
236
|
+
abi: AUDIT_REQUEST_ABI,
|
|
237
|
+
functionName: "isRequested",
|
|
238
|
+
args: [packageName, requestedVersion],
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// can't read — assume not paid
|
|
243
|
+
}
|
|
244
|
+
if (alreadyPaid) {
|
|
245
|
+
console.log(chalk.cyan(` Already paid on-chain — re-triggering audit...`));
|
|
246
|
+
console.log();
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Read fee for display
|
|
250
|
+
let feeDisplay = "0.01 0G";
|
|
251
|
+
let feeWei = 10000000000000000n;
|
|
252
|
+
try {
|
|
253
|
+
feeWei = (await publicClient.readContract({
|
|
254
|
+
address: AUDIT_REQUEST_ADDRESS_0G,
|
|
255
|
+
abi: AUDIT_REQUEST_ABI,
|
|
256
|
+
functionName: "auditFee",
|
|
257
|
+
}));
|
|
258
|
+
feeDisplay = `${formatEther(feeWei)} 0G`;
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
261
|
+
const wantAudit = await prompt(chalk.yellow(` Request on-chain audit for ${feeDisplay}? (y/n) `));
|
|
262
|
+
if (wantAudit !== "y" && wantAudit !== "yes") {
|
|
263
|
+
return askInstallWithoutAudit(packageSpec);
|
|
264
|
+
}
|
|
265
|
+
// Ask how to pay
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(chalk.bold(` How to pay?`));
|
|
268
|
+
if (privateKey) {
|
|
269
|
+
console.log(` 1) Wallet (NPMGUARD_PRIVATE_KEY)`);
|
|
270
|
+
console.log(` 2) WalletConnect (mobile wallet)`);
|
|
271
|
+
console.log(` 3) Back`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
console.log(` 1) WalletConnect (mobile wallet)`);
|
|
275
|
+
console.log(` 2) Back`);
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
const choice = await prompt(` Choice: `);
|
|
279
|
+
const backChoice = privateKey ? "3" : "2";
|
|
280
|
+
if (choice === backChoice) {
|
|
281
|
+
return askInstallWithoutAudit(packageSpec);
|
|
282
|
+
}
|
|
283
|
+
if (privateKey && choice === "1") {
|
|
284
|
+
const txSpinner = ora(" Sending payment transaction...").start();
|
|
285
|
+
try {
|
|
286
|
+
const txHash = await requestAuditOnChain(packageName, requestedVersion, privateKey);
|
|
287
|
+
txSpinner.succeed("Payment confirmed on-chain!");
|
|
288
|
+
console.log(chalk.gray(` Tx: ${BLOCK_EXPLORER}/tx/${txHash}`));
|
|
289
|
+
console.log();
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
txSpinner.fail("Transaction failed");
|
|
293
|
+
console.log(chalk.red(` ${err.shortMessage ?? err.message}`));
|
|
294
|
+
console.log();
|
|
295
|
+
return askInstallWithoutAudit(packageSpec);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else if ((privateKey && choice === "2") || (!privateKey && choice === "1")) {
|
|
299
|
+
const paid = await payViaWalletConnect(packageName, requestedVersion, feeWei, feeDisplay);
|
|
300
|
+
if (!paid)
|
|
301
|
+
return askInstallWithoutAudit(packageSpec);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
return askInstallWithoutAudit(packageSpec);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Trigger audit engine
|
|
308
|
+
const auditApiUrl = process.env.NPMGUARD_AUDIT_API_URL ?? DEFAULT_AUDIT_API_URL;
|
|
309
|
+
const auditSpinner = ora(" Running security audit...").start();
|
|
310
|
+
try {
|
|
311
|
+
const resp = await fetch(auditApiUrl, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { "Content-Type": "application/json" },
|
|
314
|
+
body: JSON.stringify({ packageName, version: requestedVersion }),
|
|
315
|
+
});
|
|
316
|
+
if (!resp.ok)
|
|
317
|
+
throw new Error(`Audit engine returned ${resp.status}`);
|
|
318
|
+
const result = await resp.json();
|
|
319
|
+
auditSpinner.stop();
|
|
320
|
+
console.log();
|
|
321
|
+
const verdict = (result.verdict ?? "UNKNOWN").toUpperCase();
|
|
322
|
+
const capabilities = result.capabilities ?? [];
|
|
323
|
+
if (verdict === "SAFE") {
|
|
324
|
+
console.log(chalk.green(` Verdict: SAFE`));
|
|
325
|
+
}
|
|
326
|
+
else if (verdict === "DANGEROUS") {
|
|
327
|
+
console.log(chalk.red(` Verdict: DANGEROUS`));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
console.log(chalk.yellow(` Verdict: ${verdict}`));
|
|
331
|
+
}
|
|
332
|
+
if (capabilities.length > 0) {
|
|
333
|
+
console.log(chalk.gray(` Capabilities: ${capabilities.join(", ")}`));
|
|
334
|
+
}
|
|
335
|
+
console.log();
|
|
336
|
+
if (verdict === "DANGEROUS" && !force) {
|
|
337
|
+
console.log(chalk.red.bold(" Installation blocked. This package is dangerous."));
|
|
338
|
+
console.log(chalk.gray(" Use --force to install anyway."));
|
|
339
|
+
console.log();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.log(chalk.gray(" Installing from npm..."));
|
|
343
|
+
console.log();
|
|
344
|
+
execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
auditSpinner.fail("Audit engine unreachable");
|
|
348
|
+
console.log(chalk.gray(` ${err.message ?? err}`));
|
|
349
|
+
console.log();
|
|
350
|
+
return askInstallWithoutAudit(packageSpec);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Contract not deployed
|
|
355
|
+
return askInstallWithoutAudit(packageSpec);
|
|
57
356
|
}
|
|
58
|
-
//
|
|
357
|
+
// ─── Audit found — show verdict ───────────────────────────────────
|
|
59
358
|
if (audit.verdict === "SAFE") {
|
|
60
359
|
console.log(chalk.green(` SAFE (score: ${audit.score})`));
|
|
61
360
|
}
|
|
@@ -81,41 +380,18 @@ export async function installCommand(packageSpec, auditSource, force = false) {
|
|
|
81
380
|
}
|
|
82
381
|
// Install from IPFS if sourceCid is available
|
|
83
382
|
if (audit.sourceCid) {
|
|
84
|
-
const
|
|
383
|
+
const ipfsUrl = `${IPFS_GATEWAY}/${audit.sourceCid}`;
|
|
384
|
+
console.log(chalk.green(` Installing from verified IPFS source...`));
|
|
385
|
+
console.log();
|
|
85
386
|
try {
|
|
86
|
-
|
|
87
|
-
await mkdir(tmpDir, { recursive: true });
|
|
88
|
-
const tarballPath = join(tmpDir, `${packageName}-${requestedVersion}.tgz`);
|
|
89
|
-
await downloadFromIPFS(audit.sourceCid, tarballPath);
|
|
90
|
-
dlSpinner.succeed("Downloaded from IPFS");
|
|
91
|
-
console.log(chalk.green(` Installing from verified IPFS source...`));
|
|
92
|
-
console.log();
|
|
93
|
-
execSync(`npm install ${tarballPath}`, { stdio: "inherit" });
|
|
94
|
-
// Fix package.json — replace the file: path with the real version
|
|
95
|
-
try {
|
|
96
|
-
const pkgPath = join(process.cwd(), "package.json");
|
|
97
|
-
const pkgRaw = await readFile(pkgPath, "utf-8");
|
|
98
|
-
const pkg = JSON.parse(pkgRaw);
|
|
99
|
-
if (pkg.dependencies?.[packageName]?.startsWith("file:")) {
|
|
100
|
-
pkg.dependencies[packageName] = `^${requestedVersion}`;
|
|
101
|
-
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Not critical if this fails
|
|
106
|
-
}
|
|
107
|
-
// Cleanup tarball
|
|
108
|
-
await unlink(tarballPath).catch(() => { });
|
|
387
|
+
execSync(`npm install ${ipfsUrl}`, { stdio: "inherit" });
|
|
109
388
|
}
|
|
110
|
-
catch
|
|
111
|
-
|
|
112
|
-
console.log(chalk.yellow(" Falling back to npm install..."));
|
|
113
|
-
console.log();
|
|
389
|
+
catch {
|
|
390
|
+
console.log(chalk.yellow(" IPFS install failed, falling back to npm..."));
|
|
114
391
|
execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
|
|
115
392
|
}
|
|
116
393
|
}
|
|
117
394
|
else {
|
|
118
|
-
// No sourceCid — install from npm
|
|
119
395
|
console.log(chalk.gray(" No IPFS source available, installing from npm..."));
|
|
120
396
|
console.log();
|
|
121
397
|
execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export declare const AUDIT_REQUEST_ADDRESS: `0x${string}`;
|
|
2
|
+
export declare const AUDIT_REQUEST_ADDRESS_0G: `0x${string}`;
|
|
3
|
+
export declare const AUDIT_REQUEST_ABI: readonly [{
|
|
4
|
+
readonly inputs: readonly [{
|
|
5
|
+
readonly name: "_auditFee";
|
|
6
|
+
readonly type: "uint256";
|
|
7
|
+
}];
|
|
8
|
+
readonly stateMutability: "nonpayable";
|
|
9
|
+
readonly type: "constructor";
|
|
10
|
+
}, {
|
|
11
|
+
readonly anonymous: false;
|
|
12
|
+
readonly inputs: readonly [{
|
|
13
|
+
readonly indexed: false;
|
|
14
|
+
readonly name: "packageName";
|
|
15
|
+
readonly type: "string";
|
|
16
|
+
}, {
|
|
17
|
+
readonly indexed: false;
|
|
18
|
+
readonly name: "version";
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
}, {
|
|
21
|
+
readonly indexed: true;
|
|
22
|
+
readonly name: "requester";
|
|
23
|
+
readonly type: "address";
|
|
24
|
+
}];
|
|
25
|
+
readonly name: "AuditRequested";
|
|
26
|
+
readonly type: "event";
|
|
27
|
+
}, {
|
|
28
|
+
readonly anonymous: false;
|
|
29
|
+
readonly inputs: readonly [{
|
|
30
|
+
readonly indexed: true;
|
|
31
|
+
readonly name: "key";
|
|
32
|
+
readonly type: "bytes32";
|
|
33
|
+
}, {
|
|
34
|
+
readonly indexed: true;
|
|
35
|
+
readonly name: "requester";
|
|
36
|
+
readonly type: "address";
|
|
37
|
+
}];
|
|
38
|
+
readonly name: "AuditRequestedByKey";
|
|
39
|
+
readonly type: "event";
|
|
40
|
+
}, {
|
|
41
|
+
readonly inputs: readonly [{
|
|
42
|
+
readonly name: "packageName";
|
|
43
|
+
readonly type: "string";
|
|
44
|
+
}, {
|
|
45
|
+
readonly name: "version";
|
|
46
|
+
readonly type: "string";
|
|
47
|
+
}];
|
|
48
|
+
readonly name: "requestAudit";
|
|
49
|
+
readonly outputs: readonly [];
|
|
50
|
+
readonly stateMutability: "payable";
|
|
51
|
+
readonly type: "function";
|
|
52
|
+
}, {
|
|
53
|
+
readonly inputs: readonly [{
|
|
54
|
+
readonly name: "key";
|
|
55
|
+
readonly type: "bytes32";
|
|
56
|
+
}];
|
|
57
|
+
readonly name: "requestAuditByKey";
|
|
58
|
+
readonly outputs: readonly [];
|
|
59
|
+
readonly stateMutability: "payable";
|
|
60
|
+
readonly type: "function";
|
|
61
|
+
}, {
|
|
62
|
+
readonly inputs: readonly [];
|
|
63
|
+
readonly name: "auditFee";
|
|
64
|
+
readonly outputs: readonly [{
|
|
65
|
+
readonly name: "";
|
|
66
|
+
readonly type: "uint256";
|
|
67
|
+
}];
|
|
68
|
+
readonly stateMutability: "view";
|
|
69
|
+
readonly type: "function";
|
|
70
|
+
}, {
|
|
71
|
+
readonly inputs: readonly [];
|
|
72
|
+
readonly name: "owner";
|
|
73
|
+
readonly outputs: readonly [{
|
|
74
|
+
readonly name: "";
|
|
75
|
+
readonly type: "address";
|
|
76
|
+
}];
|
|
77
|
+
readonly stateMutability: "view";
|
|
78
|
+
readonly type: "function";
|
|
79
|
+
}, {
|
|
80
|
+
readonly inputs: readonly [{
|
|
81
|
+
readonly name: "packageName";
|
|
82
|
+
readonly type: "string";
|
|
83
|
+
}, {
|
|
84
|
+
readonly name: "version";
|
|
85
|
+
readonly type: "string";
|
|
86
|
+
}];
|
|
87
|
+
readonly name: "isRequested";
|
|
88
|
+
readonly outputs: readonly [{
|
|
89
|
+
readonly name: "";
|
|
90
|
+
readonly type: "bool";
|
|
91
|
+
}];
|
|
92
|
+
readonly stateMutability: "view";
|
|
93
|
+
readonly type: "function";
|
|
94
|
+
}, {
|
|
95
|
+
readonly inputs: readonly [{
|
|
96
|
+
readonly name: "";
|
|
97
|
+
readonly type: "bytes32";
|
|
98
|
+
}];
|
|
99
|
+
readonly name: "requested";
|
|
100
|
+
readonly outputs: readonly [{
|
|
101
|
+
readonly name: "";
|
|
102
|
+
readonly type: "bool";
|
|
103
|
+
}];
|
|
104
|
+
readonly stateMutability: "view";
|
|
105
|
+
readonly type: "function";
|
|
106
|
+
}, {
|
|
107
|
+
readonly inputs: readonly [{
|
|
108
|
+
readonly name: "_fee";
|
|
109
|
+
readonly type: "uint256";
|
|
110
|
+
}];
|
|
111
|
+
readonly name: "setFee";
|
|
112
|
+
readonly outputs: readonly [];
|
|
113
|
+
readonly stateMutability: "nonpayable";
|
|
114
|
+
readonly type: "function";
|
|
115
|
+
}, {
|
|
116
|
+
readonly inputs: readonly [];
|
|
117
|
+
readonly name: "withdraw";
|
|
118
|
+
readonly outputs: readonly [];
|
|
119
|
+
readonly stateMutability: "nonpayable";
|
|
120
|
+
readonly type: "function";
|
|
121
|
+
}];
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// NpmGuardAuditRequest contract — deployed on Sepolia + 0G Galileo Testnet
|
|
2
|
+
// Update these addresses after running: cd contracts && npm run deploy
|
|
3
|
+
export const AUDIT_REQUEST_ADDRESS = "0x4bbaf196bde9e02594631e03c28ebe16719214f3"; // Sepolia
|
|
4
|
+
export const AUDIT_REQUEST_ADDRESS_0G = "0x1201448ae5f00e1783036439569e71ab3757d0de"; // 0G Galileo Testnet
|
|
5
|
+
export const AUDIT_REQUEST_ABI = [
|
|
6
|
+
{
|
|
7
|
+
inputs: [{ name: "_auditFee", type: "uint256" }],
|
|
8
|
+
stateMutability: "nonpayable",
|
|
9
|
+
type: "constructor",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
anonymous: false,
|
|
13
|
+
inputs: [
|
|
14
|
+
{ indexed: false, name: "packageName", type: "string" },
|
|
15
|
+
{ indexed: false, name: "version", type: "string" },
|
|
16
|
+
{ indexed: true, name: "requester", type: "address" },
|
|
17
|
+
],
|
|
18
|
+
name: "AuditRequested",
|
|
19
|
+
type: "event",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
anonymous: false,
|
|
23
|
+
inputs: [
|
|
24
|
+
{ indexed: true, name: "key", type: "bytes32" },
|
|
25
|
+
{ indexed: true, name: "requester", type: "address" },
|
|
26
|
+
],
|
|
27
|
+
name: "AuditRequestedByKey",
|
|
28
|
+
type: "event",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
inputs: [
|
|
32
|
+
{ name: "packageName", type: "string" },
|
|
33
|
+
{ name: "version", type: "string" },
|
|
34
|
+
],
|
|
35
|
+
name: "requestAudit",
|
|
36
|
+
outputs: [],
|
|
37
|
+
stateMutability: "payable",
|
|
38
|
+
type: "function",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
inputs: [{ name: "key", type: "bytes32" }],
|
|
42
|
+
name: "requestAuditByKey",
|
|
43
|
+
outputs: [],
|
|
44
|
+
stateMutability: "payable",
|
|
45
|
+
type: "function",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
inputs: [],
|
|
49
|
+
name: "auditFee",
|
|
50
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
51
|
+
stateMutability: "view",
|
|
52
|
+
type: "function",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
inputs: [],
|
|
56
|
+
name: "owner",
|
|
57
|
+
outputs: [{ name: "", type: "address" }],
|
|
58
|
+
stateMutability: "view",
|
|
59
|
+
type: "function",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
inputs: [
|
|
63
|
+
{ name: "packageName", type: "string" },
|
|
64
|
+
{ name: "version", type: "string" },
|
|
65
|
+
],
|
|
66
|
+
name: "isRequested",
|
|
67
|
+
outputs: [{ name: "", type: "bool" }],
|
|
68
|
+
stateMutability: "view",
|
|
69
|
+
type: "function",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
inputs: [{ name: "", type: "bytes32" }],
|
|
73
|
+
name: "requested",
|
|
74
|
+
outputs: [{ name: "", type: "bool" }],
|
|
75
|
+
stateMutability: "view",
|
|
76
|
+
type: "function",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
inputs: [{ name: "_fee", type: "uint256" }],
|
|
80
|
+
name: "setFee",
|
|
81
|
+
outputs: [],
|
|
82
|
+
stateMutability: "nonpayable",
|
|
83
|
+
type: "function",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
inputs: [],
|
|
87
|
+
name: "withdraw",
|
|
88
|
+
outputs: [],
|
|
89
|
+
stateMutability: "nonpayable",
|
|
90
|
+
type: "function",
|
|
91
|
+
},
|
|
92
|
+
];
|
package/dist/ens-source.js
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import { createPublicClient, http } from "viem";
|
|
2
2
|
import { sepolia } from "viem/chains";
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const RPC_URLS = process.env.SEPOLIA_RPC_URL
|
|
4
|
+
? [process.env.SEPOLIA_RPC_URL]
|
|
5
|
+
: [
|
|
6
|
+
"https://ethereum-sepolia-rpc.publicnode.com",
|
|
7
|
+
"https://rpc.sepolia.org",
|
|
8
|
+
"https://sepolia.drpc.org",
|
|
9
|
+
];
|
|
10
|
+
function makeClient(url) {
|
|
11
|
+
return createPublicClient({ chain: sepolia, transport: http(url) });
|
|
12
|
+
}
|
|
13
|
+
async function getText(ensName, key) {
|
|
14
|
+
for (const url of RPC_URLS) {
|
|
15
|
+
try {
|
|
16
|
+
return await makeClient(url).getEnsText({ name: ensName, key });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
7
24
|
export class ENSAuditSource {
|
|
8
25
|
async getAudit(packageName, version) {
|
|
9
|
-
|
|
10
|
-
|
|
26
|
+
const versionSlug = version
|
|
27
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "")
|
|
29
|
+
.toLowerCase();
|
|
11
30
|
const ensName = `${versionSlug}.${packageName}.npmguard.eth`;
|
|
12
31
|
try {
|
|
13
32
|
const [verdict, score, capabilities, reportCid, sourceCid] = await Promise.all([
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
getText(ensName, "npmguard.verdict"),
|
|
34
|
+
getText(ensName, "npmguard.score"),
|
|
35
|
+
getText(ensName, "npmguard.capabilities"),
|
|
36
|
+
getText(ensName, "npmguard.report_cid"),
|
|
37
|
+
getText(ensName, "npmguard.source_cid"),
|
|
19
38
|
]);
|
|
20
39
|
if (!verdict)
|
|
21
40
|
return null;
|
|
22
41
|
return {
|
|
23
42
|
packageName,
|
|
24
43
|
version,
|
|
25
|
-
verdict: verdict,
|
|
44
|
+
verdict: verdict.toUpperCase(),
|
|
26
45
|
score: score ? parseInt(score, 10) : 0,
|
|
27
|
-
capabilities: capabilities
|
|
46
|
+
capabilities: capabilities
|
|
47
|
+
? capabilities.split(",").map((c) => c.trim()).filter(Boolean)
|
|
48
|
+
: [],
|
|
28
49
|
reportCid: reportCid ?? undefined,
|
|
29
50
|
sourceCid: sourceCid ?? undefined,
|
|
30
51
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import "dotenv/config";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npmguard-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Check npm packages against NpmGuard security audits on ENS before installing",
|
|
6
6
|
"bin": "./dist/index.js",
|
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
],
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@walletconnect/sign-client": "^2.23.9",
|
|
25
26
|
"chalk": "^5.4.0",
|
|
26
27
|
"cli-table3": "^0.6.5",
|
|
27
28
|
"commander": "^13.1.0",
|
|
29
|
+
"dotenv": "^17.4.0",
|
|
28
30
|
"ora": "^8.2.0",
|
|
31
|
+
"qrcode-terminal": "^0.12.0",
|
|
29
32
|
"viem": "^2.34.0"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|