install-guard 1.0.1 → 1.1.2
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 +229 -52
- package/bin/cli.js +40 -14
- package/package.json +13 -4
- package/src/analyze.js +15 -22
- package/src/checks/dependencyDiff.js +114 -0
- package/src/checks/deprecation.js +24 -0
- package/src/checks/githubVerify.js +67 -0
- package/src/checks/index.js +8 -0
- package/src/checks/license.js +35 -0
- package/src/checks/maintainers.js +30 -0
- package/src/checks/recentPublish.js +70 -0
- package/src/checks/scripts.js +45 -0
- package/src/checks/typosquat.js +77 -0
- package/src/format.js +258 -0
- package/src/index.js +2 -2
- package/src/install.js +43 -15
- package/src/npm.js +31 -16
- package/src/scan.js +55 -5
- package/src/score.js +107 -26
- package/src/services/pipeline.js +105 -0
- package/src/services/scorer.js +36 -0
- package/src/typosquat.js +50 -0
- package/src/utils/cache.js +44 -0
- package/src/utils/github.js +64 -0
- package/src/utils/registry.js +99 -0
- package/install-guard-1.0.0.tgz +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks for deprecation.
|
|
3
|
+
*/
|
|
4
|
+
export function checkDeprecation(ctx) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
|
|
7
|
+
if (ctx.deprecated) {
|
|
8
|
+
findings.push({
|
|
9
|
+
severity: "high",
|
|
10
|
+
message: typeof ctx.deprecated === "string"
|
|
11
|
+
? `Deprecated: ${ctx.deprecated}`
|
|
12
|
+
: "Package is deprecated",
|
|
13
|
+
score: 3,
|
|
14
|
+
});
|
|
15
|
+
} else {
|
|
16
|
+
findings.push({
|
|
17
|
+
severity: "info",
|
|
18
|
+
message: "Not deprecated",
|
|
19
|
+
score: 0,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { id: "deprecation", findings };
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { checkGitHubTag } from "../utils/github.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the published version against GitHub releases/tags.
|
|
5
|
+
*/
|
|
6
|
+
export async function checkGithubVerify(ctx) {
|
|
7
|
+
const findings = [];
|
|
8
|
+
|
|
9
|
+
if (!ctx.repository) {
|
|
10
|
+
findings.push({
|
|
11
|
+
severity: "high",
|
|
12
|
+
message: "No repository URL in package metadata",
|
|
13
|
+
score: 4,
|
|
14
|
+
});
|
|
15
|
+
return { id: "github-verify", findings };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!ctx.repository.includes("github.com")) {
|
|
19
|
+
findings.push({
|
|
20
|
+
severity: "medium",
|
|
21
|
+
message: "Repository is not on GitHub — cannot verify tags",
|
|
22
|
+
score: 1,
|
|
23
|
+
});
|
|
24
|
+
return { id: "github-verify", findings };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await checkGitHubTag(ctx.repository, ctx.version);
|
|
28
|
+
|
|
29
|
+
if (result.error) {
|
|
30
|
+
findings.push({
|
|
31
|
+
severity: "medium",
|
|
32
|
+
message: `GitHub API: ${result.error}`,
|
|
33
|
+
score: 1,
|
|
34
|
+
});
|
|
35
|
+
return { id: "github-verify", findings };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!result.tagFound) {
|
|
39
|
+
findings.push({
|
|
40
|
+
severity: "high",
|
|
41
|
+
message: `No GitHub tag found for version ${ctx.version}`,
|
|
42
|
+
score: 4,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
findings.push({
|
|
46
|
+
severity: "info",
|
|
47
|
+
message: `GitHub tag found for v${ctx.version}`,
|
|
48
|
+
score: 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!result.recentCommits) {
|
|
53
|
+
findings.push({
|
|
54
|
+
severity: "medium",
|
|
55
|
+
message: "No recent commits in the last 90 days",
|
|
56
|
+
score: 2,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
findings.push({
|
|
60
|
+
severity: "info",
|
|
61
|
+
message: "Repository has recent activity",
|
|
62
|
+
score: 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { id: "github-verify", findings };
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { checkRecentPublish } from "./recentPublish.js";
|
|
2
|
+
export { checkDependencyDiff } from "./dependencyDiff.js";
|
|
3
|
+
export { checkScripts } from "./scripts.js";
|
|
4
|
+
export { checkTyposquat } from "./typosquat.js";
|
|
5
|
+
export { checkMaintainers } from "./maintainers.js";
|
|
6
|
+
export { checkLicense } from "./license.js";
|
|
7
|
+
export { checkGithubVerify } from "./githubVerify.js";
|
|
8
|
+
export { checkDeprecation } from "./deprecation.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const PERMISSIVE = [
|
|
2
|
+
"MIT", "ISC", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "0BSD", "Unlicense",
|
|
3
|
+
"CC0-1.0", "BlueOak-1.0.0",
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks license status.
|
|
8
|
+
*/
|
|
9
|
+
export function checkLicense(ctx) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
const raw = ctx.license;
|
|
12
|
+
const license = typeof raw === "string" ? raw : raw?.type || "Unknown";
|
|
13
|
+
|
|
14
|
+
if (license === "Unknown" || license === "UNLICENSED") {
|
|
15
|
+
findings.push({
|
|
16
|
+
severity: "high",
|
|
17
|
+
message: "No license specified",
|
|
18
|
+
score: 3,
|
|
19
|
+
});
|
|
20
|
+
} else if (PERMISSIVE.includes(license)) {
|
|
21
|
+
findings.push({
|
|
22
|
+
severity: "info",
|
|
23
|
+
message: `License: ${license}`,
|
|
24
|
+
score: 0,
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
findings.push({
|
|
28
|
+
severity: "medium",
|
|
29
|
+
message: `Non-standard license: ${license}`,
|
|
30
|
+
score: 1,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { id: "license", findings };
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes maintainer signals.
|
|
3
|
+
*/
|
|
4
|
+
export function checkMaintainers(ctx) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const maintainers = ctx.currentMaintainers || [];
|
|
7
|
+
const count = maintainers.length;
|
|
8
|
+
|
|
9
|
+
if (count === 0) {
|
|
10
|
+
findings.push({
|
|
11
|
+
severity: "high",
|
|
12
|
+
message: "No maintainers listed",
|
|
13
|
+
score: 3,
|
|
14
|
+
});
|
|
15
|
+
} else if (count === 1) {
|
|
16
|
+
findings.push({
|
|
17
|
+
severity: "medium",
|
|
18
|
+
message: `Single maintainer: ${maintainers[0].name || maintainers[0].email || "unknown"}`,
|
|
19
|
+
score: 1,
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
findings.push({
|
|
23
|
+
severity: "info",
|
|
24
|
+
message: `${count} maintainers`,
|
|
25
|
+
score: 0,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { id: "maintainers", findings };
|
|
30
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects recently published versions.
|
|
3
|
+
* Supply chain attacks often use hot-off-the-press publishes.
|
|
4
|
+
*/
|
|
5
|
+
export function checkRecentPublish(ctx) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
|
|
8
|
+
if (!ctx.publishedAt) {
|
|
9
|
+
return { id: "recent-publish", findings };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const hoursAgo = (Date.now() - new Date(ctx.publishedAt).getTime()) / (1000 * 60 * 60);
|
|
13
|
+
const daysAgo = hoursAgo / 24;
|
|
14
|
+
|
|
15
|
+
if (hoursAgo < 24) {
|
|
16
|
+
findings.push({
|
|
17
|
+
severity: "critical",
|
|
18
|
+
message: `Version ${ctx.version} was published ${Math.round(hoursAgo)} hours ago`,
|
|
19
|
+
score: 5,
|
|
20
|
+
});
|
|
21
|
+
} else if (daysAgo < 7) {
|
|
22
|
+
findings.push({
|
|
23
|
+
severity: "high",
|
|
24
|
+
message: `Version ${ctx.version} was published ${Math.round(daysAgo)} days ago`,
|
|
25
|
+
score: 2,
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
findings.push({
|
|
29
|
+
severity: "info",
|
|
30
|
+
message: `Published ${Math.round(daysAgo)} days ago`,
|
|
31
|
+
score: 0,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check for unusual version jumps
|
|
36
|
+
if (ctx.previousVersion && ctx.version) {
|
|
37
|
+
const cur = ctx.version.split(".").map(Number);
|
|
38
|
+
const prev = ctx.previousVersion.split(".").map(Number);
|
|
39
|
+
|
|
40
|
+
if (cur.length === 3 && prev.length === 3) {
|
|
41
|
+
const majorJump = cur[0] - prev[0];
|
|
42
|
+
const minorJump = cur[1] - prev[1];
|
|
43
|
+
|
|
44
|
+
if (majorJump > 1 || (majorJump === 0 && minorJump > 5)) {
|
|
45
|
+
findings.push({
|
|
46
|
+
severity: "high",
|
|
47
|
+
message: `Unusual version jump: ${ctx.previousVersion} → ${ctx.version}`,
|
|
48
|
+
score: 3,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Rapid publish cadence — multiple versions in 24h is suspicious
|
|
55
|
+
if (ctx.publishedAt && ctx.previousPublishedAt) {
|
|
56
|
+
const gap =
|
|
57
|
+
new Date(ctx.publishedAt).getTime() -
|
|
58
|
+
new Date(ctx.previousPublishedAt).getTime();
|
|
59
|
+
const gapHours = gap / (1000 * 60 * 60);
|
|
60
|
+
if (gapHours > 0 && gapHours < 2) {
|
|
61
|
+
findings.push({
|
|
62
|
+
severity: "high",
|
|
63
|
+
message: `Two versions published within ${Math.round(gapHours * 60)} minutes`,
|
|
64
|
+
score: 3,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { id: "recent-publish", findings };
|
|
70
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects lifecycle scripts that are common attack vectors.
|
|
3
|
+
*/
|
|
4
|
+
export function checkScripts(ctx) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const scripts = ctx.scripts || {};
|
|
7
|
+
|
|
8
|
+
const dangerous = ["preinstall", "install", "postinstall"];
|
|
9
|
+
const found = dangerous.filter((s) => scripts[s]);
|
|
10
|
+
|
|
11
|
+
if (found.length > 0) {
|
|
12
|
+
findings.push({
|
|
13
|
+
severity: "critical",
|
|
14
|
+
message: `Lifecycle scripts found: ${found.join(", ")}`,
|
|
15
|
+
score: 5,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Inspect script content for red flags
|
|
19
|
+
for (const name of found) {
|
|
20
|
+
const cmd = scripts[name];
|
|
21
|
+
if (
|
|
22
|
+
cmd.includes("curl ") ||
|
|
23
|
+
cmd.includes("wget ") ||
|
|
24
|
+
cmd.includes("eval(") ||
|
|
25
|
+
cmd.includes("base64") ||
|
|
26
|
+
cmd.includes("exec(") ||
|
|
27
|
+
cmd.includes("child_process")
|
|
28
|
+
) {
|
|
29
|
+
findings.push({
|
|
30
|
+
severity: "critical",
|
|
31
|
+
message: `Script "${name}" contains suspicious command: ${cmd.slice(0, 80)}`,
|
|
32
|
+
score: 5,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
findings.push({
|
|
38
|
+
severity: "info",
|
|
39
|
+
message: "No lifecycle scripts",
|
|
40
|
+
score: 0,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { id: "scripts", findings };
|
|
45
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
"crypto-js", "crypto", "http-proxy", "http-server",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function levenshtein(a, b) {
|
|
19
|
+
const m = a.length;
|
|
20
|
+
const n = b.length;
|
|
21
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
24
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
25
|
+
|
|
26
|
+
for (let i = 1; i <= m; i++) {
|
|
27
|
+
for (let j = 1; j <= n; j++) {
|
|
28
|
+
dp[i][j] =
|
|
29
|
+
a[i - 1] === b[j - 1]
|
|
30
|
+
? dp[i - 1][j - 1]
|
|
31
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return dp[m][n];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the popular package name this looks like, or null.
|
|
40
|
+
*/
|
|
41
|
+
export function checkTyposquatName(name) {
|
|
42
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, ""); // strip scope
|
|
43
|
+
if (POPULAR_PACKAGES.includes(lower)) return null;
|
|
44
|
+
|
|
45
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
46
|
+
const dist = levenshtein(lower, popular);
|
|
47
|
+
if (dist > 0 && dist <= 2 && Math.abs(lower.length - popular.length) <= 2) {
|
|
48
|
+
return popular;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check module: wraps the typosquat check as a pipeline-compatible check.
|
|
57
|
+
*/
|
|
58
|
+
export function checkTyposquat(ctx) {
|
|
59
|
+
const findings = [];
|
|
60
|
+
const match = checkTyposquatName(ctx.name);
|
|
61
|
+
|
|
62
|
+
if (match) {
|
|
63
|
+
findings.push({
|
|
64
|
+
severity: "critical",
|
|
65
|
+
message: `Package name is suspiciously similar to "${match}"`,
|
|
66
|
+
score: 5,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
findings.push({
|
|
70
|
+
severity: "info",
|
|
71
|
+
message: "No typosquatting detected",
|
|
72
|
+
score: 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { id: "typosquat", findings };
|
|
77
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
const W = 58;
|
|
4
|
+
const SEPARATOR = chalk.gray("━".repeat(W));
|
|
5
|
+
const THIN_SEP = chalk.gray("─".repeat(W));
|
|
6
|
+
|
|
7
|
+
function riskBar(score) {
|
|
8
|
+
const filled = score;
|
|
9
|
+
const empty = 10 - score;
|
|
10
|
+
const color =
|
|
11
|
+
score <= 2 ? chalk.green : score <= 5 ? chalk.yellow : score <= 7 ? chalk.red : chalk.redBright;
|
|
12
|
+
return color("█".repeat(filled)) + chalk.gray("░".repeat(empty));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function riskLabel(label) {
|
|
16
|
+
switch (label) {
|
|
17
|
+
case "LOW":
|
|
18
|
+
return chalk.green.bold("✅ LOW");
|
|
19
|
+
case "MEDIUM":
|
|
20
|
+
return chalk.yellow.bold("⚠ MEDIUM");
|
|
21
|
+
case "HIGH":
|
|
22
|
+
return chalk.red.bold("🚨 HIGH");
|
|
23
|
+
case "CRITICAL":
|
|
24
|
+
return chalk.redBright.bold("💀 CRITICAL");
|
|
25
|
+
default:
|
|
26
|
+
return label;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function severityIcon(severity) {
|
|
31
|
+
switch (severity) {
|
|
32
|
+
case "critical":
|
|
33
|
+
return chalk.redBright("✘");
|
|
34
|
+
case "high":
|
|
35
|
+
return chalk.red("✘");
|
|
36
|
+
case "medium":
|
|
37
|
+
return chalk.yellow("⚠");
|
|
38
|
+
case "info":
|
|
39
|
+
return chalk.green("✔");
|
|
40
|
+
default:
|
|
41
|
+
return chalk.gray("·");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function severityColor(severity) {
|
|
46
|
+
switch (severity) {
|
|
47
|
+
case "critical":
|
|
48
|
+
return chalk.redBright;
|
|
49
|
+
case "high":
|
|
50
|
+
return chalk.red;
|
|
51
|
+
case "medium":
|
|
52
|
+
return chalk.yellow;
|
|
53
|
+
case "info":
|
|
54
|
+
return chalk.green;
|
|
55
|
+
default:
|
|
56
|
+
return chalk.white;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function timeAgo(dateStr) {
|
|
61
|
+
if (!dateStr) return "Unknown";
|
|
62
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
63
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
64
|
+
if (hours < 1) return "just now";
|
|
65
|
+
if (hours < 24) return `${hours} hour(s) ago`;
|
|
66
|
+
const days = Math.floor(hours / 24);
|
|
67
|
+
if (days === 1) return "1 day ago";
|
|
68
|
+
if (days < 30) return `${days} days ago`;
|
|
69
|
+
const months = Math.floor(days / 30);
|
|
70
|
+
if (months < 12) return `${months} month(s) ago`;
|
|
71
|
+
const years = Math.floor(days / 365);
|
|
72
|
+
return `${years} year(s) ago`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Formats the full pipeline result for CLI display.
|
|
77
|
+
*/
|
|
78
|
+
export function formatPipelineResult(result) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(SEPARATOR);
|
|
83
|
+
lines.push(
|
|
84
|
+
chalk.bold(` 📦 ${result.package} `) + chalk.gray(`v${result.version}`)
|
|
85
|
+
);
|
|
86
|
+
if (result.description) {
|
|
87
|
+
lines.push(chalk.gray(` ${result.description.slice(0, W - 4)}`));
|
|
88
|
+
}
|
|
89
|
+
lines.push(SEPARATOR);
|
|
90
|
+
|
|
91
|
+
// ── Risk Score ──────────────────────────────
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push(
|
|
94
|
+
` Risk Level: ${riskLabel(result.label)} ${chalk.bold(`(${result.score}/10)`)}`
|
|
95
|
+
);
|
|
96
|
+
lines.push(` ${riskBar(result.score)}`);
|
|
97
|
+
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(THIN_SEP);
|
|
100
|
+
|
|
101
|
+
// ── Package Info ────────────────────────────
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(chalk.bold(" 📊 Package Info"));
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
106
|
+
const info = [
|
|
107
|
+
["Downloads (weekly)", result.downloads.toLocaleString()],
|
|
108
|
+
["Maintainers", result.maintainers.join(", ") || "None"],
|
|
109
|
+
["License", typeof result.license === "string" ? result.license : result.license?.type || "Unknown"],
|
|
110
|
+
["Published", timeAgo(result.publishedAt)],
|
|
111
|
+
["Dependencies", String(result.dependencyCount)],
|
|
112
|
+
["Total Versions", String(result.totalVersions)],
|
|
113
|
+
["Repository", result.repository ? "✔" : "✘ None"],
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const [label, value] of info) {
|
|
117
|
+
lines.push(` ${chalk.gray(label.padEnd(22))} ${chalk.white(value)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (result.deprecated) {
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(
|
|
123
|
+
chalk.red.bold(
|
|
124
|
+
` ⚠ DEPRECATED: ${typeof result.deprecated === "string" ? result.deprecated : "This package is deprecated"}`
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push(THIN_SEP);
|
|
131
|
+
|
|
132
|
+
// ── Findings ────────────────────────────────
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push(chalk.bold(" 🔍 Findings"));
|
|
135
|
+
lines.push("");
|
|
136
|
+
|
|
137
|
+
// Group by severity
|
|
138
|
+
const critical = result.findings.filter((f) => f.severity === "critical");
|
|
139
|
+
const high = result.findings.filter((f) => f.severity === "high");
|
|
140
|
+
const medium = result.findings.filter((f) => f.severity === "medium");
|
|
141
|
+
const info2 = result.findings.filter((f) => f.severity === "info");
|
|
142
|
+
|
|
143
|
+
for (const group of [critical, high, medium, info2]) {
|
|
144
|
+
for (const f of group) {
|
|
145
|
+
const icon = severityIcon(f.severity);
|
|
146
|
+
const color = severityColor(f.severity);
|
|
147
|
+
lines.push(` ${icon} ${color(f.message)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Recommendation ──────────────────────────
|
|
152
|
+
if (result.recommendation) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(THIN_SEP);
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(chalk.bold(" 💡 Recommendation"));
|
|
157
|
+
lines.push("");
|
|
158
|
+
if (result.label === "CRITICAL" || result.label === "HIGH") {
|
|
159
|
+
lines.push(chalk.red(` ⛔ Avoid installing ${result.package}@${result.version}`));
|
|
160
|
+
}
|
|
161
|
+
lines.push(
|
|
162
|
+
chalk.green(
|
|
163
|
+
` ✔ Safe alternative: ${result.package}@${chalk.bold(result.recommendation.version)}`
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
lines.push(
|
|
167
|
+
chalk.gray(` ${result.recommendation.reason}`)
|
|
168
|
+
);
|
|
169
|
+
} else if (result.label === "CRITICAL" || result.label === "HIGH") {
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push(THIN_SEP);
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push(chalk.bold(" 💡 Recommendation"));
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push(chalk.red(` ⛔ Avoid installing this version`));
|
|
176
|
+
lines.push(chalk.gray(" No safe alternative version found"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push("");
|
|
180
|
+
lines.push(SEPARATOR);
|
|
181
|
+
lines.push("");
|
|
182
|
+
|
|
183
|
+
return lines.join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Formats a summary table for scan results.
|
|
188
|
+
*/
|
|
189
|
+
export function formatScanSummary(results) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
|
|
192
|
+
results.sort((a, b) => b.score - a.score);
|
|
193
|
+
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push(SEPARATOR);
|
|
196
|
+
lines.push(chalk.bold(" 📋 Dependency Scan Summary"));
|
|
197
|
+
lines.push(SEPARATOR);
|
|
198
|
+
lines.push("");
|
|
199
|
+
|
|
200
|
+
const nameWidth = Math.max(20, ...results.map((r) => r.package.length + 2));
|
|
201
|
+
|
|
202
|
+
lines.push(
|
|
203
|
+
chalk.gray(
|
|
204
|
+
" " +
|
|
205
|
+
"Package".padEnd(nameWidth) +
|
|
206
|
+
"Score".padEnd(10) +
|
|
207
|
+
"Level"
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
lines.push(chalk.gray(" " + "─".repeat(nameWidth + 24)));
|
|
211
|
+
|
|
212
|
+
for (const r of results) {
|
|
213
|
+
const name = r.package.padEnd(nameWidth);
|
|
214
|
+
const score = `${r.score}/10`.padEnd(10);
|
|
215
|
+
let level;
|
|
216
|
+
switch (r.label) {
|
|
217
|
+
case "LOW":
|
|
218
|
+
level = chalk.green("✅ Low");
|
|
219
|
+
break;
|
|
220
|
+
case "MEDIUM":
|
|
221
|
+
level = chalk.yellow("⚠ Medium");
|
|
222
|
+
break;
|
|
223
|
+
case "HIGH":
|
|
224
|
+
level = chalk.red("🚨 High");
|
|
225
|
+
break;
|
|
226
|
+
case "CRITICAL":
|
|
227
|
+
level = chalk.redBright("💀 Critical");
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const colorFn =
|
|
232
|
+
r.label === "CRITICAL" || r.label === "HIGH"
|
|
233
|
+
? chalk.red
|
|
234
|
+
: r.label === "MEDIUM"
|
|
235
|
+
? chalk.yellow
|
|
236
|
+
: chalk.white;
|
|
237
|
+
lines.push(` ${colorFn(name)}${score}${level}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
lines.push("");
|
|
241
|
+
lines.push(THIN_SEP);
|
|
242
|
+
|
|
243
|
+
const low = results.filter((r) => r.label === "LOW").length;
|
|
244
|
+
const med = results.filter((r) => r.label === "MEDIUM").length;
|
|
245
|
+
const high = results.filter((r) => r.label === "HIGH").length;
|
|
246
|
+
const crit = results.filter((r) => r.label === "CRITICAL").length;
|
|
247
|
+
|
|
248
|
+
lines.push(
|
|
249
|
+
` ${chalk.bold("Total:")} ${results.length} packages ` +
|
|
250
|
+
`${chalk.green(`${low} low`)} · ${chalk.yellow(`${med} medium`)} · ` +
|
|
251
|
+
`${chalk.red(`${high} high`)} · ${chalk.redBright(`${crit} critical`)}`
|
|
252
|
+
);
|
|
253
|
+
lines.push("");
|
|
254
|
+
lines.push(SEPARATOR);
|
|
255
|
+
lines.push("");
|
|
256
|
+
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
package/src/index.js
CHANGED