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 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
+ ```
@@ -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 dlSpinner = ora(`Downloading verified source from IPFS (${audit.sourceCid.slice(0, 12)}...)`).start();
73
+ const ipfsUrl = `${IPFS_GATEWAY}/${audit.sourceCid}`;
74
+ console.log(chalk.green(` Installing from verified IPFS source...`));
75
+ console.log();
85
76
  try {
86
- const tmpDir = join(tmpdir(), "npmguard");
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 (err) {
111
- dlSpinner.fail("IPFS download failed");
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
  }
@@ -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
- // 1.14.0 1-14-0
10
- const versionSlug = version.replace(/\./g, "-");
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
- client.getEnsText({ name: ensName, key: "verdict" }),
15
- client.getEnsText({ name: ensName, key: "score" }),
16
- client.getEnsText({ name: ensName, key: "capabilities" }),
17
- client.getEnsText({ name: ensName, key: "reportCid" }),
18
- client.getEnsText({ name: ensName, key: "sourceCid" }),
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 ? capabilities.split(",") : [],
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npmguard-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",