npmguard-cli 0.1.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.
@@ -0,0 +1,12 @@
1
+ export interface AuditResult {
2
+ packageName: string;
3
+ version: string;
4
+ verdict: "SAFE" | "WARNING" | "CRITICAL";
5
+ score: number;
6
+ capabilities: string[];
7
+ reportCid?: string;
8
+ sourceCid?: string;
9
+ }
10
+ export interface AuditSource {
11
+ getAudit(packageName: string, version: string): Promise<AuditResult | null>;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { AuditSource } from "../audit-source.js";
2
+ export declare function checkCommand(projectPath: string, auditSource: AuditSource): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import ora from "ora";
4
+ import { scanProject } from "../scanner.js";
5
+ function verdictLabel(verdict, score) {
6
+ switch (verdict) {
7
+ case "SAFE":
8
+ return chalk.green(`✅ SAFE (${score})`);
9
+ case "WARNING":
10
+ return chalk.yellow(`⚠️ WARNING (${score})`);
11
+ case "CRITICAL":
12
+ return chalk.red(`❌ CRITICAL (${score})`);
13
+ default:
14
+ return chalk.gray("❓ UNKNOWN");
15
+ }
16
+ }
17
+ function reportLink(audit) {
18
+ if (audit.reportCid) {
19
+ return chalk.gray(` 📄 Report: https://gateway.pinata.cloud/ipfs/${audit.reportCid}`);
20
+ }
21
+ return "";
22
+ }
23
+ function npmLink(name, version) {
24
+ return chalk.gray(` 📦 https://www.npmjs.com/package/${name}/v/${version}`);
25
+ }
26
+ export async function checkCommand(projectPath, auditSource) {
27
+ const spinner = ora("Scanning project dependencies...").start();
28
+ const deps = await scanProject(projectPath);
29
+ spinner.text = `Found ${deps.length} dependencies. Checking audits...`;
30
+ const table = new Table({
31
+ head: [
32
+ chalk.bold("Package"),
33
+ chalk.bold("Installed"),
34
+ chalk.bold("Latest"),
35
+ chalk.bold("NpmGuard Verdict"),
36
+ ],
37
+ style: { head: [], border: [] },
38
+ });
39
+ let safeCount = 0;
40
+ let warningCount = 0;
41
+ let criticalCount = 0;
42
+ let notAuditedCount = 0;
43
+ const details = [];
44
+ for (const dep of deps) {
45
+ const versionToCheck = dep.latest ?? dep.installed;
46
+ const audit = await auditSource.getAudit(dep.name, versionToCheck);
47
+ let verdictCol;
48
+ if (!dep.hasUpdate) {
49
+ const currentAudit = await auditSource.getAudit(dep.name, dep.installed);
50
+ if (currentAudit) {
51
+ verdictCol = verdictLabel(currentAudit.verdict, currentAudit.score);
52
+ if (currentAudit.verdict === "SAFE")
53
+ safeCount++;
54
+ else if (currentAudit.verdict === "WARNING") {
55
+ warningCount++;
56
+ details.push(reportLink(currentAudit));
57
+ }
58
+ else if (currentAudit.verdict === "CRITICAL") {
59
+ criticalCount++;
60
+ details.push(reportLink(currentAudit));
61
+ }
62
+ }
63
+ else {
64
+ verdictCol = chalk.gray("❓ NOT AUDITED");
65
+ notAuditedCount++;
66
+ details.push(npmLink(dep.name, dep.installed));
67
+ }
68
+ }
69
+ else if (audit) {
70
+ verdictCol = verdictLabel(audit.verdict, audit.score);
71
+ if (audit.verdict === "SAFE")
72
+ safeCount++;
73
+ else if (audit.verdict === "WARNING") {
74
+ warningCount++;
75
+ details.push(reportLink(audit));
76
+ }
77
+ else if (audit.verdict === "CRITICAL") {
78
+ criticalCount++;
79
+ details.push(reportLink(audit));
80
+ }
81
+ }
82
+ else {
83
+ verdictCol = chalk.gray("❓ NOT AUDITED");
84
+ notAuditedCount++;
85
+ details.push(npmLink(dep.name, versionToCheck));
86
+ }
87
+ table.push([
88
+ dep.name,
89
+ dep.installed,
90
+ dep.latest ?? dep.installed,
91
+ verdictCol,
92
+ ]);
93
+ }
94
+ spinner.stop();
95
+ console.log();
96
+ console.log(chalk.bold("📦 NpmGuard Dependency Audit"));
97
+ console.log();
98
+ console.log(table.toString());
99
+ console.log();
100
+ // Summary
101
+ const parts = [];
102
+ if (safeCount > 0)
103
+ parts.push(chalk.green(`${safeCount} safe`));
104
+ if (warningCount > 0)
105
+ parts.push(chalk.yellow(`${warningCount} warnings`));
106
+ if (criticalCount > 0)
107
+ parts.push(chalk.red(`${criticalCount} critical`));
108
+ if (notAuditedCount > 0)
109
+ parts.push(chalk.gray(`${notAuditedCount} not audited`));
110
+ console.log(` ${parts.join(" · ")}`);
111
+ // Show detail links
112
+ if (details.length > 0) {
113
+ console.log();
114
+ for (const d of details) {
115
+ if (d)
116
+ console.log(d);
117
+ }
118
+ }
119
+ if (criticalCount > 0) {
120
+ console.log();
121
+ console.log(chalk.red.bold(` ⛔ ${criticalCount} package(s) flagged as CRITICAL — do not update without reviewing the report.`));
122
+ }
123
+ console.log();
124
+ }
@@ -0,0 +1,2 @@
1
+ import type { AuditSource } from "../audit-source.js";
2
+ export declare function installCommand(packageSpec: string, auditSource: AuditSource): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { execSync } from "node:child_process";
4
+ export async function installCommand(packageSpec, auditSource) {
5
+ // Parse package@version or just package
6
+ const atIndex = packageSpec.lastIndexOf("@");
7
+ let packageName;
8
+ let requestedVersion = null;
9
+ if (atIndex > 0) {
10
+ packageName = packageSpec.slice(0, atIndex);
11
+ requestedVersion = packageSpec.slice(atIndex + 1);
12
+ }
13
+ else {
14
+ packageName = packageSpec;
15
+ }
16
+ const spinner = ora(`Checking audit for ${packageName}...`).start();
17
+ // Get latest version from npm if not specified
18
+ if (!requestedVersion) {
19
+ try {
20
+ const resp = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
21
+ if (resp.ok) {
22
+ const data = await resp.json();
23
+ requestedVersion = data.version;
24
+ }
25
+ }
26
+ catch {
27
+ // continue without version
28
+ }
29
+ }
30
+ if (!requestedVersion) {
31
+ spinner.fail("Could not determine package version.");
32
+ return;
33
+ }
34
+ const audit = await auditSource.getAudit(packageName, requestedVersion);
35
+ spinner.stop();
36
+ if (!audit) {
37
+ console.log();
38
+ console.log(chalk.gray(` ❓ ${packageName}@${requestedVersion} has not been audited by NpmGuard.`));
39
+ console.log(chalk.gray(" Proceeding with standard npm install..."));
40
+ console.log();
41
+ execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
42
+ return;
43
+ }
44
+ console.log();
45
+ console.log(chalk.bold(` 📦 ${packageName}@${requestedVersion}`));
46
+ console.log();
47
+ if (audit.verdict === "SAFE") {
48
+ console.log(chalk.green(` ✅ SAFE (score: ${audit.score})`));
49
+ if (audit.capabilities.length > 0) {
50
+ console.log(chalk.gray(` Capabilities: ${audit.capabilities.join(", ")}`));
51
+ }
52
+ console.log();
53
+ execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
54
+ }
55
+ else if (audit.verdict === "WARNING") {
56
+ console.log(chalk.yellow(` ⚠️ WARNING (score: ${audit.score})`));
57
+ console.log(chalk.yellow(` Capabilities: ${audit.capabilities.join(", ")}`));
58
+ if (audit.reportCid) {
59
+ console.log(chalk.gray(` Report: https://gateway.pinata.cloud/ipfs/${audit.reportCid}`));
60
+ }
61
+ console.log();
62
+ console.log(chalk.yellow(" Installing with warning..."));
63
+ console.log();
64
+ execSync(`npm install ${packageSpec}`, { stdio: "inherit" });
65
+ }
66
+ else if (audit.verdict === "CRITICAL") {
67
+ console.log(chalk.red(` ❌ CRITICAL (score: ${audit.score})`));
68
+ console.log(chalk.red(` Capabilities: ${audit.capabilities.join(", ")}`));
69
+ if (audit.reportCid) {
70
+ console.log(chalk.gray(` Report: https://gateway.pinata.cloud/ipfs/${audit.reportCid}`));
71
+ }
72
+ console.log();
73
+ console.log(chalk.red.bold(" ⛔ Installation blocked. This package has critical security issues."));
74
+ console.log(chalk.gray(" Use --force to install anyway: npmguard install --force " +
75
+ packageSpec));
76
+ console.log();
77
+ }
78
+ }
@@ -0,0 +1,4 @@
1
+ import type { AuditSource, AuditResult } from "./audit-source.js";
2
+ export declare class ENSAuditSource implements AuditSource {
3
+ getAudit(packageName: string, version: string): Promise<AuditResult | null>;
4
+ }
@@ -0,0 +1,36 @@
1
+ import { createPublicClient, http } from "viem";
2
+ import { sepolia } from "viem/chains";
3
+ const client = createPublicClient({
4
+ chain: sepolia,
5
+ transport: http("https://ethereum-sepolia-rpc.publicnode.com"),
6
+ });
7
+ export class ENSAuditSource {
8
+ async getAudit(packageName, version) {
9
+ // 1.14.0 → 1-14-0
10
+ const versionSlug = version.replace(/\./g, "-");
11
+ const ensName = `${versionSlug}.${packageName}.npmguard.eth`;
12
+ try {
13
+ 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" }),
19
+ ]);
20
+ if (!verdict)
21
+ return null;
22
+ return {
23
+ packageName,
24
+ version,
25
+ verdict: verdict,
26
+ score: score ? parseInt(score, 10) : 0,
27
+ capabilities: capabilities ? capabilities.split(",") : [],
28
+ reportCid: reportCid ?? undefined,
29
+ sourceCid: sourceCid ?? undefined,
30
+ };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { resolve } from "node:path";
4
+ import { ENSAuditSource } from "./ens-source.js";
5
+ import { MockAuditSource } from "./mock-source.js";
6
+ import { checkCommand } from "./commands/check.js";
7
+ import { installCommand } from "./commands/install.js";
8
+ const program = new Command();
9
+ program
10
+ .name("npmguard")
11
+ .description("Check npm packages against NpmGuard security audits")
12
+ .version("0.1.0");
13
+ program
14
+ .command("check")
15
+ .description("Scan project dependencies and check audit status for available updates")
16
+ .option("-p, --path <path>", "Path to project directory", ".")
17
+ .option("--mock", "Use mock data instead of ENS")
18
+ .action(async (opts) => {
19
+ const auditSource = opts.mock
20
+ ? new MockAuditSource()
21
+ : new ENSAuditSource();
22
+ const projectPath = resolve(opts.path);
23
+ await checkCommand(projectPath, auditSource);
24
+ });
25
+ program
26
+ .command("install <package>")
27
+ .description("Install a package after checking its NpmGuard audit status")
28
+ .option("--force", "Install even if flagged as CRITICAL")
29
+ .option("--mock", "Use mock data instead of ENS")
30
+ .action(async (pkg, opts) => {
31
+ const auditSource = opts.mock
32
+ ? new MockAuditSource()
33
+ : new ENSAuditSource();
34
+ await installCommand(pkg, auditSource);
35
+ });
36
+ program.parse();
@@ -0,0 +1,4 @@
1
+ import type { AuditSource, AuditResult } from "./audit-source.js";
2
+ export declare class MockAuditSource implements AuditSource {
3
+ getAudit(packageName: string, version: string): Promise<AuditResult | null>;
4
+ }
@@ -0,0 +1,50 @@
1
+ // Mock data for demo — simulates what ENS would return
2
+ const MOCK_AUDITS = {
3
+ "axios@1.14.0": {
4
+ packageName: "axios",
5
+ version: "1.14.0",
6
+ verdict: "SAFE",
7
+ score: 92,
8
+ capabilities: ["network"],
9
+ reportCid: "bafkreia3dgrfewkj6q4sdpqrbxcfuxa47d3ku4uzbauqdk4qo7gok3geoi",
10
+ sourceCid: "bafybeif372guv6lwfzdx622uyqmtk3bkxuhsozd6j5bmzxgqohe4ste77q",
11
+ },
12
+ "axios@1.13.0": {
13
+ packageName: "axios",
14
+ version: "1.13.0",
15
+ verdict: "SAFE",
16
+ score: 90,
17
+ capabilities: ["network"],
18
+ reportCid: "bafkreia3dgrfewkj6q4sdpqrbxcfuxa47d3ku4uzbauqdk4qo7gok3geoi",
19
+ },
20
+ "lodash@4.18.1": {
21
+ packageName: "lodash",
22
+ version: "4.18.1",
23
+ verdict: "WARNING",
24
+ score: 65,
25
+ capabilities: ["network", "filesystem"],
26
+ reportCid: "QmT5NvUtoM5nWFfrQdVrFtvGfKFmG7AHE8P34isapyhCxX", // mock
27
+ },
28
+ "express@5.2.1": {
29
+ packageName: "express",
30
+ version: "5.2.1",
31
+ verdict: "CRITICAL",
32
+ score: 12,
33
+ capabilities: ["network", "filesystem", "process_spawn", "binary_download"],
34
+ reportCid: "QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ", // mock
35
+ },
36
+ "chalk@5.6.2": {
37
+ packageName: "chalk",
38
+ version: "5.6.2",
39
+ verdict: "SAFE",
40
+ score: 98,
41
+ capabilities: [],
42
+ reportCid: "QmRf22bZar3WKmojipms22PkXH1MZGmvsqzQtuSvQE3uhm", // mock
43
+ },
44
+ };
45
+ export class MockAuditSource {
46
+ async getAudit(packageName, version) {
47
+ const key = `${packageName}@${version}`;
48
+ return MOCK_AUDITS[key] ?? null;
49
+ }
50
+ }
@@ -0,0 +1,7 @@
1
+ export interface PackageDep {
2
+ name: string;
3
+ installed: string;
4
+ latest: string | null;
5
+ hasUpdate: boolean;
6
+ }
7
+ export declare function scanProject(projectPath: string): Promise<PackageDep[]>;
@@ -0,0 +1,35 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ export async function scanProject(projectPath) {
4
+ const pkgPath = join(projectPath, "package.json");
5
+ const raw = await readFile(pkgPath, "utf-8");
6
+ const pkg = JSON.parse(raw);
7
+ const allDeps = {
8
+ ...pkg.dependencies,
9
+ ...pkg.devDependencies,
10
+ };
11
+ const results = [];
12
+ for (const [name, versionRange] of Object.entries(allDeps)) {
13
+ // Strip ^, ~, >= etc. to get the installed version
14
+ const installed = versionRange.replace(/^[\^~>=<]*/, "");
15
+ // Fetch latest from npm registry
16
+ let latest = null;
17
+ try {
18
+ const resp = await fetch(`https://registry.npmjs.org/${name}/latest`);
19
+ if (resp.ok) {
20
+ const data = await resp.json();
21
+ latest = data.version;
22
+ }
23
+ }
24
+ catch {
25
+ // Network error — skip
26
+ }
27
+ results.push({
28
+ name,
29
+ installed,
30
+ latest,
31
+ hasUpdate: latest !== null && latest !== installed,
32
+ });
33
+ }
34
+ return results;
35
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "npmguard-cli",
3
+ "version": "0.1.0",
4
+ "description": "Check npm packages against NpmGuard security audits on ENS before installing",
5
+ "bin": {
6
+ "npmguard-cli": "./dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "prepublishOnly": "npm run build",
14
+ "dev": "tsx src/index.ts"
15
+ },
16
+ "keywords": [
17
+ "npm",
18
+ "security",
19
+ "audit",
20
+ "ens",
21
+ "ipfs",
22
+ "supply-chain"
23
+ ],
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "chalk": "^5.4.0",
27
+ "cli-table3": "^0.6.5",
28
+ "commander": "^13.1.0",
29
+ "ora": "^8.2.0",
30
+ "viem": "^2.34.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "tsx": "^4.19.0",
35
+ "typescript": "^5.9.0"
36
+ }
37
+ }