install-guard 1.0.0 → 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 +21 -0
- package/README.md +151 -51
- package/bin/cli.js +17 -12
- package/package.json +10 -5
- package/src/analyze.js +6 -19
- package/src/format.js +202 -0
- package/src/index.js +1 -1
- package/src/install.js +35 -13
- package/src/npm.js +31 -16
- package/src/scan.js +53 -5
- package/src/score.js +107 -26
- package/src/typosquat.js +50 -0
- package/install-guard-1.0.0.tgz +0 -0
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
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
|
|
1
5
|
# 🚨 Should You Trust That npm Package Before Installing?
|
|
2
6
|
|
|
3
|
-
**
|
|
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
|
|
12
|
+
<summary>📦 See it in action</summary>
|
|
9
13
|
|
|
10
14
|
```bash
|
|
11
|
-
npx
|
|
15
|
+
$ npx install-guard install some-random-lib
|
|
12
16
|
```
|
|
13
17
|
|
|
14
18
|
```
|
|
15
|
-
|
|
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)
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
⚠ Uses install scripts
|
|
22
|
-
⚠ Recently published
|
|
38
|
+
─────────────────────────────────────────────────
|
|
23
39
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
Analyze a package:
|
|
52
81
|
|
|
53
82
|
```bash
|
|
54
|
-
npx
|
|
83
|
+
npx install-guard axios
|
|
55
84
|
```
|
|
56
85
|
|
|
57
|
-
Safely install a
|
|
86
|
+
Safely install with a risk check:
|
|
58
87
|
|
|
59
88
|
```bash
|
|
60
|
-
npx
|
|
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
|
-
npx
|
|
95
|
+
npx install-guard scan
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Scan with detailed output per package:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx install-guard scan --verbose
|
|
67
102
|
```
|
|
68
103
|
|
|
69
104
|
---
|
|
70
105
|
|
|
71
106
|
## 🧠 How Risk Score Works
|
|
72
107
|
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
Each factor contributes to a **risk score (0–10)**. Lower = safer.
|
|
83
124
|
|
|
84
125
|
---
|
|
85
126
|
|
|
@@ -88,51 +129,95 @@ Lower score = safer package.
|
|
|
88
129
|
### ✅ Safe package
|
|
89
130
|
|
|
90
131
|
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
- 🔍
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
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
|
-
| Feature | npm audit |
|
|
124
|
-
|
|
125
|
-
| Known vulnerabilities | ✅ | ✅
|
|
126
|
-
| Trust analysis | ❌ | ✅
|
|
127
|
-
| Pre-install check | ❌ | ✅
|
|
128
|
-
| Install blocking | ❌ | ✅
|
|
204
|
+
| Feature | npm audit | install-guard |
|
|
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
|
|
|
132
217
|
## 📦 Install globally
|
|
133
218
|
|
|
134
219
|
```bash
|
|
135
|
-
npm install -g
|
|
220
|
+
npm install -g install-guard
|
|
136
221
|
```
|
|
137
222
|
|
|
138
223
|
---
|
|
@@ -140,9 +225,24 @@ npm install -g dep-shield
|
|
|
140
225
|
## 🔮 Roadmap
|
|
141
226
|
|
|
142
227
|
- 🔍 GitHub activity analysis
|
|
143
|
-
- 🧠
|
|
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
|
@@ -7,14 +7,15 @@ import { scanProject } from "../src/scan.js";
|
|
|
7
7
|
const program = new Command();
|
|
8
8
|
|
|
9
9
|
program
|
|
10
|
-
.name("
|
|
11
|
-
.description("
|
|
10
|
+
.name("install-guard")
|
|
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
|
-
|
|
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
|
|
26
|
-
.
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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": "
|
|
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": [
|
|
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
|
|
11
|
+
const result = calculateRisk(data);
|
|
12
12
|
|
|
13
13
|
spinner.stop();
|
|
14
|
+
console.log(formatAnalysis(data, result));
|
|
14
15
|
|
|
15
|
-
|
|
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(
|
|
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
package/src/install.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
26
|
+
if (!VALID_PKG_NAME.test(pkgName)) {
|
|
27
|
+
console.log(chalk.red("\n ✘ Invalid package name.\n"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
const
|
|
24
|
-
const { score, warnings } = calculateRisk(data);
|
|
31
|
+
const spinner = ora(`Analyzing ${pkgName}...`).start();
|
|
25
32
|
|
|
26
|
-
|
|
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
|
-
|
|
43
|
+
console.log(formatAnalysis(data, result));
|
|
29
44
|
|
|
30
|
-
if (
|
|
45
|
+
if (result.level === "high") {
|
|
31
46
|
const ans = await askQuestion(
|
|
32
|
-
"
|
|
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("\
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
5
|
-
const
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
13
|
-
const latest = data["dist-tags"]
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
5
|
+
const checks = [];
|
|
6
|
+
const now = new Date();
|
|
4
7
|
|
|
5
|
-
//
|
|
6
|
-
if (pkg.downloads <
|
|
8
|
+
// ── Downloads ──────────────────────────────────
|
|
9
|
+
if (pkg.downloads < 100) {
|
|
7
10
|
score += 3;
|
|
8
|
-
|
|
9
|
-
} else if (pkg.downloads <
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
42
|
+
// ── Install scripts ───────────────────────────
|
|
28
43
|
if (pkg.hasInstallScript) {
|
|
29
44
|
score += 3;
|
|
30
|
-
|
|
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
|
-
//
|
|
34
|
-
if (pkg.
|
|
35
|
-
score
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
124
|
+
score = Math.max(0, Math.min(10, score));
|
|
125
|
+
const level = score <= 3 ? "low" : score <= 6 ? "medium" : "high";
|
|
45
126
|
|
|
46
|
-
return { score,
|
|
127
|
+
return { score, checks, level };
|
|
47
128
|
}
|
package/src/typosquat.js
ADDED
|
@@ -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
|
+
}
|
package/install-guard-1.0.0.tgz
DELETED
|
Binary file
|