npmguard-cli 0.3.1 → 0.4.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 +6 -39
- package/dist/ens-source.js +21 -9
- package/package.json +1 -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,18 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { readFile, writeFile, unlink, mkdir } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
4
|
const IPFS_GATEWAY = "https://gateway.pinata.cloud/ipfs";
|
|
8
|
-
async function downloadFromIPFS(cid, dest) {
|
|
9
|
-
const resp = await fetch(`${IPFS_GATEWAY}/${cid}`);
|
|
10
|
-
if (!resp.ok) {
|
|
11
|
-
throw new Error(`IPFS download failed: ${resp.status}`);
|
|
12
|
-
}
|
|
13
|
-
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
14
|
-
await writeFile(dest, buffer);
|
|
15
|
-
}
|
|
16
5
|
export async function installCommand(packageSpec, auditSource, force = false) {
|
|
17
6
|
const atIndex = packageSpec.lastIndexOf("@");
|
|
18
7
|
let packageName;
|
|
@@ -81,36 +70,14 @@ export async function installCommand(packageSpec, auditSource, force = false) {
|
|
|
81
70
|
}
|
|
82
71
|
// Install from IPFS if sourceCid is available
|
|
83
72
|
if (audit.sourceCid) {
|
|
84
|
-
const
|
|
73
|
+
const ipfsUrl = `${IPFS_GATEWAY}/${audit.sourceCid}`;
|
|
74
|
+
console.log(chalk.green(` Installing from verified IPFS source...`));
|
|
75
|
+
console.log();
|
|
85
76
|
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(() => { });
|
|
77
|
+
execSync(`npm install ${ipfsUrl}`, { stdio: "inherit" });
|
|
109
78
|
}
|
|
110
|
-
catch
|
|
111
|
-
|
|
112
|
-
console.log(chalk.yellow(" Falling back to npm install..."));
|
|
113
|
-
console.log();
|
|
79
|
+
catch {
|
|
80
|
+
console.log(chalk.yellow(" IPFS install failed, falling back to npm..."));
|
|
114
81
|
execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
|
|
115
82
|
}
|
|
116
83
|
}
|
package/dist/ens-source.js
CHANGED
|
@@ -4,27 +4,39 @@ const client = createPublicClient({
|
|
|
4
4
|
chain: sepolia,
|
|
5
5
|
transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
|
|
6
6
|
});
|
|
7
|
+
async function getText(ensName, key) {
|
|
8
|
+
try {
|
|
9
|
+
return await client.getEnsText({ name: ensName, key });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
export class ENSAuditSource {
|
|
8
16
|
async getAudit(packageName, version) {
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
const versionSlug = version
|
|
18
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "")
|
|
20
|
+
.toLowerCase();
|
|
11
21
|
const ensName = `${versionSlug}.${packageName}.npmguard.eth`;
|
|
12
22
|
try {
|
|
13
23
|
const [verdict, score, capabilities, reportCid, sourceCid] = await Promise.all([
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
getText(ensName, "npmguard.verdict"),
|
|
25
|
+
getText(ensName, "npmguard.score"),
|
|
26
|
+
getText(ensName, "npmguard.capabilities"),
|
|
27
|
+
getText(ensName, "npmguard.report_cid"),
|
|
28
|
+
getText(ensName, "npmguard.source_cid"),
|
|
19
29
|
]);
|
|
20
30
|
if (!verdict)
|
|
21
31
|
return null;
|
|
22
32
|
return {
|
|
23
33
|
packageName,
|
|
24
34
|
version,
|
|
25
|
-
verdict: verdict,
|
|
35
|
+
verdict: verdict.toUpperCase(),
|
|
26
36
|
score: score ? parseInt(score, 10) : 0,
|
|
27
|
-
capabilities: capabilities
|
|
37
|
+
capabilities: capabilities
|
|
38
|
+
? capabilities.split(",").map((c) => c.trim()).filter(Boolean)
|
|
39
|
+
: [],
|
|
28
40
|
reportCid: reportCid ?? undefined,
|
|
29
41
|
sourceCid: sourceCid ?? undefined,
|
|
30
42
|
};
|