install-guard 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarthak Kumar Sahoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,27 +1,56 @@
1
+ ![npm](https://img.shields.io/npm/v/install-guard)
2
+ ![downloads](https://img.shields.io/npm/dw/install-guard)
3
+ ![license](https://img.shields.io/npm/l/install-guard)
4
+
1
5
  # 🚨 Should You Trust That npm Package Before Installing?
2
6
 
3
- **install-guard** analyzes npm packages and tells you if they are safe to install — before you install them.
7
+ **install-guard** analyzes npm packages for security risks and tells you if they're safe — **before** you install them.
4
8
 
5
9
  ---
6
10
 
7
11
  <details>
8
- <summary>Example</summary>
12
+ <summary>📦 See it in action</summary>
9
13
 
10
14
  ```bash
11
- npx install-guard install some-random-lib
15
+ $ npx install-guard install some-random-lib
12
16
  ```
13
17
 
14
18
  ```
15
- 📦 Analyzing some-random-lib...
19
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20
+ 📦 some-random-lib v0.1.3
21
+ A random utility library
22
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
+
24
+ Risk Score: 8/10 🚨 High Risk
25
+ ████████░░
26
+
27
+ ─────────────────────────────────────────────────
28
+
29
+ 📊 Package Info
16
30
 
17
- Downloads (weekly): 120
18
- Risk Score: 8/10 🚨 High Risk
31
+ Downloads (weekly) 120
32
+ Maintainers 1
33
+ License Unknown
34
+ Last Published 3 days ago
35
+ Dependencies 14
36
+ Versions 2
19
37
 
20
- ⚠ Very low downloads
21
- ⚠ Uses install scripts
22
- ⚠ Recently published
38
+ ─────────────────────────────────────────────────
23
39
 
24
- Do you still want to install this package? (y/n)
40
+ 🔍 Security Checks
41
+
42
+ ✘ Downloads: Very low (120/week)
43
+ ✔ Last Updated: Recently updated
44
+ ✘ Install Scripts: Has install/postinstall scripts
45
+ ⚠ Maintainers: Single maintainer
46
+ ✘ License: No license specified
47
+ ⚠ Repository: No repository URL
48
+ ✘ Package Age: Published less than 30 days ago
49
+ ✔ Dependencies: 14 direct dependencies
50
+
51
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52
+
53
+ ⚠ High risk package. Continue install? (y/n):
25
54
  ```
26
55
 
27
56
  </details>
@@ -32,9 +61,9 @@ Risk Score: 8/10 🚨 High Risk
32
61
 
33
62
  Installing npm packages blindly is risky.
34
63
 
35
- - Malicious packages are published daily
64
+ - Malicious packages are published **daily**
36
65
  - Popular packages get compromised
37
- - `npm audit` only checks known vulnerabilities — not trust
66
+ - `npm audit` only checks known vulnerabilities — **not trust**
38
67
 
39
68
  You shouldn't have to guess if a package is safe.
40
69
 
@@ -42,44 +71,56 @@ You shouldn't have to guess if a package is safe.
42
71
 
43
72
  ## 🛡️ The Solution
44
73
 
45
- install-guard gives you a **risk score before you install anything**.
74
+ install-guard gives you a **risk score before you install anything**, powered by 10+ security checks.
46
75
 
47
76
  ---
48
77
 
49
78
  ## ⚡ Quick Start
50
79
 
51
- Check a package:
80
+ Analyze a package:
52
81
 
53
82
  ```bash
54
83
  npx install-guard axios
55
84
  ```
56
85
 
57
- Safely install a package:
86
+ Safely install with a risk check:
58
87
 
59
88
  ```bash
60
89
  npx install-guard install axios
61
90
  ```
62
91
 
63
- Scan your project:
92
+ Scan your entire project:
64
93
 
65
94
  ```bash
66
95
  npx install-guard scan
67
96
  ```
68
97
 
98
+ Scan with detailed output per package:
99
+
100
+ ```bash
101
+ npx install-guard scan --verbose
102
+ ```
103
+
69
104
  ---
70
105
 
71
106
  ## 🧠 How Risk Score Works
72
107
 
73
- install-guard analyzes:
74
-
75
- - 📉 Weekly downloads (popularity)
76
- - 🕒 Last update time
77
- - ⚠ Install/postinstall scripts
78
- - 📦 Version activity
108
+ install-guard runs **10+ security checks** on every package:
79
109
 
80
- Each factor contributes to a **risk score (0–10)**.
110
+ | Check | What it detects |
111
+ |-------|----------------|
112
+ | 📉 **Downloads** | Low popularity = higher risk |
113
+ | 🕒 **Last Updated** | Abandoned packages |
114
+ | ⚠ **Install Scripts** | `preinstall` / `postinstall` hooks (common attack vector) |
115
+ | 👥 **Maintainers** | Single or no maintainers |
116
+ | 📜 **License** | Missing or non-permissive licenses |
117
+ | 🔗 **Repository** | No source code link |
118
+ | 📅 **Package Age** | Brand new packages (< 30 days) |
119
+ | 📦 **Dependencies** | High dependency count |
120
+ | 🚫 **Deprecated** | Flagged as deprecated on npm |
121
+ | 🧠 **Typosquatting** | Names suspiciously similar to popular packages |
81
122
 
82
- Lower score = safer package.
123
+ Each factor contributes to a **risk score (0–10)**. Lower = safer.
83
124
 
84
125
  ---
85
126
 
@@ -88,44 +129,88 @@ Lower score = safer package.
88
129
  ### ✅ Safe package
89
130
 
90
131
  ```
91
- axios
92
- Downloads: 20,000,000+
93
- Risk: 1/10
132
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
133
+ 📦 axios v1.7.2
134
+
135
+ Risk Score: 1/10 ✅ Low Risk
136
+ █░░░░░░░░░
137
+
138
+ 🔍 Security Checks
139
+ ✔ Downloads: 44,392,817/week
140
+ ✔ Last Updated: Recently updated
141
+ ✔ Install Scripts: No install scripts
142
+ ✔ Maintainers: 3 maintainers
143
+ ✔ License: MIT
144
+ ✔ Repository: Has repository link
145
+ ✔ Package Age: 9+ year(s) old
146
+ ✔ Dependencies: 8 direct dependencies
147
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94
148
  ```
95
149
 
96
- ---
97
-
98
150
  ### 🚨 Risky package
99
151
 
100
152
  ```
101
- some-lib
102
- Downloads: 200
103
- Risk: 8/10
153
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
154
+ 📦 some-lib v0.1.0
155
+
156
+ Risk Score: 8/10 🚨 High Risk
157
+ ████████░░
158
+
159
+ 🔍 Security Checks
160
+ ✘ Downloads: Very low (83/week)
161
+ ✘ Install Scripts: Has install/postinstall scripts
162
+ ✘ License: No license specified
163
+ ⚠ Maintainers: Single maintainer
164
+ ⚠ Repository: No repository URL
165
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
166
+ ```
104
167
 
105
- Low downloads
106
- ⚠ Uses install scripts
168
+ ### 📋 Project scan
169
+
170
+ ```
171
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
172
+ 📋 Dependency Scan Summary
173
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
174
+
175
+ Package Risk Status
176
+ ────────────────────────────────────────────
177
+ some-lib 8/10 🚨 High Risk
178
+ old-utils 5/10 ⚠ Medium
179
+ express 0/10 ✅ Safe
180
+ axios 1/10 ✅ Safe
181
+
182
+ ─────────────────────────────────────────────────
183
+ Summary: 2 safe · 1 medium · 1 high risk
184
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107
185
  ```
108
186
 
109
187
  ---
110
188
 
111
189
  ## ✨ Features
112
190
 
113
- - 🔍 Analyze any npm package instantly
114
- - Risk scoring (0–10)
115
- - 🛑 Block risky installs
116
- - 📦 Project-wide dependency scan
117
- - Zero setup (works with npx)
191
+ - 🔍 **Deep analysis** 10+ security checks per package
192
+ - 🧠 **Typosquatting detection** — catches lookalike package names
193
+ - **Risk scoring (0–10)** — instant safety assessment
194
+ - 🛑 **Block risky installs** — prompts before installing medium/high risk packages
195
+ - 📋 **Project-wide scan** audit all dependencies in one command
196
+ - 📊 **Summary table** — sorted by risk with safe/medium/high breakdown
197
+ - ⚡ **Zero setup** — works instantly with `npx`
198
+ - 🎨 **Beautiful CLI output** — color-coded, easy to read
118
199
 
119
200
  ---
120
201
 
121
202
  ## 🤔 Why not npm audit?
122
203
 
123
204
  | Feature | npm audit | install-guard |
124
- |-----------------------|-----------|------------|
125
- | Known vulnerabilities | ✅ | ✅ |
126
- | Trust analysis | ❌ | ✅ |
127
- | Pre-install check | ❌ | ✅ |
128
- | Install blocking | ❌ | ✅ |
205
+ |-----------------------|-----------|---------------|
206
+ | Known vulnerabilities | ✅ | ✅ |
207
+ | Trust analysis | ❌ | ✅ |
208
+ | Pre-install check | ❌ | ✅ |
209
+ | Install blocking | ❌ | ✅ |
210
+ | Typosquatting check | ❌ | ✅ |
211
+ | License analysis | ❌ | ✅ |
212
+ | Maintainer check | ❌ | ✅ |
213
+ | Package age check | ❌ | ✅ |
129
214
 
130
215
  ---
131
216
 
@@ -140,9 +225,24 @@ npm install -g install-guard
140
225
  ## 🔮 Roadmap
141
226
 
142
227
  - 🔍 GitHub activity analysis
143
- - 🧠 Typosquatting detection
228
+ - 🧠 Advanced typosquatting (permutations, homoglyphs)
144
229
  - 📊 Dependency tree visualization
145
- - 🔌 CI/CD integration
230
+ - 🔌 CI/CD integration (exit codes for pipelines)
231
+ - 🏷️ `.install-guardrc` config for custom thresholds
232
+ - 📝 JSON/CSV report export
233
+
234
+ ---
235
+
236
+ ## 🤝 Contributing
237
+
238
+ PRs welcome! Let's make npm safer together.
239
+
240
+ ---
241
+
242
+ ## ⭐ Support
243
+
244
+ If you find this useful, consider giving it a star ⭐
245
+ It helps others discover the project!
146
246
 
147
247
  ---
148
248
 
package/bin/cli.js CHANGED
@@ -8,13 +8,14 @@ const program = new Command();
8
8
 
9
9
  program
10
10
  .name("install-guard")
11
- .description("Check npm package risk before installing");
11
+ .description("Analyze npm packages for security risks before installing")
12
+ .version("2.0.0");
12
13
 
13
14
  program
14
15
  .argument("[package]", "package name to analyze")
15
16
  .action(async (pkg) => {
16
17
  if (!pkg) {
17
- console.log("Please provide a package name");
18
+ program.help();
18
19
  return;
19
20
  }
20
21
  await analyzePackage(pkg);
@@ -22,14 +23,18 @@ program
22
23
 
23
24
  program
24
25
  .command("scan")
25
- .description("Scan current project dependencies")
26
- .action(scanProject);
27
-
26
+ .description("Scan all project dependencies for risks")
27
+ .option("-v, --verbose", "Show detailed analysis for each package")
28
+ .action(async (opts) => {
29
+ await scanProject({ verbose: opts.verbose });
30
+ });
31
+
28
32
  program
29
- .command("install <pkg>")
30
- .description("Analyze and install package safely")
31
- .action(async (pkg) => {
32
- const { analyzeAndPrompt } = await import("../src/install.js");
33
- await analyzeAndPrompt(pkg);
34
- });
33
+ .command("install <pkg>")
34
+ .description("Analyze and safely install a package")
35
+ .action(async (pkg) => {
36
+ const { analyzeAndPrompt } = await import("../src/install.js");
37
+ await analyzeAndPrompt(pkg);
38
+ });
39
+
35
40
  program.parse();
package/package.json CHANGED
@@ -1,22 +1,27 @@
1
1
  {
2
2
  "name": "install-guard",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "main": "index.js",
5
5
  "bin": {
6
6
  "install-guard": "./bin/cli.js"
7
7
  },
8
-
9
8
  "type": "module",
10
9
  "scripts": {
11
10
  "test": "echo \"Error: no test specified\" && exit 1"
12
11
  },
13
12
  "author": "Sarthak Kumar Sahoo",
14
13
  "license": "MIT",
15
- "description": "Check if an npm package is safe before installing",
14
+ "description": "Analyze npm packages for security risks before installing",
16
15
  "dependencies": {
17
16
  "chalk": "^5.6.2",
18
17
  "commander": "^14.0.3",
19
18
  "ora": "^9.3.0"
20
19
  },
21
- "keywords": ["npm", "security", "cli", "dependency", "audit"]
20
+ "keywords": [
21
+ "npm",
22
+ "security",
23
+ "cli",
24
+ "dependency",
25
+ "audit"
26
+ ]
22
27
  }
package/src/analyze.js CHANGED
@@ -1,34 +1,21 @@
1
- import chalk from "chalk";
2
1
  import ora from "ora";
3
2
  import { getPackageData } from "./npm.js";
4
3
  import { calculateRisk } from "./score.js";
4
+ import { formatAnalysis } from "./format.js";
5
5
 
6
6
  export async function analyze(pkgName) {
7
7
  const spinner = ora(`Analyzing ${pkgName}...`).start();
8
8
 
9
9
  try {
10
10
  const data = await getPackageData(pkgName);
11
- const { score, warnings } = calculateRisk(data);
11
+ const result = calculateRisk(data);
12
12
 
13
13
  spinner.stop();
14
+ console.log(formatAnalysis(data, result));
14
15
 
15
- console.log(chalk.bold(`\n📦 ${data.name}`));
16
- console.log(`Version: ${data.version}`);
17
- console.log(`Downloads (weekly): ${data.downloads.toLocaleString()}`);
18
- console.log(`Risk Score: ${score}/10`);
19
-
20
- if (score <= 3) {
21
- console.log(chalk.green("✅ Low risk"));
22
- } else if (score <= 6) {
23
- console.log(chalk.yellow("⚠ Medium risk"));
24
- } else {
25
- console.log(chalk.red("🚨 High risk"));
26
- }
27
-
28
- warnings.forEach((w) => {
29
- console.log(chalk.yellow(`⚠ ${w}`));
30
- });
16
+ return { data, result };
31
17
  } catch (err) {
32
- spinner.fail("Failed to fetch package");
18
+ spinner.fail(`Failed to analyze "${pkgName}": ${err.message}`);
19
+ return null;
33
20
  }
34
21
  }
package/src/format.js ADDED
@@ -0,0 +1,202 @@
1
+ import chalk from "chalk";
2
+
3
+ const SEPARATOR = chalk.gray("━".repeat(50));
4
+ const THIN_SEP = chalk.gray("─".repeat(50));
5
+
6
+ function riskBar(score) {
7
+ const filled = score;
8
+ const empty = 10 - score;
9
+ const color =
10
+ score <= 3 ? chalk.green : score <= 6 ? chalk.yellow : chalk.red;
11
+ return color("█".repeat(filled)) + chalk.gray("░".repeat(empty));
12
+ }
13
+
14
+ function riskLabel(level) {
15
+ switch (level) {
16
+ case "low":
17
+ return chalk.green.bold("✅ Low Risk");
18
+ case "medium":
19
+ return chalk.yellow.bold("⚠ Medium Risk");
20
+ case "high":
21
+ return chalk.red.bold("🚨 High Risk");
22
+ }
23
+ }
24
+
25
+ function checkIcon(status) {
26
+ switch (status) {
27
+ case "pass":
28
+ return chalk.green("✔");
29
+ case "warn":
30
+ return chalk.yellow("⚠");
31
+ case "fail":
32
+ return chalk.red("✘");
33
+ }
34
+ }
35
+
36
+ function timeAgo(dateStr) {
37
+ if (!dateStr) return "Unknown";
38
+ const diff = Date.now() - new Date(dateStr).getTime();
39
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
40
+ if (days < 1) return "today";
41
+ if (days === 1) return "1 day ago";
42
+ if (days < 30) return `${days} days ago`;
43
+ const months = Math.floor(days / 30);
44
+ if (months === 1) return "1 month ago";
45
+ if (months < 12) return `${months} months ago`;
46
+ const years = Math.floor(days / 365);
47
+ if (years === 1) return "1 year ago";
48
+ return `${years} years ago`;
49
+ }
50
+
51
+ export function formatAnalysis(data, result) {
52
+ const lines = [];
53
+
54
+ lines.push("");
55
+ lines.push(SEPARATOR);
56
+ lines.push(
57
+ chalk.bold(` 📦 ${data.name} `) + chalk.gray(`v${data.version}`)
58
+ );
59
+ if (data.description) {
60
+ lines.push(chalk.gray(` ${data.description.slice(0, 70)}`));
61
+ }
62
+ lines.push(SEPARATOR);
63
+
64
+ // Risk score
65
+ lines.push("");
66
+ lines.push(
67
+ ` Risk Score: ${chalk.bold(`${result.score}/10`)} ${riskLabel(result.level)}`
68
+ );
69
+ lines.push(` ${riskBar(result.score)}`);
70
+
71
+ lines.push("");
72
+ lines.push(THIN_SEP);
73
+
74
+ // Package info
75
+ lines.push("");
76
+ lines.push(chalk.bold(" 📊 Package Info"));
77
+ lines.push("");
78
+
79
+ const info = [
80
+ ["Downloads (weekly)", data.downloads.toLocaleString()],
81
+ ["Maintainers", String(data.maintainers.length)],
82
+ [
83
+ "License",
84
+ typeof data.license === "string"
85
+ ? data.license
86
+ : data.license?.type || "Unknown",
87
+ ],
88
+ ["Last Published", timeAgo(data.lastPublished)],
89
+ ["Dependencies", String(data.dependencies)],
90
+ ["Versions", String(data.totalVersions)],
91
+ ];
92
+
93
+ for (const [label, value] of info) {
94
+ lines.push(
95
+ ` ${chalk.gray(label.padEnd(22))} ${chalk.white(value)}`
96
+ );
97
+ }
98
+
99
+ if (data.deprecated) {
100
+ lines.push("");
101
+ lines.push(
102
+ chalk.red.bold(
103
+ ` ⚠ DEPRECATED: ${typeof data.deprecated === "string" ? data.deprecated : "This package is deprecated"}`
104
+ )
105
+ );
106
+ }
107
+
108
+ lines.push("");
109
+ lines.push(THIN_SEP);
110
+
111
+ // Security checks
112
+ lines.push("");
113
+ lines.push(chalk.bold(" 🔍 Security Checks"));
114
+ lines.push("");
115
+
116
+ for (const check of result.checks) {
117
+ const icon = checkIcon(check.status);
118
+ const detail =
119
+ check.status === "pass"
120
+ ? chalk.green(check.detail)
121
+ : check.status === "warn"
122
+ ? chalk.yellow(check.detail)
123
+ : chalk.red(check.detail);
124
+ lines.push(` ${icon} ${chalk.gray(check.label + ":")} ${detail}`);
125
+ }
126
+
127
+ lines.push("");
128
+ lines.push(SEPARATOR);
129
+ lines.push("");
130
+
131
+ return lines.join("\n");
132
+ }
133
+
134
+ export function formatScanSummary(results) {
135
+ const lines = [];
136
+
137
+ results.sort((a, b) => b.result.score - a.result.score);
138
+
139
+ lines.push("");
140
+ lines.push(SEPARATOR);
141
+ lines.push(chalk.bold(" 📋 Dependency Scan Summary"));
142
+ lines.push(SEPARATOR);
143
+ lines.push("");
144
+
145
+ const nameWidth = Math.max(
146
+ 20,
147
+ ...results.map((r) => r.data.name.length + 2)
148
+ );
149
+
150
+ lines.push(
151
+ chalk.gray(
152
+ " " +
153
+ "Package".padEnd(nameWidth) +
154
+ "Risk".padEnd(12) +
155
+ "Status"
156
+ )
157
+ );
158
+ lines.push(chalk.gray(" " + "─".repeat(nameWidth + 26)));
159
+
160
+ for (const { data, result } of results) {
161
+ const name = data.name.padEnd(nameWidth);
162
+ const risk = `${result.score}/10`.padEnd(12);
163
+ let status;
164
+ switch (result.level) {
165
+ case "low":
166
+ status = chalk.green("✅ Safe");
167
+ break;
168
+ case "medium":
169
+ status = chalk.yellow("⚠ Medium");
170
+ break;
171
+ case "high":
172
+ status = chalk.red("🚨 High Risk");
173
+ break;
174
+ }
175
+
176
+ const colorFn =
177
+ result.level === "high"
178
+ ? chalk.red
179
+ : result.level === "medium"
180
+ ? chalk.yellow
181
+ : chalk.white;
182
+ lines.push(` ${colorFn(name)}${risk}${status}`);
183
+ }
184
+
185
+ lines.push("");
186
+ lines.push(THIN_SEP);
187
+
188
+ const safe = results.filter((r) => r.result.level === "low").length;
189
+ const medium = results.filter(
190
+ (r) => r.result.level === "medium"
191
+ ).length;
192
+ const high = results.filter((r) => r.result.level === "high").length;
193
+
194
+ lines.push(
195
+ ` ${chalk.bold("Summary:")} ${chalk.green(`${safe} safe`)} · ${chalk.yellow(`${medium} medium`)} · ${chalk.red(`${high} high risk`)}`
196
+ );
197
+ lines.push("");
198
+ lines.push(SEPARATOR);
199
+ lines.push("");
200
+
201
+ return lines.join("\n");
202
+ }
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { analyze } from "./analyze.js";
2
2
 
3
3
  export async function analyzePackage(pkg) {
4
- await analyze(pkg);
4
+ return await analyze(pkg);
5
5
  }
package/src/install.js CHANGED
@@ -1,7 +1,12 @@
1
- import { execSync } from "child_process";
1
+ import { execFileSync } from "child_process";
2
2
  import readline from "readline";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
3
5
  import { getPackageData } from "./npm.js";
4
6
  import { calculateRisk } from "./score.js";
7
+ import { formatAnalysis } from "./format.js";
8
+
9
+ const VALID_PKG_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._^~>=<|-]+)?$/i;
5
10
 
6
11
  function askQuestion(query) {
7
12
  const rl = readline.createInterface({
@@ -18,27 +23,44 @@ function askQuestion(query) {
18
23
  }
19
24
 
20
25
  export async function analyzeAndPrompt(pkgName) {
21
- console.log(`\nAnalyzing ${pkgName}...\n`);
26
+ if (!VALID_PKG_NAME.test(pkgName)) {
27
+ console.log(chalk.red("\n ✘ Invalid package name.\n"));
28
+ return;
29
+ }
22
30
 
23
- const data = await getPackageData(pkgName);
24
- const { score, warnings } = calculateRisk(data);
31
+ const spinner = ora(`Analyzing ${pkgName}...`).start();
25
32
 
26
- console.log(`Risk Score: ${score}/10`);
33
+ let data, result;
34
+ try {
35
+ data = await getPackageData(pkgName);
36
+ result = calculateRisk(data);
37
+ spinner.stop();
38
+ } catch (err) {
39
+ spinner.fail(`Failed to analyze "${pkgName}": ${err.message}`);
40
+ return;
41
+ }
27
42
 
28
- warnings.forEach((w) => console.log(`⚠ ${w}`));
43
+ console.log(formatAnalysis(data, result));
29
44
 
30
- if (score >= 6) {
45
+ if (result.level === "high") {
31
46
  const ans = await askQuestion(
32
- "\nHigh risk package. Continue install? (y/n): "
47
+ chalk.red.bold(" High risk package. Continue install? (y/n): ")
48
+ );
49
+ if (ans.toLowerCase() !== "y") {
50
+ console.log(chalk.yellow("\n Installation aborted.\n"));
51
+ return;
52
+ }
53
+ } else if (result.level === "medium") {
54
+ const ans = await askQuestion(
55
+ chalk.yellow(" ⚠ Medium risk. Continue install? (y/n): ")
33
56
  );
34
-
35
57
  if (ans.toLowerCase() !== "y") {
36
- console.log("Installation aborted.");
58
+ console.log(chalk.yellow("\n Installation aborted.\n"));
37
59
  return;
38
60
  }
39
61
  }
40
62
 
41
- console.log("\nInstalling...\n");
42
-
43
- execSync(`npm install ${pkgName}`, { stdio: "inherit" });
63
+ console.log(chalk.green("\n Installing...\n"));
64
+ execFileSync("npm", ["install", pkgName], { stdio: "inherit" });
65
+ console.log(chalk.green(`\n ✔ ${pkgName} installed successfully.\n`));
44
66
  }
package/src/npm.js CHANGED
@@ -1,27 +1,42 @@
1
- import axios from "axios";
2
-
3
1
  export async function getPackageData(pkg) {
4
- const registryUrl = `https://registry.npmjs.org/${pkg}`;
5
- const downloadUrl = `https://api.npmjs.org/downloads/point/last-week/${pkg}`;
2
+ const encodedPkg = encodeURIComponent(pkg).replace("%40", "@");
3
+ const registryUrl = `https://registry.npmjs.org/${encodedPkg}`;
4
+ const downloadUrl = `https://api.npmjs.org/downloads/point/last-week/${encodedPkg}`;
6
5
 
7
6
  const [registryRes, downloadRes] = await Promise.all([
8
- axios.get(registryUrl),
9
- axios.get(downloadUrl),
7
+ fetch(registryUrl).then((r) => {
8
+ if (!r.ok) throw new Error(`Package "${pkg}" not found on npm`);
9
+ return r.json();
10
+ }),
11
+ fetch(downloadUrl)
12
+ .then((r) => r.json())
13
+ .catch(() => ({ downloads: 0 })),
10
14
  ]);
11
15
 
12
- const data = registryRes.data;
13
- const latest = data["dist-tags"].latest;
14
- const versionData = data.versions[latest];
16
+ const data = registryRes;
17
+ const latest = data["dist-tags"]?.latest;
18
+ if (!latest) throw new Error(`No published version found for "${pkg}"`);
19
+
20
+ const versionData = data.versions?.[latest] || {};
21
+ const timeData = data.time || {};
15
22
 
16
23
  return {
17
24
  name: data.name,
18
- maintainers: data.maintainers?.length || 0,
19
- lastPublished: data.time?.[latest],
20
- hasInstallScript:
21
- versionData.scripts?.install ||
22
- versionData.scripts?.postinstall ||
23
- false,
24
25
  version: latest,
25
- downloads: downloadRes.data.downloads || 0,
26
+ description: data.description || "",
27
+ downloads: downloadRes.downloads || 0,
28
+ maintainers: data.maintainers || [],
29
+ license: versionData.license || data.license || "Unknown",
30
+ lastPublished: timeData[latest],
31
+ firstPublished: timeData.created,
32
+ hasInstallScript: !!(
33
+ versionData.scripts?.install ||
34
+ versionData.scripts?.preinstall ||
35
+ versionData.scripts?.postinstall
36
+ ),
37
+ deprecated: versionData.deprecated || false,
38
+ repository: data.repository?.url || versionData.repository?.url || null,
39
+ dependencies: Object.keys(versionData.dependencies || {}).length,
40
+ totalVersions: Object.keys(data.versions || {}).length,
26
41
  };
27
42
  }
package/src/scan.js CHANGED
@@ -1,15 +1,63 @@
1
1
  import fs from "fs";
2
- import { analyzePackage } from "./index.js";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import { getPackageData } from "./npm.js";
6
+ import { calculateRisk } from "./score.js";
7
+ import { formatAnalysis, formatScanSummary } from "./format.js";
3
8
 
4
- export async function scanProject() {
5
- const pkg = JSON.parse(fs.readFileSync("package.json"));
9
+ export async function scanProject({ verbose } = {}) {
10
+ const pkgPath = path.resolve("package.json");
11
+
12
+ if (!fs.existsSync(pkgPath)) {
13
+ console.log(chalk.red("\n ✘ No package.json found in current directory\n"));
14
+ process.exit(1);
15
+ }
16
+
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
6
18
 
7
19
  const deps = {
8
20
  ...pkg.dependencies,
9
21
  ...pkg.devDependencies,
10
22
  };
11
23
 
12
- for (const dep of Object.keys(deps)) {
13
- await analyzePackage(dep);
24
+ const depNames = Object.keys(deps);
25
+
26
+ if (depNames.length === 0) {
27
+ console.log(chalk.yellow("\n No dependencies found.\n"));
28
+ return;
29
+ }
30
+
31
+ console.log(chalk.bold(`\n Scanning ${depNames.length} dependencies...\n`));
32
+
33
+ const results = [];
34
+ const spinner = ora();
35
+
36
+ for (const dep of depNames) {
37
+ spinner.start(`Analyzing ${dep}...`);
38
+ try {
39
+ const data = await getPackageData(dep);
40
+ const result = calculateRisk(data);
41
+ results.push({ data, result });
42
+
43
+ if (verbose) {
44
+ spinner.stop();
45
+ console.log(formatAnalysis(data, result));
46
+ } else {
47
+ const icon =
48
+ result.level === "high"
49
+ ? "🚨"
50
+ : result.level === "medium"
51
+ ? "⚠"
52
+ : "✅";
53
+ spinner.succeed(
54
+ `${dep} ${chalk.gray(`(${result.score}/10)`)} ${icon}`
55
+ );
56
+ }
57
+ } catch {
58
+ spinner.fail(`${dep} - failed to fetch`);
59
+ }
14
60
  }
61
+
62
+ console.log(formatScanSummary(results));
15
63
  }
package/src/score.js CHANGED
@@ -1,47 +1,128 @@
1
+ import { checkTyposquat } from "./typosquat.js";
2
+
1
3
  export function calculateRisk(pkg) {
2
4
  let score = 0;
3
- const warnings = [];
5
+ const checks = [];
6
+ const now = new Date();
4
7
 
5
- // 🟢 Popularity (VERY IMPORTANT)
6
- if (pkg.downloads < 1000) {
8
+ // ── Downloads ──────────────────────────────────
9
+ if (pkg.downloads < 100) {
7
10
  score += 3;
8
- warnings.push("Very low downloads");
9
- } else if (pkg.downloads < 10000) {
11
+ checks.push({ label: "Downloads", status: "fail", detail: `Very low (${pkg.downloads.toLocaleString()}/week)` });
12
+ } else if (pkg.downloads < 1_000) {
10
13
  score += 2;
11
- warnings.push("Low downloads");
14
+ checks.push({ label: "Downloads", status: "warn", detail: `Low (${pkg.downloads.toLocaleString()}/week)` });
15
+ } else if (pkg.downloads < 10_000) {
16
+ score += 1;
17
+ checks.push({ label: "Downloads", status: "warn", detail: `Moderate (${pkg.downloads.toLocaleString()}/week)` });
18
+ } else {
19
+ checks.push({ label: "Downloads", status: "pass", detail: `${pkg.downloads.toLocaleString()}/week` });
12
20
  }
13
21
 
14
- // 🟡 Update recency
15
- const lastUpdate = new Date(pkg.lastPublished);
16
- const now = new Date();
17
- const diffMonths = (now - lastUpdate) / (1000 * 60 * 60 * 24 * 30);
22
+ if (pkg.downloads > 1_000_000) score -= 2;
23
+ if (pkg.downloads > 5_000_000) score -= 1;
18
24
 
19
- if (diffMonths > 12) {
20
- score += 3;
21
- warnings.push("Not updated in over a year");
22
- } else if (diffMonths > 6) {
23
- score += 1;
24
- warnings.push("Not updated recently");
25
+ // ── Update recency ────────────────────────────
26
+ if (pkg.lastPublished) {
27
+ const diffMonths = (now - new Date(pkg.lastPublished)) / (1000 * 60 * 60 * 24 * 30);
28
+ if (diffMonths > 24) {
29
+ score += 3;
30
+ checks.push({ label: "Last Updated", status: "fail", detail: "Not updated in over 2 years" });
31
+ } else if (diffMonths > 12) {
32
+ score += 2;
33
+ checks.push({ label: "Last Updated", status: "warn", detail: "Not updated in over a year" });
34
+ } else if (diffMonths > 6) {
35
+ score += 1;
36
+ checks.push({ label: "Last Updated", status: "warn", detail: "Not updated in 6+ months" });
37
+ } else {
38
+ checks.push({ label: "Last Updated", status: "pass", detail: "Recently updated" });
39
+ }
25
40
  }
26
41
 
27
- // 🔴 Install scripts (HIGH RISK)
42
+ // ── Install scripts ───────────────────────────
28
43
  if (pkg.hasInstallScript) {
29
44
  score += 3;
30
- warnings.push("Uses install/postinstall scripts");
45
+ checks.push({ label: "Install Scripts", status: "fail", detail: "Has install/postinstall scripts" });
46
+ } else {
47
+ checks.push({ label: "Install Scripts", status: "pass", detail: "No install scripts" });
48
+ }
49
+
50
+ // ── Maintainers ───────────────────────────────
51
+ const maintainerCount = pkg.maintainers?.length || 0;
52
+ if (maintainerCount === 0) {
53
+ score += 2;
54
+ checks.push({ label: "Maintainers", status: "fail", detail: "No maintainers listed" });
55
+ } else if (maintainerCount === 1) {
56
+ score += 1;
57
+ checks.push({ label: "Maintainers", status: "warn", detail: "Single maintainer" });
58
+ } else {
59
+ checks.push({ label: "Maintainers", status: "pass", detail: `${maintainerCount} maintainers` });
60
+ }
61
+
62
+ // ── License ───────────────────────────────────
63
+ const license = (typeof pkg.license === "string" ? pkg.license : pkg.license?.type) || "Unknown";
64
+ const permissive = ["MIT", "ISC", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "Unlicense"];
65
+ if (license === "Unknown" || license === "UNLICENSED") {
66
+ score += 2;
67
+ checks.push({ label: "License", status: "fail", detail: "No license specified" });
68
+ } else if (permissive.includes(license)) {
69
+ checks.push({ label: "License", status: "pass", detail: license });
70
+ } else {
71
+ score += 1;
72
+ checks.push({ label: "License", status: "warn", detail: `${license} (review recommended)` });
73
+ }
74
+
75
+ // ── Repository ────────────────────────────────
76
+ if (!pkg.repository) {
77
+ score += 1;
78
+ checks.push({ label: "Repository", status: "warn", detail: "No repository URL" });
79
+ } else {
80
+ checks.push({ label: "Repository", status: "pass", detail: "Has repository link" });
81
+ }
82
+
83
+ // ── Package age ───────────────────────────────
84
+ if (pkg.firstPublished) {
85
+ const ageDays = (now - new Date(pkg.firstPublished)) / (1000 * 60 * 60 * 24);
86
+ if (ageDays < 30) {
87
+ score += 2;
88
+ checks.push({ label: "Package Age", status: "fail", detail: "Published less than 30 days ago" });
89
+ } else if (ageDays < 180) {
90
+ score += 1;
91
+ checks.push({ label: "Package Age", status: "warn", detail: "Published less than 6 months ago" });
92
+ } else {
93
+ const years = Math.floor(ageDays / 365);
94
+ checks.push({ label: "Package Age", status: "pass", detail: years > 0 ? `${years}+ year(s) old` : "6+ months old" });
95
+ }
96
+ }
97
+
98
+ // ── Dependency count ──────────────────────────
99
+ if (pkg.dependencies > 20) {
100
+ score += 1;
101
+ checks.push({ label: "Dependencies", status: "warn", detail: `${pkg.dependencies} direct dependencies (high)` });
102
+ } else {
103
+ checks.push({ label: "Dependencies", status: "pass", detail: `${pkg.dependencies} direct dependencies` });
31
104
  }
32
105
 
33
- // 🟢 High trust signal
34
- if (pkg.downloads > 1000000) {
35
- score -= 2; // reduce risk
106
+ // ── Deprecated ────────────────────────────────
107
+ if (pkg.deprecated) {
108
+ score += 3;
109
+ checks.push({
110
+ label: "Deprecated",
111
+ status: "fail",
112
+ detail: typeof pkg.deprecated === "string" ? pkg.deprecated : "Package is deprecated",
113
+ });
36
114
  }
37
115
 
38
- if (pkg.downloads > 5000000) {
39
- score -= 2;
116
+ // ── Typosquatting ─────────────────────────────
117
+ const typosquatMatch = checkTyposquat(pkg.name);
118
+ if (typosquatMatch) {
119
+ score += 3;
120
+ checks.push({ label: "Typosquatting", status: "fail", detail: `Name is suspiciously similar to "${typosquatMatch}"` });
40
121
  }
41
122
 
42
123
  // Normalize
43
- if (score < 0) score = 0;
44
- if (score > 10) score = 10;
124
+ score = Math.max(0, Math.min(10, score));
125
+ const level = score <= 3 ? "low" : score <= 6 ? "medium" : "high";
45
126
 
46
- return { score, warnings };
127
+ return { score, checks, level };
47
128
  }
@@ -0,0 +1,50 @@
1
+ const POPULAR_PACKAGES = [
2
+ "express", "react", "vue", "angular", "lodash", "axios", "moment",
3
+ "webpack", "babel", "typescript", "eslint", "prettier", "jest",
4
+ "mocha", "chai", "next", "nuxt", "gatsby", "svelte", "jquery",
5
+ "underscore", "async", "chalk", "commander", "inquirer", "ora",
6
+ "nodemon", "dotenv", "cors", "mongoose", "sequelize", "prisma",
7
+ "socket.io", "passport", "bcrypt", "jsonwebtoken", "uuid",
8
+ "mysql", "pg", "redis", "mongodb", "fastify", "koa", "hapi",
9
+ "request", "node-fetch", "got", "cheerio", "puppeteer",
10
+ "sharp", "multer", "helmet", "morgan", "winston", "debug",
11
+ "bluebird", "rxjs", "ramda", "date-fns", "dayjs", "zod",
12
+ "yup", "ajv", "joi", "class-validator", "formik",
13
+ "tailwindcss", "bootstrap", "sass", "less", "styled-components",
14
+ "vite", "esbuild", "rollup", "parcel", "turbo",
15
+ ];
16
+
17
+ function levenshtein(a, b) {
18
+ const m = a.length;
19
+ const n = b.length;
20
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
21
+
22
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
23
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
24
+
25
+ for (let i = 1; i <= m; i++) {
26
+ for (let j = 1; j <= n; j++) {
27
+ dp[i][j] =
28
+ a[i - 1] === b[j - 1]
29
+ ? dp[i - 1][j - 1]
30
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
31
+ }
32
+ }
33
+
34
+ return dp[m][n];
35
+ }
36
+
37
+ export function checkTyposquat(name) {
38
+ const lower = name.toLowerCase();
39
+
40
+ if (POPULAR_PACKAGES.includes(lower)) return null;
41
+
42
+ for (const popular of POPULAR_PACKAGES) {
43
+ const dist = levenshtein(lower, popular);
44
+ if (dist > 0 && dist <= 2 && Math.abs(lower.length - popular.length) <= 2) {
45
+ return popular;
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
Binary file