easy-dep-graph 1.1.3 → 1.2.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/LICENSE +21 -21
- package/README.md +162 -130
- package/bin/index.js +1128 -455
- package/package.json +74 -67
package/bin/index.js
CHANGED
|
@@ -1,36 +1,291 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import shell from
|
|
3
|
-
import mustache from
|
|
4
|
-
import fastify from
|
|
5
|
-
import open from
|
|
6
|
-
import fs from
|
|
7
|
-
import path from
|
|
8
|
-
import semver from
|
|
2
|
+
import shell from 'shelljs';
|
|
3
|
+
import mustache from 'mustache';
|
|
4
|
+
import fastify from 'fastify';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import semver from 'semver';
|
|
9
9
|
let flatDeps;
|
|
10
|
+
// ─── Known Malicious / Compromised Packages Database ────────────────
|
|
11
|
+
const knownMaliciousPackages = [
|
|
12
|
+
// === Compromised legitimate packages (specific bad versions) ===
|
|
13
|
+
{
|
|
14
|
+
name: 'axios',
|
|
15
|
+
badVersions: ['1.14.1', '0.30.4'],
|
|
16
|
+
severity: 'critical',
|
|
17
|
+
description: 'Compromised via maintainer account hijack (Mar 2026). Injected RAT malware via plain-crypto-js dependency.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'ua-parser-js',
|
|
21
|
+
badVersions: ['0.7.29', '0.8.0', '1.0.0'],
|
|
22
|
+
severity: 'critical',
|
|
23
|
+
description: 'Compromised via account hijack (Oct 2021). Installed crypto miners and Danabot trojan.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'coa',
|
|
27
|
+
badVersions: ['2.0.3', '2.0.4', '2.1.1', '2.1.3', '3.1.3'],
|
|
28
|
+
severity: 'critical',
|
|
29
|
+
description: 'Compromised via account hijack (Nov 2021). Malicious preinstall script downloaded Danabot trojan.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'rc',
|
|
33
|
+
badVersions: ['1.2.9', '1.3.9', '2.3.9'],
|
|
34
|
+
severity: 'critical',
|
|
35
|
+
description: 'Compromised via account hijack (Nov 2021). Same Danabot trojan payload as coa.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'event-stream',
|
|
39
|
+
badVersions: ['3.3.6'],
|
|
40
|
+
severity: 'critical',
|
|
41
|
+
description: 'Compromised via social-engineered maintainer transfer (Nov 2018). Stole Bitcoin from Copay wallets via flatmap-stream.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'colors',
|
|
45
|
+
badVersions: ['1.4.1', '1.4.44-liberty-2'],
|
|
46
|
+
severity: 'high',
|
|
47
|
+
description: 'Sabotaged by maintainer (Jan 2022). Infinite loop printing zalgo text causing DoS. Use 1.4.0 instead.',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'faker',
|
|
51
|
+
badVersions: ['6.6.6'],
|
|
52
|
+
severity: 'high',
|
|
53
|
+
description: 'Sabotaged by maintainer (Jan 2022). Package contents wiped. Use @faker-js/faker instead.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'node-ipc',
|
|
57
|
+
badVersions: ['10.1.1', '10.1.2', '9.2.2', '11.0.0', '11.1.0'],
|
|
58
|
+
severity: 'critical',
|
|
59
|
+
description: 'Protestware (Mar 2022). Geo-targeted file destruction for Russian/Belarusian IPs. CVE-2022-23812.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'eslint-scope',
|
|
63
|
+
badVersions: ['3.7.2'],
|
|
64
|
+
severity: 'critical',
|
|
65
|
+
description: 'Compromised (Jul 2018). Stole npm tokens from developer machines.',
|
|
66
|
+
},
|
|
67
|
+
// === Always-malicious packages (any version) ===
|
|
68
|
+
{
|
|
69
|
+
name: 'flatmap-stream',
|
|
70
|
+
badVersions: '*',
|
|
71
|
+
severity: 'critical',
|
|
72
|
+
description: 'Malicious package used in event-stream supply chain attack. Stole cryptocurrency credentials.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'plain-crypto-js',
|
|
76
|
+
badVersions: '*',
|
|
77
|
+
severity: 'critical',
|
|
78
|
+
description: 'Malicious dependency injected into compromised axios versions. Delivers cross-platform RAT.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'peacenotwar',
|
|
82
|
+
badVersions: '*',
|
|
83
|
+
severity: 'high',
|
|
84
|
+
description: 'Protestware dropped by compromised node-ipc. Writes files to user desktop.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'getcookies',
|
|
88
|
+
badVersions: '*',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
description: 'Backdoor hidden in express middleware (May 2018). Remote code execution.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'crossenv',
|
|
94
|
+
badVersions: '*',
|
|
95
|
+
severity: 'critical',
|
|
96
|
+
description: 'Typosquat of cross-env. Steals environment variables and npm tokens.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'event-strem',
|
|
100
|
+
badVersions: '*',
|
|
101
|
+
severity: 'critical',
|
|
102
|
+
description: 'Typosquat of event-stream. Credential theft.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'lodahs',
|
|
106
|
+
badVersions: '*',
|
|
107
|
+
severity: 'critical',
|
|
108
|
+
description: 'Typosquat of lodash. Malware dropper.',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'babelcli',
|
|
112
|
+
badVersions: '*',
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
description: 'Typosquat of babel-cli. Steals environment variables.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'd3.js',
|
|
118
|
+
badVersions: '*',
|
|
119
|
+
severity: 'critical',
|
|
120
|
+
description: 'Typosquat of d3. Credential theft.',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'ffmpegs',
|
|
124
|
+
badVersions: '*',
|
|
125
|
+
severity: 'critical',
|
|
126
|
+
description: 'Typosquat of ffmpeg. Crypto miner.',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'gruntcli',
|
|
130
|
+
badVersions: '*',
|
|
131
|
+
severity: 'critical',
|
|
132
|
+
description: 'Typosquat of grunt-cli. Steals environment variables.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'http-proxy.js',
|
|
136
|
+
badVersions: '*',
|
|
137
|
+
severity: 'critical',
|
|
138
|
+
description: 'Typosquat of http-proxy. Credential theft.',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'mongose',
|
|
142
|
+
badVersions: '*',
|
|
143
|
+
severity: 'critical',
|
|
144
|
+
description: 'Typosquat of mongoose. Steals environment variables.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'node-fabric',
|
|
148
|
+
badVersions: '*',
|
|
149
|
+
severity: 'critical',
|
|
150
|
+
description: 'Typosquat of fabric. Opens remote shell.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'node-opencv',
|
|
154
|
+
badVersions: '*',
|
|
155
|
+
severity: 'critical',
|
|
156
|
+
description: 'Typosquat of opencv. Credential theft.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'node-opensl',
|
|
160
|
+
badVersions: '*',
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
description: 'Typosquat of openssl. Credential theft.',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'node-openssl',
|
|
166
|
+
badVersions: '*',
|
|
167
|
+
severity: 'critical',
|
|
168
|
+
description: 'Typosquat of openssl. Credential theft.',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'nodecaffe',
|
|
172
|
+
badVersions: '*',
|
|
173
|
+
severity: 'critical',
|
|
174
|
+
description: 'Typosquat of node-caffe. Credential theft.',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'nodefabric',
|
|
178
|
+
badVersions: '*',
|
|
179
|
+
severity: 'critical',
|
|
180
|
+
description: 'Typosquat of node-fabric. Credential theft.',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'nodemailer-js',
|
|
184
|
+
badVersions: '*',
|
|
185
|
+
severity: 'critical',
|
|
186
|
+
description: 'Typosquat of nodemailer. Credential theft.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'noderequest',
|
|
190
|
+
badVersions: '*',
|
|
191
|
+
severity: 'critical',
|
|
192
|
+
description: 'Typosquat of request. Credential theft.',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'nodesass',
|
|
196
|
+
badVersions: '*',
|
|
197
|
+
severity: 'critical',
|
|
198
|
+
description: 'Typosquat of node-sass. Credential theft.',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'nodesqlite',
|
|
202
|
+
badVersions: '*',
|
|
203
|
+
severity: 'critical',
|
|
204
|
+
description: 'Typosquat of sqlite. Credential theft.',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'shadowsock',
|
|
208
|
+
badVersions: '*',
|
|
209
|
+
severity: 'critical',
|
|
210
|
+
description: 'Typosquat of shadowsocks. Credential theft.',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'sqlite.js',
|
|
214
|
+
badVersions: '*',
|
|
215
|
+
severity: 'critical',
|
|
216
|
+
description: 'Typosquat of sqlite3. Credential theft.',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'proxy.js',
|
|
220
|
+
badVersions: '*',
|
|
221
|
+
severity: 'critical',
|
|
222
|
+
description: 'Typosquat of proxy. Credential theft.',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'discorsd.js',
|
|
226
|
+
badVersions: '*',
|
|
227
|
+
severity: 'critical',
|
|
228
|
+
description: 'Typosquat of discord.js. Token stealer.',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'colored',
|
|
232
|
+
badVersions: '*',
|
|
233
|
+
severity: 'critical',
|
|
234
|
+
description: 'Typosquat of colors. Credential theft.',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'mariadb',
|
|
238
|
+
badVersions: '*',
|
|
239
|
+
severity: 'critical',
|
|
240
|
+
description: 'Typosquat of mariasql. Credential theft.',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'electron-native-notify',
|
|
244
|
+
badVersions: '*',
|
|
245
|
+
severity: 'critical',
|
|
246
|
+
description: 'Malicious package (Jun 2019). Stole cryptocurrency credentials.',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'rest-client',
|
|
250
|
+
badVersions: '*',
|
|
251
|
+
severity: 'critical',
|
|
252
|
+
description: 'Malicious package (Aug 2019). Injected code to steal credentials.',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'bootstrap-sass',
|
|
256
|
+
badVersions: '*',
|
|
257
|
+
severity: 'high',
|
|
258
|
+
description: 'Typosquat with crypto miner.',
|
|
259
|
+
},
|
|
260
|
+
];
|
|
10
261
|
void (async function main() {
|
|
11
262
|
// Check if peer dependencies mode
|
|
12
|
-
const peerDependenciesMode = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
13
|
-
-1;
|
|
263
|
+
const peerDependenciesMode = process.argv.findIndex((a) => a.toLowerCase() === '--peer-dependencies') !== -1;
|
|
14
264
|
// List of packages user wants to see the dependencies
|
|
15
|
-
const packagesArgIndex = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
265
|
+
const packagesArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--packages');
|
|
16
266
|
const packagesFilter = packagesArgIndex !== -1 ? process.argv[packagesArgIndex + 1] : undefined;
|
|
17
267
|
// Only get the dependents of a specific package
|
|
18
|
-
const packageArgIndex = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
268
|
+
const packageArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--package-dependents');
|
|
19
269
|
const packageName = packageArgIndex !== -1 ? process.argv[packageArgIndex + 1] : undefined;
|
|
20
270
|
// Get port number
|
|
21
|
-
const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
271
|
+
const portArgIndex = process.argv.findIndex((a) => a.toLowerCase() === '--port');
|
|
22
272
|
const port = portArgIndex !== -1 ? +process.argv[portArgIndex + 1] : 8080;
|
|
23
273
|
// Get if should not open browser
|
|
24
|
-
const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
274
|
+
const shouldOpenBrowser = process.argv.findIndex((a) => a.toLowerCase() === '--no-open') === -1;
|
|
25
275
|
// Get if should not apply force layout
|
|
26
|
-
const shouldApplyForceLayout = process.argv.findIndex((a) => a.toLowerCase() ===
|
|
27
|
-
|
|
276
|
+
const shouldApplyForceLayout = process.argv.findIndex((a) => a.toLowerCase() === '--no-force-layout') === -1;
|
|
277
|
+
// Check if security scan mode
|
|
278
|
+
const securityScanMode = process.argv.findIndex((a) => a.toLowerCase() === '--security-scan') !== -1;
|
|
279
|
+
if (securityScanMode) {
|
|
280
|
+
await runSecurityScanMode(port, shouldOpenBrowser);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
28
283
|
if (peerDependenciesMode) {
|
|
29
284
|
await runPeerDependenciesMode(port, shouldOpenBrowser);
|
|
30
285
|
return;
|
|
31
286
|
}
|
|
32
287
|
// Run npm list command
|
|
33
|
-
const result = shell.exec(`npm list --json ${packageName ??
|
|
288
|
+
const result = shell.exec(`npm list --json ${packageName ?? '--all'}`, {
|
|
34
289
|
windowsHide: true,
|
|
35
290
|
silent: true,
|
|
36
291
|
});
|
|
@@ -39,7 +294,7 @@ void (async function main() {
|
|
|
39
294
|
console.log(`Generating dependency graph for: "${packageInfo.name}"...`);
|
|
40
295
|
// Filter dependencies if needed
|
|
41
296
|
if (packagesFilter != null) {
|
|
42
|
-
const packagesFilterList = packagesFilter.split(
|
|
297
|
+
const packagesFilterList = packagesFilter.split(',').map((d) => d.trim());
|
|
43
298
|
for (const key of Object.keys(packageInfo.dependencies)) {
|
|
44
299
|
if (!packagesFilterList.includes(key)) {
|
|
45
300
|
delete packageInfo.dependencies[key];
|
|
@@ -75,7 +330,7 @@ void (async function main() {
|
|
|
75
330
|
x,
|
|
76
331
|
y,
|
|
77
332
|
size: 10,
|
|
78
|
-
color: dep[1].isRoot ?
|
|
333
|
+
color: dep[1].isRoot ? '#22c55e' : '#3b82f6',
|
|
79
334
|
};
|
|
80
335
|
})),
|
|
81
336
|
edges: JSON.stringify(Object.entries(flatDeps)
|
|
@@ -91,7 +346,7 @@ void (async function main() {
|
|
|
91
346
|
const app = fastify({
|
|
92
347
|
logger: false,
|
|
93
348
|
});
|
|
94
|
-
app.get(
|
|
349
|
+
app.get('/', (_req, resp) => resp.type('text/html').send(html));
|
|
95
350
|
// Run the server
|
|
96
351
|
app.listen({ port }, (err, address) => {
|
|
97
352
|
if (err)
|
|
@@ -123,10 +378,10 @@ function flatDepsRecursive(deps, parentDepName) {
|
|
|
123
378
|
}
|
|
124
379
|
}
|
|
125
380
|
async function runPeerDependenciesMode(port, shouldOpenBrowser) {
|
|
126
|
-
console.log(
|
|
381
|
+
console.log('Analyzing peer dependencies...');
|
|
127
382
|
// Get the current project's package.json
|
|
128
|
-
const packageJsonPath = path.join(process.cwd(),
|
|
129
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath,
|
|
383
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
384
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
130
385
|
const installedPackages = {
|
|
131
386
|
...packageJson.dependencies,
|
|
132
387
|
...packageJson.devDependencies,
|
|
@@ -138,8 +393,8 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
|
|
|
138
393
|
const resolvedVersion = resolveVersion(info.versions);
|
|
139
394
|
const isInPackageJson = installedPackages[name] !== undefined;
|
|
140
395
|
// Check if installed in node_modules (even if not in package.json)
|
|
141
|
-
const nodeModulesPath = path.join(process.cwd(),
|
|
142
|
-
const pkgPath = path.join(nodeModulesPath, name,
|
|
396
|
+
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
|
|
397
|
+
const pkgPath = path.join(nodeModulesPath, name, 'package.json');
|
|
143
398
|
const existsInNodeModules = fs.existsSync(pkgPath);
|
|
144
399
|
const isInstalledByDependency = existsInNodeModules && !isInPackageJson;
|
|
145
400
|
return {
|
|
@@ -163,8 +418,8 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
|
|
|
163
418
|
const app = fastify({
|
|
164
419
|
logger: false,
|
|
165
420
|
});
|
|
166
|
-
app.get(
|
|
167
|
-
app.post(
|
|
421
|
+
app.get('/', (_req, resp) => resp.type('text/html').send(html));
|
|
422
|
+
app.post('/install', async (req, resp) => {
|
|
168
423
|
const { package: pkg, version } = req.body;
|
|
169
424
|
console.log(`Installing ${pkg}@${version}...`);
|
|
170
425
|
const installResult = shell.exec(`npm install ${pkg}@${version}`, {
|
|
@@ -182,7 +437,7 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
|
|
|
182
437
|
console.error(`Failed to install ${pkg}@${version}`);
|
|
183
438
|
return {
|
|
184
439
|
success: false,
|
|
185
|
-
message: installResult.stderr ||
|
|
440
|
+
message: installResult.stderr || 'Installation failed',
|
|
186
441
|
};
|
|
187
442
|
}
|
|
188
443
|
});
|
|
@@ -197,7 +452,7 @@ async function runPeerDependenciesMode(port, shouldOpenBrowser) {
|
|
|
197
452
|
}
|
|
198
453
|
function collectPeerDependenciesRecursively(initialPackages) {
|
|
199
454
|
const peerDeps = {};
|
|
200
|
-
const nodeModulesPath = path.join(process.cwd(),
|
|
455
|
+
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
|
|
201
456
|
const visited = new Set();
|
|
202
457
|
const queue = Object.keys(initialPackages);
|
|
203
458
|
while (queue.length > 0) {
|
|
@@ -208,11 +463,11 @@ function collectPeerDependenciesRecursively(initialPackages) {
|
|
|
208
463
|
}
|
|
209
464
|
visited.add(packageName);
|
|
210
465
|
try {
|
|
211
|
-
const pkgJsonPath = path.join(nodeModulesPath, packageName,
|
|
466
|
+
const pkgJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
|
|
212
467
|
if (!fs.existsSync(pkgJsonPath)) {
|
|
213
468
|
continue;
|
|
214
469
|
}
|
|
215
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath,
|
|
470
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
216
471
|
// Collect peer dependencies
|
|
217
472
|
if (pkgJson.peerDependencies) {
|
|
218
473
|
for (const [peerDepName, peerDepVersion] of Object.entries(pkgJson.peerDependencies)) {
|
|
@@ -254,7 +509,7 @@ function collectPeerDependenciesRecursively(initialPackages) {
|
|
|
254
509
|
}
|
|
255
510
|
function resolveVersion(versions) {
|
|
256
511
|
if (versions.length === 0) {
|
|
257
|
-
return { version:
|
|
512
|
+
return { version: '*', isConflict: false };
|
|
258
513
|
}
|
|
259
514
|
if (versions.length === 1) {
|
|
260
515
|
return { version: versions[0], isConflict: false };
|
|
@@ -286,445 +541,863 @@ function resolveVersion(versions) {
|
|
|
286
541
|
}
|
|
287
542
|
else {
|
|
288
543
|
// Ranges don't intersect - conflict detected
|
|
289
|
-
return { version: uniqueVersions.join(
|
|
544
|
+
return { version: uniqueVersions.join(' | '), isConflict: true };
|
|
290
545
|
}
|
|
291
546
|
}
|
|
292
547
|
return { version: intersection, isConflict: false };
|
|
293
548
|
}
|
|
294
549
|
catch (error) {
|
|
295
550
|
// If semver parsing fails, fall back to showing all versions
|
|
296
|
-
return { version: uniqueVersions.join(
|
|
551
|
+
return { version: uniqueVersions.join(' | '), isConflict: true };
|
|
297
552
|
}
|
|
298
553
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
.container {
|
|
319
|
-
max-width: 1200px;
|
|
320
|
-
margin: 0 auto;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
h1 {
|
|
324
|
-
color: white;
|
|
325
|
-
margin-bottom: 2rem;
|
|
326
|
-
font-size: 2.5rem;
|
|
327
|
-
text-align: center;
|
|
328
|
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.peer-deps-list {
|
|
332
|
-
background: white;
|
|
333
|
-
border-radius: 12px;
|
|
334
|
-
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
335
|
-
overflow: hidden;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.peer-dep-item {
|
|
339
|
-
padding: 1.5rem;
|
|
340
|
-
border-bottom: 1px solid #e5e7eb;
|
|
341
|
-
transition: background-color 0.2s;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
.peer-dep-item:last-child {
|
|
345
|
-
border-bottom: none;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.peer-dep-item:hover {
|
|
349
|
-
background-color: #f9fafb;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
.peer-dep-header {
|
|
353
|
-
display: flex;
|
|
354
|
-
justify-content: space-between;
|
|
355
|
-
align-items: center;
|
|
356
|
-
margin-bottom: 0.75rem;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.peer-dep-name {
|
|
360
|
-
font-size: 1.25rem;
|
|
361
|
-
font-weight: 600;
|
|
362
|
-
color: #1f2937;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
.peer-dep-version {
|
|
366
|
-
font-family: 'Courier New', monospace;
|
|
367
|
-
padding: 0.25rem 0.75rem;
|
|
368
|
-
background: #f3f4f6;
|
|
369
|
-
border-radius: 6px;
|
|
370
|
-
font-size: 0.875rem;
|
|
371
|
-
color: #4b5563;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
.peer-dep-version.conflict {
|
|
375
|
-
background: #fee2e2;
|
|
376
|
-
color: #dc2626;
|
|
377
|
-
font-weight: 600;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
.peer-dep-info {
|
|
381
|
-
display: flex;
|
|
382
|
-
justify-content: space-between;
|
|
383
|
-
align-items: center;
|
|
384
|
-
flex-wrap: wrap;
|
|
385
|
-
gap: 1rem;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
.required-by {
|
|
389
|
-
flex: 1;
|
|
390
|
-
min-width: 200px;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
.required-by-label {
|
|
394
|
-
font-size: 0.75rem;
|
|
395
|
-
color: #6b7280;
|
|
396
|
-
text-transform: uppercase;
|
|
397
|
-
letter-spacing: 0.05em;
|
|
398
|
-
margin-bottom: 0.25rem;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.required-by-list {
|
|
402
|
-
display: flex;
|
|
403
|
-
flex-wrap: wrap;
|
|
404
|
-
gap: 0.5rem;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
.required-by-tag {
|
|
408
|
-
display: inline-block;
|
|
409
|
-
padding: 0.25rem 0.5rem;
|
|
410
|
-
background: #dbeafe;
|
|
411
|
-
color: #1e40af;
|
|
412
|
-
border-radius: 4px;
|
|
413
|
-
font-size: 0.75rem;
|
|
414
|
-
font-weight: 500;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.install-btn {
|
|
418
|
-
padding: 0.5rem 1.5rem;
|
|
419
|
-
background: #3b82f6;
|
|
420
|
-
color: white;
|
|
421
|
-
border: none;
|
|
422
|
-
border-radius: 6px;
|
|
423
|
-
font-size: 0.875rem;
|
|
424
|
-
font-weight: 500;
|
|
425
|
-
cursor: pointer;
|
|
426
|
-
transition: all 0.2s;
|
|
427
|
-
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
.install-btn:hover {
|
|
431
|
-
background: #2563eb;
|
|
432
|
-
transform: translateY(-1px);
|
|
433
|
-
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
.install-btn:active {
|
|
437
|
-
transform: translateY(0);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.install-btn:disabled {
|
|
441
|
-
background: #9ca3af;
|
|
442
|
-
cursor: not-allowed;
|
|
443
|
-
transform: none;
|
|
444
|
-
box-shadow: none;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
.installed-badge {
|
|
448
|
-
display: flex;
|
|
449
|
-
align-items: center;
|
|
450
|
-
gap: 0.5rem;
|
|
451
|
-
padding: 0.5rem 1rem;
|
|
452
|
-
background: #d1fae5;
|
|
453
|
-
color: #065f46;
|
|
454
|
-
border-radius: 6px;
|
|
455
|
-
font-size: 0.875rem;
|
|
456
|
-
font-weight: 600;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
.installed-by-dep-badge {
|
|
460
|
-
display: flex;
|
|
461
|
-
align-items: center;
|
|
462
|
-
gap: 0.5rem;
|
|
463
|
-
padding: 0.5rem 1rem;
|
|
464
|
-
background: #e0e7ff;
|
|
465
|
-
color: #3730a3;
|
|
466
|
-
border-radius: 6px;
|
|
467
|
-
font-size: 0.875rem;
|
|
468
|
-
font-weight: 600;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
.checkmark {
|
|
472
|
-
color: #10b981;
|
|
473
|
-
font-size: 1.25rem;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
.message {
|
|
477
|
-
margin-top: 0.5rem;
|
|
478
|
-
padding: 0.5rem;
|
|
479
|
-
border-radius: 4px;
|
|
480
|
-
font-size: 0.875rem;
|
|
481
|
-
display: none;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.message.success {
|
|
485
|
-
background: #d1fae5;
|
|
486
|
-
color: #065f46;
|
|
487
|
-
display: block;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
.message.error {
|
|
491
|
-
background: #fee2e2;
|
|
492
|
-
color: #dc2626;
|
|
493
|
-
display: block;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.no-deps {
|
|
497
|
-
padding: 3rem;
|
|
498
|
-
text-align: center;
|
|
499
|
-
color: #6b7280;
|
|
500
|
-
font-size: 1.125rem;
|
|
501
|
-
}
|
|
502
|
-
</style>
|
|
503
|
-
</head>
|
|
504
|
-
<body>
|
|
505
|
-
<div class="container">
|
|
506
|
-
<h1>Peer Dependencies for {{projectName}}</h1>
|
|
507
|
-
<div class="peer-deps-list">
|
|
508
|
-
{{#peerDeps}}
|
|
509
|
-
<div class="peer-dep-item">
|
|
510
|
-
<div class="peer-dep-header">
|
|
511
|
-
<span class="peer-dep-name">{{name}}</span>
|
|
512
|
-
<span class="peer-dep-version {{#isConflict}}conflict{{/isConflict}}">
|
|
513
|
-
{{#isConflict}}Conflict: {{/isConflict}}{{version}}
|
|
514
|
-
</span>
|
|
515
|
-
</div>
|
|
516
|
-
<div class="peer-dep-info">
|
|
517
|
-
<div class="required-by">
|
|
518
|
-
<div class="required-by-label">Required by:</div>
|
|
519
|
-
<div class="required-by-list">
|
|
520
|
-
{{#requiredBy}}
|
|
521
|
-
<span class="required-by-tag">{{.}}</span>
|
|
522
|
-
{{/requiredBy}}
|
|
523
|
-
</div>
|
|
524
|
-
</div>
|
|
525
|
-
<div class="action-container">
|
|
526
|
-
{{#isInstalled}}
|
|
527
|
-
<div class="installed-badge">
|
|
528
|
-
Installed
|
|
529
|
-
</div>
|
|
530
|
-
{{/isInstalled}}
|
|
531
|
-
{{#isInstalledByDependency}}
|
|
532
|
-
<div class="installed-by-dep-badge">
|
|
533
|
-
Installed by dependency
|
|
534
|
-
</div>
|
|
535
|
-
{{/isInstalledByDependency}}
|
|
536
|
-
{{^isInstalled}}
|
|
537
|
-
{{^isInstalledByDependency}}
|
|
538
|
-
<button class="install-btn" onclick="installPackage('{{name}}', '{{version}}', this)">
|
|
539
|
-
npm install
|
|
540
|
-
</button>
|
|
541
|
-
<div class="message" id="msg-{{name}}"></div>
|
|
542
|
-
{{/isInstalledByDependency}}
|
|
543
|
-
{{/isInstalled}}
|
|
544
|
-
</div>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
{{/peerDeps}}
|
|
548
|
-
{{^peerDeps}}
|
|
549
|
-
<div class="no-deps">
|
|
550
|
-
No peer dependencies found!
|
|
551
|
-
</div>
|
|
552
|
-
{{/peerDeps}}
|
|
553
|
-
</div>
|
|
554
|
-
</div>
|
|
555
|
-
|
|
556
|
-
<script>
|
|
557
|
-
async function installPackage(packageName, version, button) {
|
|
558
|
-
const messageEl = document.getElementById('msg-' + packageName);
|
|
559
|
-
|
|
560
|
-
button.disabled = true;
|
|
561
|
-
button.textContent = 'Installing...';
|
|
562
|
-
messageEl.className = 'message';
|
|
563
|
-
messageEl.textContent = '';
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
const response = await fetch('/install', {
|
|
567
|
-
method: 'POST',
|
|
568
|
-
headers: {
|
|
569
|
-
'Content-Type': 'application/json',
|
|
570
|
-
},
|
|
571
|
-
body: JSON.stringify({
|
|
572
|
-
package: packageName,
|
|
573
|
-
version: version
|
|
574
|
-
})
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const result = await response.json();
|
|
578
|
-
|
|
579
|
-
if (result.success) {
|
|
580
|
-
messageEl.className = 'message success';
|
|
581
|
-
messageEl.textContent = result.message;
|
|
582
|
-
button.style.display = 'none';
|
|
583
|
-
|
|
584
|
-
// Optionally reload after a delay
|
|
585
|
-
setTimeout(() => {
|
|
586
|
-
location.reload();
|
|
587
|
-
}, 2000);
|
|
588
|
-
} else {
|
|
589
|
-
messageEl.className = 'message error';
|
|
590
|
-
messageEl.textContent = result.message;
|
|
591
|
-
button.disabled = false;
|
|
592
|
-
button.textContent = 'npm install';
|
|
593
|
-
}
|
|
594
|
-
} catch (error) {
|
|
595
|
-
messageEl.className = 'message error';
|
|
596
|
-
messageEl.textContent = 'Failed to install package: ' + error.message;
|
|
597
|
-
button.disabled = false;
|
|
598
|
-
button.textContent = 'npm install';
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
</script>
|
|
602
|
-
</body>
|
|
603
|
-
</html>
|
|
604
|
-
`;
|
|
605
|
-
}
|
|
606
|
-
function getTemplate() {
|
|
607
|
-
return `
|
|
608
|
-
<html>
|
|
609
|
-
<head>
|
|
610
|
-
<title>{{name}}'s Dependency Graph</title>
|
|
611
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
|
|
612
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"></script>
|
|
613
|
-
<script type="text/javascript">
|
|
614
|
-
function applyForceLayout(graph, iterations) {
|
|
615
|
-
var nodes = graph.nodes();
|
|
616
|
-
var edges = graph.edges();
|
|
617
|
-
|
|
618
|
-
// Physics constants - lower repulsion for better spacing
|
|
619
|
-
var repulsionStrength = 50;
|
|
620
|
-
var attractionStrength = 0.01;
|
|
621
|
-
var damping = 0.5;
|
|
622
|
-
|
|
623
|
-
for (var iter = 0; iter < iterations; iter++) {
|
|
624
|
-
var forces = {};
|
|
625
|
-
|
|
626
|
-
// Initialize forces
|
|
627
|
-
nodes.forEach(function(nodeId) {
|
|
628
|
-
forces[nodeId] = { x: 0, y: 0 };
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
// Repulsive forces between all nodes
|
|
632
|
-
for (var i = 0; i < nodes.length; i++) {
|
|
633
|
-
for (var j = i + 1; j < nodes.length; j++) {
|
|
634
|
-
var node1 = nodes[i];
|
|
635
|
-
var node2 = nodes[j];
|
|
636
|
-
var attrs1 = graph.getNodeAttributes(node1);
|
|
637
|
-
var attrs2 = graph.getNodeAttributes(node2);
|
|
638
|
-
|
|
639
|
-
var dx = attrs2.x - attrs1.x;
|
|
640
|
-
var dy = attrs2.y - attrs1.y;
|
|
641
|
-
var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
|
|
642
|
-
var force = repulsionStrength / (distance * distance);
|
|
643
|
-
|
|
644
|
-
var fx = (dx / distance) * force;
|
|
645
|
-
var fy = (dy / distance) * force;
|
|
646
|
-
|
|
647
|
-
forces[node1].x -= fx;
|
|
648
|
-
forces[node1].y -= fy;
|
|
649
|
-
forces[node2].x += fx;
|
|
650
|
-
forces[node2].y += fy;
|
|
554
|
+
// ─── Security Scan Logic ────────────────────────────────────────────
|
|
555
|
+
async function runSecurityScanMode(port, shouldOpenBrowser) {
|
|
556
|
+
console.log('Running security scan...\n');
|
|
557
|
+
const findings = [];
|
|
558
|
+
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
|
|
559
|
+
// Phase 1: Scan node_modules against known malicious packages
|
|
560
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
561
|
+
const entries = fs.readdirSync(nodeModulesPath);
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
// Handle scoped packages (@scope/name)
|
|
564
|
+
if (entry.startsWith('@')) {
|
|
565
|
+
const scopePath = path.join(nodeModulesPath, entry);
|
|
566
|
+
try {
|
|
567
|
+
const scopedEntries = fs.readdirSync(scopePath);
|
|
568
|
+
for (const scopedEntry of scopedEntries) {
|
|
569
|
+
const fullName = `${entry}/${scopedEntry}`;
|
|
570
|
+
checkPackageAgainstDatabase(nodeModulesPath, fullName, findings);
|
|
651
571
|
}
|
|
652
572
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
var source = edge.source || edge[0];
|
|
658
|
-
var target = edge.target || edge[1];
|
|
659
|
-
var attrs1 = graph.getNodeAttributes(source);
|
|
660
|
-
var attrs2 = graph.getNodeAttributes(target);
|
|
661
|
-
|
|
662
|
-
var dx = attrs2.x - attrs1.x;
|
|
663
|
-
var dy = attrs2.y - attrs1.y;
|
|
664
|
-
var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
|
|
665
|
-
|
|
666
|
-
var fx = dx * attractionStrength;
|
|
667
|
-
var fy = dy * attractionStrength;
|
|
668
|
-
|
|
669
|
-
forces[source].x += fx;
|
|
670
|
-
forces[source].y += fy;
|
|
671
|
-
forces[target].x -= fx;
|
|
672
|
-
forces[target].y -= fy;
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
// Apply forces with damping
|
|
676
|
-
nodes.forEach(function(nodeId) {
|
|
677
|
-
var attrs = graph.getNodeAttributes(nodeId);
|
|
678
|
-
attrs.x += forces[nodeId].x * damping;
|
|
679
|
-
attrs.y += forces[nodeId].y * damping;
|
|
680
|
-
});
|
|
573
|
+
catch {
|
|
574
|
+
// Skip unreadable scope directories
|
|
575
|
+
}
|
|
576
|
+
continue;
|
|
681
577
|
}
|
|
578
|
+
checkPackageAgainstDatabase(nodeModulesPath, entry, findings);
|
|
682
579
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
console.warn('Warning: node_modules directory not found. Skipping local package scan.\n');
|
|
583
|
+
}
|
|
584
|
+
// Phase 2: Run npm audit
|
|
585
|
+
console.log('Running npm audit...');
|
|
586
|
+
const auditResult = shell.exec('npm audit --json', {
|
|
587
|
+
windowsHide: true,
|
|
588
|
+
silent: true,
|
|
589
|
+
});
|
|
590
|
+
if (auditResult.code !== 0 && auditResult.stdout.trim() === '') {
|
|
591
|
+
console.warn('Warning: npm audit failed (missing package-lock.json or no network). Showing known-malicious results only.\n');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
try {
|
|
595
|
+
const auditData = JSON.parse(auditResult.stdout);
|
|
596
|
+
const vulnerabilities = auditData.vulnerabilities ?? {};
|
|
597
|
+
for (const [pkgName, vulnInfo] of Object.entries(vulnerabilities)) {
|
|
598
|
+
// Skip if already flagged by our known-malicious list
|
|
599
|
+
if (findings.some((f) => f.name === pkgName))
|
|
600
|
+
continue;
|
|
601
|
+
const severity = vulnInfo.severity ?? 'moderate';
|
|
602
|
+
const via = Array.isArray(vulnInfo.via)
|
|
603
|
+
? vulnInfo.via
|
|
604
|
+
.map((v) => (typeof v === 'string' ? v : (v.title ?? v.name ?? '')))
|
|
605
|
+
.filter(Boolean)
|
|
606
|
+
.join('; ')
|
|
607
|
+
: String(vulnInfo.via ?? '');
|
|
608
|
+
const installedVer = vulnInfo.range ?? vulnInfo.version ?? 'unknown';
|
|
609
|
+
findings.push({
|
|
610
|
+
name: pkgName,
|
|
611
|
+
installedVersion: installedVer,
|
|
612
|
+
severity: severity,
|
|
613
|
+
source: 'npm-audit',
|
|
614
|
+
description: via || `Vulnerability reported by npm audit (${severity})`,
|
|
704
615
|
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
console.warn('Warning: Could not parse npm audit output.\n');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Sort findings: critical first, then high, moderate, low
|
|
623
|
+
const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
|
|
624
|
+
findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
625
|
+
// Phase 3: Terminal output
|
|
626
|
+
printSecurityScanResults(findings);
|
|
627
|
+
// Phase 4: Serve HTML report
|
|
628
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
629
|
+
let projectName = 'Unknown Project';
|
|
630
|
+
try {
|
|
631
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
632
|
+
projectName = packageJson.name ?? projectName;
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
// Ignore
|
|
636
|
+
}
|
|
637
|
+
const criticalCount = findings.filter((f) => f.severity === 'critical').length;
|
|
638
|
+
const highCount = findings.filter((f) => f.severity === 'high').length;
|
|
639
|
+
const moderateCount = findings.filter((f) => f.severity === 'moderate').length;
|
|
640
|
+
const lowCount = findings.filter((f) => f.severity === 'low').length;
|
|
641
|
+
const data = {
|
|
642
|
+
projectName,
|
|
643
|
+
findings,
|
|
644
|
+
totalCount: findings.length,
|
|
645
|
+
criticalCount,
|
|
646
|
+
highCount,
|
|
647
|
+
moderateCount,
|
|
648
|
+
lowCount,
|
|
649
|
+
hasCritical: criticalCount > 0,
|
|
650
|
+
hasHigh: highCount > 0,
|
|
651
|
+
hasModerate: moderateCount > 0,
|
|
652
|
+
hasLow: lowCount > 0,
|
|
653
|
+
hasFindings: findings.length > 0,
|
|
654
|
+
};
|
|
655
|
+
const html = mustache.render(getSecurityScanTemplate(), data);
|
|
656
|
+
const app = fastify({ logger: false });
|
|
657
|
+
app.get('/', (_req, resp) => resp.type('text/html').send(html));
|
|
658
|
+
app.listen({ port }, (err, address) => {
|
|
659
|
+
if (err)
|
|
660
|
+
throw err;
|
|
661
|
+
console.log(`\nSecurity report available at: ${address}`);
|
|
662
|
+
if (shouldOpenBrowser)
|
|
663
|
+
open(address);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function checkPackageAgainstDatabase(nodeModulesPath, packageName, findings) {
|
|
667
|
+
const pkgJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
|
|
668
|
+
if (!fs.existsSync(pkgJsonPath))
|
|
669
|
+
return;
|
|
670
|
+
let installedVersion;
|
|
671
|
+
try {
|
|
672
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
673
|
+
installedVersion = pkgJson.version ?? '0.0.0';
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
for (const entry of knownMaliciousPackages) {
|
|
679
|
+
if (entry.name !== packageName)
|
|
680
|
+
continue;
|
|
681
|
+
if (entry.badVersions === '*') {
|
|
682
|
+
findings.push({
|
|
683
|
+
name: packageName,
|
|
684
|
+
installedVersion,
|
|
685
|
+
severity: entry.severity,
|
|
686
|
+
source: 'known-malicious',
|
|
687
|
+
description: entry.description,
|
|
705
688
|
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
renderEdgeLabels: false,
|
|
715
|
-
defaultNodeColor: '#3b82f6',
|
|
716
|
-
defaultEdgeColor: '#94a3b8'
|
|
689
|
+
}
|
|
690
|
+
else if (entry.badVersions.includes(installedVersion)) {
|
|
691
|
+
findings.push({
|
|
692
|
+
name: packageName,
|
|
693
|
+
installedVersion,
|
|
694
|
+
severity: entry.severity,
|
|
695
|
+
source: 'known-malicious',
|
|
696
|
+
description: entry.description,
|
|
717
697
|
});
|
|
718
698
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function printSecurityScanResults(findings) {
|
|
702
|
+
if (findings.length === 0) {
|
|
703
|
+
console.log('\x1b[32m✔ No known malicious packages or vulnerabilities found.\x1b[0m\n');
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const critical = findings.filter((f) => f.severity === 'critical');
|
|
707
|
+
const high = findings.filter((f) => f.severity === 'high');
|
|
708
|
+
const moderate = findings.filter((f) => f.severity === 'moderate');
|
|
709
|
+
const low = findings.filter((f) => f.severity === 'low');
|
|
710
|
+
console.log(`\x1b[1mSecurity Scan Results:\x1b[0m`);
|
|
711
|
+
console.log(`─────────────────────────────────────────────`);
|
|
712
|
+
if (critical.length > 0)
|
|
713
|
+
console.log(` \x1b[31m● ${critical.length} critical\x1b[0m`);
|
|
714
|
+
if (high.length > 0)
|
|
715
|
+
console.log(` \x1b[33m● ${high.length} high\x1b[0m`);
|
|
716
|
+
if (moderate.length > 0)
|
|
717
|
+
console.log(` \x1b[36m● ${moderate.length} moderate\x1b[0m`);
|
|
718
|
+
if (low.length > 0)
|
|
719
|
+
console.log(` \x1b[37m● ${low.length} low\x1b[0m`);
|
|
720
|
+
console.log(`─────────────────────────────────────────────\n`);
|
|
721
|
+
for (const finding of findings) {
|
|
722
|
+
const color = finding.severity === 'critical'
|
|
723
|
+
? '\x1b[31m'
|
|
724
|
+
: finding.severity === 'high'
|
|
725
|
+
? '\x1b[33m'
|
|
726
|
+
: finding.severity === 'moderate'
|
|
727
|
+
? '\x1b[36m'
|
|
728
|
+
: '\x1b[37m';
|
|
729
|
+
const sourceTag = finding.source === 'known-malicious' ? '[KNOWN MALICIOUS]' : '[npm audit]';
|
|
730
|
+
console.log(` ${color}${finding.severity.toUpperCase()}\x1b[0m ${finding.name}@${finding.installedVersion} ${sourceTag}`);
|
|
731
|
+
console.log(` ${finding.description}`);
|
|
732
|
+
console.log();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// ─── Security Scan HTML Template ────────────────────────────────────
|
|
736
|
+
function getSecurityScanTemplate() {
|
|
737
|
+
return `
|
|
738
|
+
<html>
|
|
739
|
+
<head>
|
|
740
|
+
<title>{{projectName}} - Security Scan</title>
|
|
741
|
+
<style>
|
|
742
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
743
|
+
|
|
744
|
+
body {
|
|
745
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
746
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
747
|
+
min-height: 100vh;
|
|
748
|
+
padding: 2rem;
|
|
749
|
+
color: #e2e8f0;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
753
|
+
|
|
754
|
+
h1 {
|
|
755
|
+
color: white;
|
|
756
|
+
margin-bottom: 0.5rem;
|
|
757
|
+
font-size: 2.5rem;
|
|
758
|
+
text-align: center;
|
|
759
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.subtitle {
|
|
763
|
+
text-align: center;
|
|
764
|
+
color: #94a3b8;
|
|
765
|
+
margin-bottom: 2rem;
|
|
766
|
+
font-size: 1.1rem;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.summary-cards {
|
|
770
|
+
display: flex;
|
|
771
|
+
gap: 1rem;
|
|
772
|
+
margin-bottom: 2rem;
|
|
773
|
+
flex-wrap: wrap;
|
|
774
|
+
justify-content: center;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.summary-card {
|
|
778
|
+
padding: 1.25rem 2rem;
|
|
779
|
+
border-radius: 12px;
|
|
780
|
+
text-align: center;
|
|
781
|
+
min-width: 140px;
|
|
782
|
+
backdrop-filter: blur(10px);
|
|
783
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.summary-card .count {
|
|
787
|
+
font-size: 2.5rem;
|
|
788
|
+
font-weight: 700;
|
|
789
|
+
line-height: 1;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.summary-card .label {
|
|
793
|
+
font-size: 0.8rem;
|
|
794
|
+
text-transform: uppercase;
|
|
795
|
+
letter-spacing: 0.1em;
|
|
796
|
+
margin-top: 0.5rem;
|
|
797
|
+
opacity: 0.9;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.card-critical { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); }
|
|
801
|
+
.card-critical .count { color: #ef4444; }
|
|
802
|
+
.card-critical .label { color: #fca5a5; }
|
|
803
|
+
|
|
804
|
+
.card-high { background: rgba(245, 158, 11, 0.2); border-color: rgba(245, 158, 11, 0.4); }
|
|
805
|
+
.card-high .count { color: #f59e0b; }
|
|
806
|
+
.card-high .label { color: #fcd34d; }
|
|
807
|
+
|
|
808
|
+
.card-moderate { background: rgba(59, 130, 246, 0.2); border-color: rgba(59, 130, 246, 0.4); }
|
|
809
|
+
.card-moderate .count { color: #3b82f6; }
|
|
810
|
+
.card-moderate .label { color: #93c5fd; }
|
|
811
|
+
|
|
812
|
+
.card-low { background: rgba(107, 114, 128, 0.2); border-color: rgba(107, 114, 128, 0.4); }
|
|
813
|
+
.card-low .count { color: #9ca3af; }
|
|
814
|
+
.card-low .label { color: #d1d5db; }
|
|
815
|
+
|
|
816
|
+
.findings-list {
|
|
817
|
+
background: rgba(255, 255, 255, 0.05);
|
|
818
|
+
border-radius: 12px;
|
|
819
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
820
|
+
overflow: hidden;
|
|
821
|
+
backdrop-filter: blur(10px);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.finding-item {
|
|
825
|
+
padding: 1.25rem 1.5rem;
|
|
826
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
827
|
+
transition: background-color 0.2s;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.finding-item:last-child { border-bottom: none; }
|
|
831
|
+
.finding-item:hover { background: rgba(255,255,255,0.04); }
|
|
832
|
+
|
|
833
|
+
.finding-header {
|
|
834
|
+
display: flex;
|
|
835
|
+
align-items: center;
|
|
836
|
+
gap: 1rem;
|
|
837
|
+
margin-bottom: 0.5rem;
|
|
838
|
+
flex-wrap: wrap;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.severity-badge {
|
|
842
|
+
padding: 0.2rem 0.75rem;
|
|
843
|
+
border-radius: 9999px;
|
|
844
|
+
font-size: 0.7rem;
|
|
845
|
+
font-weight: 700;
|
|
846
|
+
text-transform: uppercase;
|
|
847
|
+
letter-spacing: 0.05em;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.severity-critical { background: #ef4444; color: white; }
|
|
851
|
+
.severity-high { background: #f59e0b; color: #1a1a2e; }
|
|
852
|
+
.severity-moderate { background: #3b82f6; color: white; }
|
|
853
|
+
.severity-low { background: #6b7280; color: white; }
|
|
854
|
+
|
|
855
|
+
.finding-name {
|
|
856
|
+
font-size: 1.2rem;
|
|
857
|
+
font-weight: 600;
|
|
858
|
+
color: #f1f5f9;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.finding-version {
|
|
862
|
+
font-family: 'Courier New', monospace;
|
|
863
|
+
font-size: 0.85rem;
|
|
864
|
+
padding: 0.15rem 0.6rem;
|
|
865
|
+
background: rgba(255,255,255,0.1);
|
|
866
|
+
border-radius: 4px;
|
|
867
|
+
color: #cbd5e1;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.source-tag {
|
|
871
|
+
font-size: 0.7rem;
|
|
872
|
+
padding: 0.15rem 0.6rem;
|
|
873
|
+
border-radius: 4px;
|
|
874
|
+
font-weight: 600;
|
|
875
|
+
text-transform: uppercase;
|
|
876
|
+
letter-spacing: 0.05em;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.source-known-malicious { background: rgba(239, 68, 68, 0.2); color: #fca5a5; border: 1px solid rgba(239, 68, 68, 0.3); }
|
|
880
|
+
.source-npm-audit { background: rgba(139, 92, 246, 0.2); color: #c4b5fd; border: 1px solid rgba(139, 92, 246, 0.3); }
|
|
881
|
+
|
|
882
|
+
.finding-description {
|
|
883
|
+
color: #94a3b8;
|
|
884
|
+
font-size: 0.9rem;
|
|
885
|
+
line-height: 1.5;
|
|
886
|
+
padding-left: 0.25rem;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.no-findings {
|
|
890
|
+
padding: 4rem 2rem;
|
|
891
|
+
text-align: center;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.no-findings .checkmark {
|
|
895
|
+
font-size: 4rem;
|
|
896
|
+
margin-bottom: 1rem;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.no-findings h2 {
|
|
900
|
+
color: #22c55e;
|
|
901
|
+
font-size: 1.5rem;
|
|
902
|
+
margin-bottom: 0.5rem;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.no-findings p {
|
|
906
|
+
color: #94a3b8;
|
|
907
|
+
}
|
|
908
|
+
</style>
|
|
909
|
+
</head>
|
|
910
|
+
<body>
|
|
911
|
+
<div class="container">
|
|
912
|
+
<h1>Security Scan</h1>
|
|
913
|
+
<p class="subtitle">{{projectName}}</p>
|
|
914
|
+
|
|
915
|
+
{{#hasFindings}}
|
|
916
|
+
<div class="summary-cards">
|
|
917
|
+
{{#hasCritical}}
|
|
918
|
+
<div class="summary-card card-critical">
|
|
919
|
+
<div class="count">{{criticalCount}}</div>
|
|
920
|
+
<div class="label">Critical</div>
|
|
921
|
+
</div>
|
|
922
|
+
{{/hasCritical}}
|
|
923
|
+
{{#hasHigh}}
|
|
924
|
+
<div class="summary-card card-high">
|
|
925
|
+
<div class="count">{{highCount}}</div>
|
|
926
|
+
<div class="label">High</div>
|
|
927
|
+
</div>
|
|
928
|
+
{{/hasHigh}}
|
|
929
|
+
{{#hasModerate}}
|
|
930
|
+
<div class="summary-card card-moderate">
|
|
931
|
+
<div class="count">{{moderateCount}}</div>
|
|
932
|
+
<div class="label">Moderate</div>
|
|
933
|
+
</div>
|
|
934
|
+
{{/hasModerate}}
|
|
935
|
+
{{#hasLow}}
|
|
936
|
+
<div class="summary-card card-low">
|
|
937
|
+
<div class="count">{{lowCount}}</div>
|
|
938
|
+
<div class="label">Low</div>
|
|
939
|
+
</div>
|
|
940
|
+
{{/hasLow}}
|
|
941
|
+
</div>
|
|
942
|
+
|
|
943
|
+
<div class="findings-list">
|
|
944
|
+
{{#findings}}
|
|
945
|
+
<div class="finding-item">
|
|
946
|
+
<div class="finding-header">
|
|
947
|
+
<span class="severity-badge severity-{{severity}}">{{severity}}</span>
|
|
948
|
+
<span class="finding-name">{{name}}</span>
|
|
949
|
+
<span class="finding-version">{{installedVersion}}</span>
|
|
950
|
+
<span class="source-tag source-{{source}}">{{source}}</span>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="finding-description">{{description}}</div>
|
|
953
|
+
</div>
|
|
954
|
+
{{/findings}}
|
|
955
|
+
</div>
|
|
956
|
+
{{/hasFindings}}
|
|
957
|
+
|
|
958
|
+
{{^hasFindings}}
|
|
959
|
+
<div class="findings-list">
|
|
960
|
+
<div class="no-findings">
|
|
961
|
+
<div class="checkmark">✓</div>
|
|
962
|
+
<h2>All Clear</h2>
|
|
963
|
+
<p>No known malicious packages or vulnerabilities were found.</p>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
{{/hasFindings}}
|
|
967
|
+
</div>
|
|
968
|
+
</body>
|
|
969
|
+
</html>
|
|
970
|
+
`;
|
|
971
|
+
}
|
|
972
|
+
function getPeerDepsTemplate() {
|
|
973
|
+
return `
|
|
974
|
+
<html>
|
|
975
|
+
<head>
|
|
976
|
+
<title>{{projectName}} - Peer Dependencies</title>
|
|
977
|
+
<style>
|
|
978
|
+
* {
|
|
979
|
+
margin: 0;
|
|
980
|
+
padding: 0;
|
|
981
|
+
box-sizing: border-box;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
body {
|
|
985
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
986
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
987
|
+
min-height: 100vh;
|
|
988
|
+
padding: 2rem;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.container {
|
|
992
|
+
max-width: 1200px;
|
|
993
|
+
margin: 0 auto;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
h1 {
|
|
997
|
+
color: white;
|
|
998
|
+
margin-bottom: 2rem;
|
|
999
|
+
font-size: 2.5rem;
|
|
1000
|
+
text-align: center;
|
|
1001
|
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.peer-deps-list {
|
|
1005
|
+
background: white;
|
|
1006
|
+
border-radius: 12px;
|
|
1007
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
1008
|
+
overflow: hidden;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.peer-dep-item {
|
|
1012
|
+
padding: 1.5rem;
|
|
1013
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1014
|
+
transition: background-color 0.2s;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.peer-dep-item:last-child {
|
|
1018
|
+
border-bottom: none;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.peer-dep-item:hover {
|
|
1022
|
+
background-color: #f9fafb;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.peer-dep-header {
|
|
1026
|
+
display: flex;
|
|
1027
|
+
justify-content: space-between;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
margin-bottom: 0.75rem;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.peer-dep-name {
|
|
1033
|
+
font-size: 1.25rem;
|
|
1034
|
+
font-weight: 600;
|
|
1035
|
+
color: #1f2937;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.peer-dep-version {
|
|
1039
|
+
font-family: 'Courier New', monospace;
|
|
1040
|
+
padding: 0.25rem 0.75rem;
|
|
1041
|
+
background: #f3f4f6;
|
|
1042
|
+
border-radius: 6px;
|
|
1043
|
+
font-size: 0.875rem;
|
|
1044
|
+
color: #4b5563;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.peer-dep-version.conflict {
|
|
1048
|
+
background: #fee2e2;
|
|
1049
|
+
color: #dc2626;
|
|
1050
|
+
font-weight: 600;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.peer-dep-info {
|
|
1054
|
+
display: flex;
|
|
1055
|
+
justify-content: space-between;
|
|
1056
|
+
align-items: center;
|
|
1057
|
+
flex-wrap: wrap;
|
|
1058
|
+
gap: 1rem;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.required-by {
|
|
1062
|
+
flex: 1;
|
|
1063
|
+
min-width: 200px;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.required-by-label {
|
|
1067
|
+
font-size: 0.75rem;
|
|
1068
|
+
color: #6b7280;
|
|
1069
|
+
text-transform: uppercase;
|
|
1070
|
+
letter-spacing: 0.05em;
|
|
1071
|
+
margin-bottom: 0.25rem;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.required-by-list {
|
|
1075
|
+
display: flex;
|
|
1076
|
+
flex-wrap: wrap;
|
|
1077
|
+
gap: 0.5rem;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.required-by-tag {
|
|
1081
|
+
display: inline-block;
|
|
1082
|
+
padding: 0.25rem 0.5rem;
|
|
1083
|
+
background: #dbeafe;
|
|
1084
|
+
color: #1e40af;
|
|
1085
|
+
border-radius: 4px;
|
|
1086
|
+
font-size: 0.75rem;
|
|
1087
|
+
font-weight: 500;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.install-btn {
|
|
1091
|
+
padding: 0.5rem 1.5rem;
|
|
1092
|
+
background: #3b82f6;
|
|
1093
|
+
color: white;
|
|
1094
|
+
border: none;
|
|
1095
|
+
border-radius: 6px;
|
|
1096
|
+
font-size: 0.875rem;
|
|
1097
|
+
font-weight: 500;
|
|
1098
|
+
cursor: pointer;
|
|
1099
|
+
transition: all 0.2s;
|
|
1100
|
+
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.install-btn:hover {
|
|
1104
|
+
background: #2563eb;
|
|
1105
|
+
transform: translateY(-1px);
|
|
1106
|
+
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.install-btn:active {
|
|
1110
|
+
transform: translateY(0);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.install-btn:disabled {
|
|
1114
|
+
background: #9ca3af;
|
|
1115
|
+
cursor: not-allowed;
|
|
1116
|
+
transform: none;
|
|
1117
|
+
box-shadow: none;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.installed-badge {
|
|
1121
|
+
display: flex;
|
|
1122
|
+
align-items: center;
|
|
1123
|
+
gap: 0.5rem;
|
|
1124
|
+
padding: 0.5rem 1rem;
|
|
1125
|
+
background: #d1fae5;
|
|
1126
|
+
color: #065f46;
|
|
1127
|
+
border-radius: 6px;
|
|
1128
|
+
font-size: 0.875rem;
|
|
1129
|
+
font-weight: 600;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.installed-by-dep-badge {
|
|
1133
|
+
display: flex;
|
|
1134
|
+
align-items: center;
|
|
1135
|
+
gap: 0.5rem;
|
|
1136
|
+
padding: 0.5rem 1rem;
|
|
1137
|
+
background: #e0e7ff;
|
|
1138
|
+
color: #3730a3;
|
|
1139
|
+
border-radius: 6px;
|
|
1140
|
+
font-size: 0.875rem;
|
|
1141
|
+
font-weight: 600;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.checkmark {
|
|
1145
|
+
color: #10b981;
|
|
1146
|
+
font-size: 1.25rem;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.message {
|
|
1150
|
+
margin-top: 0.5rem;
|
|
1151
|
+
padding: 0.5rem;
|
|
1152
|
+
border-radius: 4px;
|
|
1153
|
+
font-size: 0.875rem;
|
|
1154
|
+
display: none;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.message.success {
|
|
1158
|
+
background: #d1fae5;
|
|
1159
|
+
color: #065f46;
|
|
1160
|
+
display: block;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.message.error {
|
|
1164
|
+
background: #fee2e2;
|
|
1165
|
+
color: #dc2626;
|
|
1166
|
+
display: block;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.no-deps {
|
|
1170
|
+
padding: 3rem;
|
|
1171
|
+
text-align: center;
|
|
1172
|
+
color: #6b7280;
|
|
1173
|
+
font-size: 1.125rem;
|
|
1174
|
+
}
|
|
1175
|
+
</style>
|
|
1176
|
+
</head>
|
|
1177
|
+
<body>
|
|
1178
|
+
<div class="container">
|
|
1179
|
+
<h1>Peer Dependencies for {{projectName}}</h1>
|
|
1180
|
+
<div class="peer-deps-list">
|
|
1181
|
+
{{#peerDeps}}
|
|
1182
|
+
<div class="peer-dep-item">
|
|
1183
|
+
<div class="peer-dep-header">
|
|
1184
|
+
<span class="peer-dep-name">{{name}}</span>
|
|
1185
|
+
<span class="peer-dep-version {{#isConflict}}conflict{{/isConflict}}">
|
|
1186
|
+
{{#isConflict}}Conflict: {{/isConflict}}{{version}}
|
|
1187
|
+
</span>
|
|
1188
|
+
</div>
|
|
1189
|
+
<div class="peer-dep-info">
|
|
1190
|
+
<div class="required-by">
|
|
1191
|
+
<div class="required-by-label">Required by:</div>
|
|
1192
|
+
<div class="required-by-list">
|
|
1193
|
+
{{#requiredBy}}
|
|
1194
|
+
<span class="required-by-tag">{{.}}</span>
|
|
1195
|
+
{{/requiredBy}}
|
|
1196
|
+
</div>
|
|
1197
|
+
</div>
|
|
1198
|
+
<div class="action-container">
|
|
1199
|
+
{{#isInstalled}}
|
|
1200
|
+
<div class="installed-badge">
|
|
1201
|
+
Installed
|
|
1202
|
+
</div>
|
|
1203
|
+
{{/isInstalled}}
|
|
1204
|
+
{{#isInstalledByDependency}}
|
|
1205
|
+
<div class="installed-by-dep-badge">
|
|
1206
|
+
Installed by dependency
|
|
1207
|
+
</div>
|
|
1208
|
+
{{/isInstalledByDependency}}
|
|
1209
|
+
{{^isInstalled}}
|
|
1210
|
+
{{^isInstalledByDependency}}
|
|
1211
|
+
<button class="install-btn" onclick="installPackage('{{name}}', '{{version}}', this)">
|
|
1212
|
+
npm install
|
|
1213
|
+
</button>
|
|
1214
|
+
<div class="message" id="msg-{{name}}"></div>
|
|
1215
|
+
{{/isInstalledByDependency}}
|
|
1216
|
+
{{/isInstalled}}
|
|
1217
|
+
</div>
|
|
1218
|
+
</div>
|
|
1219
|
+
</div>
|
|
1220
|
+
{{/peerDeps}}
|
|
1221
|
+
{{^peerDeps}}
|
|
1222
|
+
<div class="no-deps">
|
|
1223
|
+
No peer dependencies found!
|
|
1224
|
+
</div>
|
|
1225
|
+
{{/peerDeps}}
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
|
|
1229
|
+
<script>
|
|
1230
|
+
async function installPackage(packageName, version, button) {
|
|
1231
|
+
const messageEl = document.getElementById('msg-' + packageName);
|
|
1232
|
+
|
|
1233
|
+
button.disabled = true;
|
|
1234
|
+
button.textContent = 'Installing...';
|
|
1235
|
+
messageEl.className = 'message';
|
|
1236
|
+
messageEl.textContent = '';
|
|
1237
|
+
|
|
1238
|
+
try {
|
|
1239
|
+
const response = await fetch('/install', {
|
|
1240
|
+
method: 'POST',
|
|
1241
|
+
headers: {
|
|
1242
|
+
'Content-Type': 'application/json',
|
|
1243
|
+
},
|
|
1244
|
+
body: JSON.stringify({
|
|
1245
|
+
package: packageName,
|
|
1246
|
+
version: version
|
|
1247
|
+
})
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const result = await response.json();
|
|
1251
|
+
|
|
1252
|
+
if (result.success) {
|
|
1253
|
+
messageEl.className = 'message success';
|
|
1254
|
+
messageEl.textContent = result.message;
|
|
1255
|
+
button.style.display = 'none';
|
|
1256
|
+
|
|
1257
|
+
// Optionally reload after a delay
|
|
1258
|
+
setTimeout(() => {
|
|
1259
|
+
location.reload();
|
|
1260
|
+
}, 2000);
|
|
1261
|
+
} else {
|
|
1262
|
+
messageEl.className = 'message error';
|
|
1263
|
+
messageEl.textContent = result.message;
|
|
1264
|
+
button.disabled = false;
|
|
1265
|
+
button.textContent = 'npm install';
|
|
1266
|
+
}
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
messageEl.className = 'message error';
|
|
1269
|
+
messageEl.textContent = 'Failed to install package: ' + error.message;
|
|
1270
|
+
button.disabled = false;
|
|
1271
|
+
button.textContent = 'npm install';
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
</script>
|
|
1275
|
+
</body>
|
|
1276
|
+
</html>
|
|
1277
|
+
`;
|
|
1278
|
+
}
|
|
1279
|
+
function getTemplate() {
|
|
1280
|
+
return `
|
|
1281
|
+
<html>
|
|
1282
|
+
<head>
|
|
1283
|
+
<title>{{name}}'s Dependency Graph</title>
|
|
1284
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
|
|
1285
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"></script>
|
|
1286
|
+
<script type="text/javascript">
|
|
1287
|
+
function applyForceLayout(graph, iterations) {
|
|
1288
|
+
var nodes = graph.nodes();
|
|
1289
|
+
var edges = graph.edges();
|
|
1290
|
+
|
|
1291
|
+
// Physics constants - lower repulsion for better spacing
|
|
1292
|
+
var repulsionStrength = 50;
|
|
1293
|
+
var attractionStrength = 0.01;
|
|
1294
|
+
var damping = 0.5;
|
|
1295
|
+
|
|
1296
|
+
for (var iter = 0; iter < iterations; iter++) {
|
|
1297
|
+
var forces = {};
|
|
1298
|
+
|
|
1299
|
+
// Initialize forces
|
|
1300
|
+
nodes.forEach(function(nodeId) {
|
|
1301
|
+
forces[nodeId] = { x: 0, y: 0 };
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
// Repulsive forces between all nodes
|
|
1305
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1306
|
+
for (var j = i + 1; j < nodes.length; j++) {
|
|
1307
|
+
var node1 = nodes[i];
|
|
1308
|
+
var node2 = nodes[j];
|
|
1309
|
+
var attrs1 = graph.getNodeAttributes(node1);
|
|
1310
|
+
var attrs2 = graph.getNodeAttributes(node2);
|
|
1311
|
+
|
|
1312
|
+
var dx = attrs2.x - attrs1.x;
|
|
1313
|
+
var dy = attrs2.y - attrs1.y;
|
|
1314
|
+
var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
|
|
1315
|
+
var force = repulsionStrength / (distance * distance);
|
|
1316
|
+
|
|
1317
|
+
var fx = (dx / distance) * force;
|
|
1318
|
+
var fy = (dy / distance) * force;
|
|
1319
|
+
|
|
1320
|
+
forces[node1].x -= fx;
|
|
1321
|
+
forces[node1].y -= fy;
|
|
1322
|
+
forces[node2].x += fx;
|
|
1323
|
+
forces[node2].y += fy;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Attractive forces along edges
|
|
1328
|
+
edges.forEach(function(edgeId) {
|
|
1329
|
+
var edge = graph.extremities(edgeId);
|
|
1330
|
+
var source = edge.source || edge[0];
|
|
1331
|
+
var target = edge.target || edge[1];
|
|
1332
|
+
var attrs1 = graph.getNodeAttributes(source);
|
|
1333
|
+
var attrs2 = graph.getNodeAttributes(target);
|
|
1334
|
+
|
|
1335
|
+
var dx = attrs2.x - attrs1.x;
|
|
1336
|
+
var dy = attrs2.y - attrs1.y;
|
|
1337
|
+
var distance = Math.sqrt(dx * dx + dy * dy) || 0.1;
|
|
1338
|
+
|
|
1339
|
+
var fx = dx * attractionStrength;
|
|
1340
|
+
var fy = dy * attractionStrength;
|
|
1341
|
+
|
|
1342
|
+
forces[source].x += fx;
|
|
1343
|
+
forces[source].y += fy;
|
|
1344
|
+
forces[target].x -= fx;
|
|
1345
|
+
forces[target].y -= fy;
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// Apply forces with damping
|
|
1349
|
+
nodes.forEach(function(nodeId) {
|
|
1350
|
+
var attrs = graph.getNodeAttributes(nodeId);
|
|
1351
|
+
attrs.x += forces[nodeId].x * damping;
|
|
1352
|
+
attrs.y += forces[nodeId].y * damping;
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function renderGraph() {
|
|
1358
|
+
var nodes = {{{nodes}}};
|
|
1359
|
+
var edges = {{{edges}}};
|
|
1360
|
+
|
|
1361
|
+
var graph = new graphology.Graph();
|
|
1362
|
+
|
|
1363
|
+
nodes.forEach(function(node) {
|
|
1364
|
+
graph.addNode(node.key, {
|
|
1365
|
+
label: node.label,
|
|
1366
|
+
x: node.x,
|
|
1367
|
+
y: node.y,
|
|
1368
|
+
size: node.size,
|
|
1369
|
+
color: node.color
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
edges.forEach(function(edge, index) {
|
|
1374
|
+
graph.mergeEdge(edge.source, edge.target, {
|
|
1375
|
+
size: 2,
|
|
1376
|
+
color: '#94a3b8'
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// Apply custom force-directed layout
|
|
1381
|
+
{{#applyForceLayout}}
|
|
1382
|
+
applyForceLayout(graph, 100);
|
|
1383
|
+
{{/applyForceLayout}}
|
|
1384
|
+
|
|
1385
|
+
var container = document.getElementById("dep-graph");
|
|
1386
|
+
var renderer = new Sigma(graph, container, {
|
|
1387
|
+
renderEdgeLabels: false,
|
|
1388
|
+
defaultNodeColor: '#3b82f6',
|
|
1389
|
+
defaultEdgeColor: '#94a3b8'
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
</script>
|
|
1393
|
+
<style>
|
|
1394
|
+
body { margin: 0; padding: 0; }
|
|
1395
|
+
#dep-graph { width: 100vw; height: 100vh; background: #f8fafc; }
|
|
1396
|
+
</style>
|
|
1397
|
+
</head>
|
|
1398
|
+
<body onload="renderGraph()">
|
|
1399
|
+
<div id="dep-graph"></div>
|
|
1400
|
+
</body>
|
|
1401
|
+
</html>
|
|
729
1402
|
`;
|
|
730
1403
|
}
|