break-anything 0.1.0 → 0.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/README.md +126 -155
- package/bin/brk.js +21 -2
- package/docs/assets/hero.gif +0 -0
- package/docs/hero-gif.md +32 -34
- package/docs/launch.md +68 -80
- package/docs/mcp-tools.md +176 -33
- package/docs/readme/README_zh.md +89 -126
- package/docs/roadmap.md +51 -0
- package/package.json +5 -5
- package/src/http.js +1 -1
- package/src/mcp.js +21 -1
- package/src/report.js +141 -40
- package/src/sast.js +128 -0
package/src/report.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Human-readable terminal reports. No third-party UI deps.
|
|
2
|
+
// English by default; set BRK_LANG=zh for Chinese labels.
|
|
3
|
+
|
|
4
|
+
const LANG = (process.env.BRK_LANG || "en").toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
2
5
|
|
|
3
6
|
const C = {
|
|
4
7
|
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
@@ -6,55 +9,114 @@ const C = {
|
|
|
6
9
|
};
|
|
7
10
|
|
|
8
11
|
const SEV_COLOR = { CRITICAL: C.red, HIGH: C.red, MEDIUM: C.yellow, LOW: C.cyan, UNKNOWN: C.gray };
|
|
9
|
-
const REACH_LABEL = {
|
|
10
|
-
imported: "reachable(已被引用)",
|
|
11
|
-
direct: "direct(直接依赖)",
|
|
12
|
-
transitive: "transitive(传递依赖)",
|
|
13
|
-
unknown: "—",
|
|
14
|
-
};
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const T = {
|
|
14
|
+
en: {
|
|
15
|
+
depsTitle: "Break Anything · dependency scan",
|
|
16
|
+
depsTarget: (t, n, s) => `target ${t} · ${n} packages scanned · ${s}`,
|
|
17
|
+
depsClean: "✓ No matching vulnerabilities in the OSV database.",
|
|
18
|
+
cleanNote: " Note: this means \"none found\", not \"safe\".",
|
|
19
|
+
reach: { imported: "reachable", direct: "direct dep", transitive: "transitive", unknown: "—" },
|
|
20
|
+
epss: (v) => (v != null ? `EPSS ${(v * 100).toFixed(1)}%` : "EPSS —"),
|
|
21
|
+
fix: (v) => `fix → ${v}`,
|
|
22
|
+
noFix: "no fix available",
|
|
23
|
+
summary: "summary",
|
|
24
|
+
findings: "findings",
|
|
25
|
+
reachable: "reachable",
|
|
26
|
+
skipped: (n) => `(${n} packages failed to query, skipped)`,
|
|
27
|
+
heuristic: "Reachability is a heuristic (is it imported), not proof the vulnerable code runs.",
|
|
28
|
+
secretsTitle: "Break Anything · secret scan",
|
|
29
|
+
secretsTarget: (t, on) => `target ${t} · history scan ${on ? "on" : "off"}`,
|
|
30
|
+
secretsClean: "✓ No leaked secrets found.",
|
|
31
|
+
history: "git history",
|
|
32
|
+
inHistory: "in history",
|
|
33
|
+
rotate: "Secrets in history may still be live even after deletion — rotate them, don't just delete the commit.",
|
|
34
|
+
imageTitle: "Break Anything · image scan",
|
|
35
|
+
imageClean: "✓ No known vulnerabilities in the image.",
|
|
36
|
+
more: (n) => `… ${n} more, use --json for the full list`,
|
|
37
|
+
fixable: "fixable",
|
|
38
|
+
sastTitle: "Break Anything · static code scan",
|
|
39
|
+
sastClean: "✓ No issues found by the static-analysis rules.",
|
|
40
|
+
},
|
|
41
|
+
zh: {
|
|
42
|
+
depsTitle: "Break Anything · 依赖扫描",
|
|
43
|
+
depsTarget: (t, n, s) => `目标 ${t} · 已扫 ${n} 个包 · ${s}`,
|
|
44
|
+
depsClean: "✓ 未在已知漏洞库(OSV)中发现匹配的漏洞。",
|
|
45
|
+
cleanNote: " 注意:这表示\"未发现\",不等于\"安全\"。",
|
|
46
|
+
reach: { imported: "可达(已被引用)", direct: "直接依赖", transitive: "传递依赖", unknown: "—" },
|
|
47
|
+
epss: (v) => (v != null ? `EPSS ${(v * 100).toFixed(1)}%` : "EPSS —"),
|
|
48
|
+
fix: (v) => `修复 → ${v}`,
|
|
49
|
+
noFix: "无修复版本",
|
|
50
|
+
summary: "小结",
|
|
51
|
+
findings: "条",
|
|
52
|
+
reachable: "可达",
|
|
53
|
+
skipped: (n) => `(${n} 个包查询失败,已跳过)`,
|
|
54
|
+
heuristic: "可达性为启发式(判断是否被引用),非漏洞函数被调用的证明。",
|
|
55
|
+
secretsTitle: "Break Anything · 密钥扫描",
|
|
56
|
+
secretsTarget: (t, on) => `目标 ${t} · 历史扫描 ${on ? "开" : "关"}`,
|
|
57
|
+
secretsClean: "✓ 未发现疑似泄露的密钥。",
|
|
58
|
+
history: "git 历史",
|
|
59
|
+
inHistory: "历史中",
|
|
60
|
+
rotate: "历史里的密钥即使已删除也可能仍然有效 —— 请轮换(rotate),不要只删提交。",
|
|
61
|
+
imageTitle: "Break Anything · 镜像扫描",
|
|
62
|
+
imageClean: "✓ 未在镜像中发现已知漏洞。",
|
|
63
|
+
more: (n) => `… 还有 ${n} 条,用 --json 查看全部`,
|
|
64
|
+
fixable: "可修复",
|
|
65
|
+
sastTitle: "Break Anything · 静态代码扫描",
|
|
66
|
+
sastClean: "✓ 静态分析规则未发现问题。",
|
|
67
|
+
},
|
|
68
|
+
}[LANG];
|
|
69
|
+
|
|
70
|
+
function palette(color) {
|
|
71
|
+
return color ? C : new Proxy({}, { get: () => "" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderHuman(result, { color = true, top } = {}) {
|
|
75
|
+
const c = palette(color);
|
|
18
76
|
const lines = [];
|
|
19
77
|
|
|
20
78
|
if (!result.ok) {
|
|
21
79
|
lines.push(`${c.yellow}${result.message}${c.reset}`);
|
|
22
|
-
lines.push(`${c.gray}
|
|
80
|
+
lines.push(`${c.gray}${result.target}${c.reset}`);
|
|
23
81
|
return lines.join("\n");
|
|
24
82
|
}
|
|
25
83
|
|
|
26
84
|
const srcDesc = result.sources.map((s) => `${s.ecosystem}:${s.packages}`).join(" ");
|
|
27
|
-
lines.push(`${c.bold}
|
|
28
|
-
lines.push(`${c.gray}
|
|
85
|
+
lines.push(`${c.bold}${T.depsTitle}${c.reset}`);
|
|
86
|
+
lines.push(`${c.gray}${T.depsTarget(result.target, result.scanned, srcDesc)}${c.reset}`);
|
|
29
87
|
lines.push("");
|
|
30
88
|
|
|
31
89
|
if (!result.findings.length) {
|
|
32
|
-
lines.push(`${c.green}
|
|
33
|
-
lines.push(`${c.gray}
|
|
90
|
+
lines.push(`${c.green}${T.depsClean}${c.reset}`);
|
|
91
|
+
lines.push(`${c.gray}${T.cleanNote}${c.reset}`);
|
|
34
92
|
return lines.join("\n");
|
|
35
93
|
}
|
|
36
94
|
|
|
37
|
-
|
|
95
|
+
const shown = top && result.findings.length > top ? result.findings.slice(0, top) : result.findings;
|
|
96
|
+
for (const f of shown) {
|
|
38
97
|
const sev = `${SEV_COLOR[f.severity] || ""}${f.severity.padEnd(8)}${c.reset}`;
|
|
39
98
|
const idLabel = f.cve || f.id;
|
|
40
|
-
const
|
|
41
|
-
|
|
99
|
+
const fix = f.fixedVersions.length
|
|
100
|
+
? `${c.green}${T.fix(f.fixedVersions.join(", "))}${c.reset}`
|
|
101
|
+
: `${c.red}${T.noFix}${c.reset}`;
|
|
42
102
|
lines.push(`${sev} ${c.bold}${f.package}@${f.version}${c.reset} ${c.gray}(${f.ecosystem})${c.reset}`);
|
|
43
|
-
lines.push(` ${c.cyan}${idLabel}${c.reset} · ${
|
|
103
|
+
lines.push(` ${c.cyan}${idLabel}${c.reset} · ${T.reach[f.reachability]} · ${T.epss(f.epss)} · ${fix}`);
|
|
44
104
|
if (f.summary) lines.push(` ${c.gray}${f.summary.replace(/\s+/g, " ").slice(0, 120)}${c.reset}`);
|
|
45
105
|
lines.push("");
|
|
46
106
|
}
|
|
107
|
+
if (shown.length < result.findings.length) {
|
|
108
|
+
lines.push(`${c.gray}${T.more(result.findings.length - shown.length)}${c.reset}`);
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
47
111
|
|
|
48
112
|
const s = result.summary;
|
|
49
113
|
const sevParts = Object.entries(s.bySeverity)
|
|
50
114
|
.sort((a, b) => sevOrder(b[0]) - sevOrder(a[0]))
|
|
51
115
|
.map(([k, v]) => `${k} ${v}`)
|
|
52
116
|
.join(" ");
|
|
53
|
-
lines.push(`${c.bold}
|
|
54
|
-
if (result.errors?.length) {
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
lines.push(`${c.gray}可达性为启发式(判断是否被引用),非漏洞函数被调用的证明。${c.reset}`);
|
|
117
|
+
lines.push(`${c.bold}${T.summary}${c.reset} ${s.total} ${T.findings} · ${s.reachable} ${T.reachable} · ${sevParts}`);
|
|
118
|
+
if (result.errors?.length) lines.push(`${c.gray}${T.skipped(result.errors.length)}${c.reset}`);
|
|
119
|
+
lines.push(`${c.gray}${T.heuristic}${c.reset}`);
|
|
58
120
|
return lines.join("\n");
|
|
59
121
|
}
|
|
60
122
|
|
|
@@ -62,24 +124,24 @@ function sevOrder(k) {
|
|
|
62
124
|
return { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, UNKNOWN: 0 }[k] ?? 0;
|
|
63
125
|
}
|
|
64
126
|
|
|
65
|
-
// ----
|
|
127
|
+
// ---- secrets ----
|
|
66
128
|
export function renderSecrets(result, { color = true } = {}) {
|
|
67
|
-
const c = color
|
|
129
|
+
const c = palette(color);
|
|
68
130
|
const lines = [];
|
|
69
|
-
lines.push(`${c.bold}
|
|
70
|
-
lines.push(`${c.gray}
|
|
131
|
+
lines.push(`${c.bold}${T.secretsTitle}${c.reset}`);
|
|
132
|
+
lines.push(`${c.gray}${T.secretsTarget(result.target, result.historyScanned)}${c.reset}`);
|
|
71
133
|
lines.push("");
|
|
72
134
|
|
|
73
135
|
if (!result.findings.length) {
|
|
74
|
-
lines.push(`${c.green}
|
|
75
|
-
lines.push(`${c.gray}
|
|
136
|
+
lines.push(`${c.green}${T.secretsClean}${c.reset}`);
|
|
137
|
+
lines.push(`${c.gray}${T.cleanNote}${c.reset}`);
|
|
76
138
|
return lines.join("\n");
|
|
77
139
|
}
|
|
78
140
|
|
|
79
141
|
for (const f of result.findings) {
|
|
80
142
|
const sev = `${SEV_COLOR[f.severity] || ""}${f.severity.padEnd(8)}${c.reset}`;
|
|
81
143
|
const loc = f.source === "history"
|
|
82
|
-
? `${c.yellow}
|
|
144
|
+
? `${c.yellow}${T.history}${c.reset} ${c.gray}@${f.commit}${c.reset}`
|
|
83
145
|
: `${f.location}${f.line ? `:${f.line}` : ""}`;
|
|
84
146
|
lines.push(`${sev} ${c.bold}${f.rule}${c.reset}`);
|
|
85
147
|
lines.push(` ${loc} · ${c.gray}${f.match}${c.reset}`);
|
|
@@ -87,16 +149,16 @@ export function renderSecrets(result, { color = true } = {}) {
|
|
|
87
149
|
lines.push("");
|
|
88
150
|
const s = result.summary;
|
|
89
151
|
const parts = Object.entries(s.bySeverity).sort((a, b) => sevOrder(b[0]) - sevOrder(a[0])).map(([k, v]) => `${k} ${v}`).join(" ");
|
|
90
|
-
lines.push(`${c.bold}
|
|
91
|
-
if (s.inHistory) lines.push(`${c.yellow}
|
|
152
|
+
lines.push(`${c.bold}${T.summary}${c.reset} ${s.total} ${T.findings} · ${s.inHistory} ${T.inHistory} · ${parts}`);
|
|
153
|
+
if (s.inHistory) lines.push(`${c.yellow}${T.rotate}${c.reset}`);
|
|
92
154
|
return lines.join("\n");
|
|
93
155
|
}
|
|
94
156
|
|
|
95
|
-
// ----
|
|
157
|
+
// ---- image ----
|
|
96
158
|
export function renderImage(result, { color = true } = {}) {
|
|
97
|
-
const c = color
|
|
159
|
+
const c = palette(color);
|
|
98
160
|
const lines = [];
|
|
99
|
-
lines.push(`${c.bold}
|
|
161
|
+
lines.push(`${c.bold}${T.imageTitle}${c.reset} ${c.gray}(${result.engine})${c.reset}`);
|
|
100
162
|
|
|
101
163
|
if (!result.ok) {
|
|
102
164
|
lines.push("");
|
|
@@ -104,21 +166,60 @@ export function renderImage(result, { color = true } = {}) {
|
|
|
104
166
|
return lines.join("\n");
|
|
105
167
|
}
|
|
106
168
|
|
|
107
|
-
lines.push(`${c.gray}
|
|
169
|
+
lines.push(`${c.gray}${result.target}${c.reset}`);
|
|
108
170
|
lines.push("");
|
|
109
171
|
if (!result.findings.length) {
|
|
110
|
-
lines.push(`${c.green}
|
|
172
|
+
lines.push(`${c.green}${T.imageClean}${c.reset}`);
|
|
111
173
|
return lines.join("\n");
|
|
112
174
|
}
|
|
113
175
|
for (const f of result.findings.slice(0, 40)) {
|
|
114
176
|
const sev = `${SEV_COLOR[f.severity] || ""}${f.severity.padEnd(8)}${c.reset}`;
|
|
115
|
-
const fix = f.fixedVersions.length ? `${c.green}
|
|
177
|
+
const fix = f.fixedVersions.length ? `${c.green}${T.fix(f.fixedVersions.join(", "))}${c.reset}` : `${c.red}${T.noFix}${c.reset}`;
|
|
116
178
|
lines.push(`${sev} ${c.bold}${f.package}@${f.version}${c.reset} ${c.cyan}${f.cve || f.id}${c.reset} ${fix} ${c.gray}[${f.class}]${c.reset}`);
|
|
117
179
|
}
|
|
118
|
-
if (result.findings.length > 40) lines.push(`${c.gray}
|
|
180
|
+
if (result.findings.length > 40) lines.push(`${c.gray}${T.more(result.findings.length - 40)}${c.reset}`);
|
|
119
181
|
lines.push("");
|
|
120
182
|
const s = result.summary;
|
|
121
183
|
const parts = Object.entries(s.bySeverity).sort((a, b) => sevOrder(b[0]) - sevOrder(a[0])).map(([k, v]) => `${k} ${v}`).join(" ");
|
|
122
|
-
lines.push(`${c.bold}
|
|
184
|
+
lines.push(`${c.bold}${T.summary}${c.reset} ${s.total} ${T.findings} · ${s.fixable} ${T.fixable} · ${parts}`);
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- SAST ----
|
|
189
|
+
export function renderSast(result, { color = true, top } = {}) {
|
|
190
|
+
const c = palette(color);
|
|
191
|
+
const lines = [];
|
|
192
|
+
lines.push(`${c.bold}${T.sastTitle}${c.reset} ${c.gray}(${result.engine})${c.reset}`);
|
|
193
|
+
|
|
194
|
+
if (!result.ok) {
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push(`${c.yellow}${result.message}${c.reset}`);
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push(`${c.gray}${result.target}${c.reset}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
if (!result.findings.length) {
|
|
203
|
+
lines.push(`${c.green}${T.sastClean}${c.reset}`);
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const shown = top && result.findings.length > top ? result.findings.slice(0, top) : result.findings;
|
|
208
|
+
for (const f of shown) {
|
|
209
|
+
const sev = `${SEV_COLOR[f.severity] || ""}${f.severity.padEnd(8)}${c.reset}`;
|
|
210
|
+
const tags = [f.cwe[0], f.owasp[0]].filter(Boolean).join(" · ");
|
|
211
|
+
lines.push(`${sev} ${c.bold}${f.rule}${c.reset} ${c.gray}${tags}${c.reset}`);
|
|
212
|
+
lines.push(` ${c.cyan}${f.location}${c.reset} ${c.gray}${f.message.slice(0, 100)}${c.reset}`);
|
|
213
|
+
if (f.snippet) lines.push(` ${c.gray}${f.snippet.slice(0, 90)}${c.reset}`);
|
|
214
|
+
lines.push("");
|
|
215
|
+
}
|
|
216
|
+
if (shown.length < result.findings.length) {
|
|
217
|
+
lines.push(`${c.gray}${T.more(result.findings.length - shown.length)}${c.reset}`);
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const s = result.summary;
|
|
222
|
+
const parts = Object.entries(s.bySeverity).sort((a, b) => sevOrder(b[0]) - sevOrder(a[0])).map(([k, v]) => `${k} ${v}`).join(" ");
|
|
223
|
+
lines.push(`${c.bold}${T.summary}${c.reset} ${s.total} ${T.findings} · ${parts}`);
|
|
123
224
|
return lines.join("\n");
|
|
124
225
|
}
|
package/src/sast.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// 静态代码分析(SAST):封装 Semgrep。装了 semgrep 就能用,没装给安装提示。
|
|
4
|
+
// 把 Semgrep 的 JSON 归一化成 Break Anything 统一的发现结构。
|
|
5
|
+
// 默认规则集 p/security-audit(首次运行会从 Semgrep registry 拉取并缓存,需联网)。
|
|
6
|
+
|
|
7
|
+
export async function scanSast(path, { config = "p/security-audit" } = {}) {
|
|
8
|
+
const semgrep = await which("semgrep");
|
|
9
|
+
if (!semgrep) {
|
|
10
|
+
return {
|
|
11
|
+
target: path,
|
|
12
|
+
ok: false,
|
|
13
|
+
engine: "semgrep",
|
|
14
|
+
message:
|
|
15
|
+
"未找到 semgrep。SAST 依赖 Semgrep。安装:\n" +
|
|
16
|
+
" pipx install semgrep (或 brew install semgrep)\n" +
|
|
17
|
+
" 文档:https://semgrep.dev/docs/getting-started/",
|
|
18
|
+
findings: [],
|
|
19
|
+
summary: { total: 0, bySeverity: {} },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await run(semgrep, ["--json", "--quiet", "--config", config, "--timeout", "0", path]);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return {
|
|
28
|
+
target: path,
|
|
29
|
+
ok: false,
|
|
30
|
+
engine: "semgrep",
|
|
31
|
+
message: `semgrep 执行失败:${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
+
findings: [],
|
|
33
|
+
summary: { total: 0, bySeverity: {} },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return normalizeSemgrep(raw, path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 导出以便单测:把 semgrep JSON 文本归一化
|
|
40
|
+
export function normalizeSemgrep(rawJson, target) {
|
|
41
|
+
let data;
|
|
42
|
+
try { data = typeof rawJson === "string" ? JSON.parse(rawJson) : rawJson; }
|
|
43
|
+
catch { return { target, ok: false, engine: "semgrep", message: "无法解析 semgrep 输出", findings: [], summary: { total: 0, bySeverity: {} } }; }
|
|
44
|
+
|
|
45
|
+
const findings = [];
|
|
46
|
+
for (const r of data.results || []) {
|
|
47
|
+
const sev = mapSeverity(r.extra?.severity);
|
|
48
|
+
const meta = r.extra?.metadata || {};
|
|
49
|
+
findings.push({
|
|
50
|
+
rule: shortRule(r.check_id),
|
|
51
|
+
id: r.check_id,
|
|
52
|
+
severity: sev,
|
|
53
|
+
location: `${r.path}:${r.start?.line ?? "?"}`,
|
|
54
|
+
message: (r.extra?.message || "").replace(/\s+/g, " ").trim(),
|
|
55
|
+
cwe: toArray(meta.cwe),
|
|
56
|
+
owasp: toArray(meta.owasp),
|
|
57
|
+
confidence: meta.confidence || null,
|
|
58
|
+
snippet: (r.extra?.lines || "").trim().slice(0, 200),
|
|
59
|
+
references: toArray(meta.references).slice(0, 3),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
findings.sort((a, b) => sevRank(b.severity) - sevRank(a.severity));
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
target,
|
|
66
|
+
ok: true,
|
|
67
|
+
engine: "semgrep",
|
|
68
|
+
scanned: data.paths?.scanned?.length ?? undefined,
|
|
69
|
+
errors: (data.errors || []).length,
|
|
70
|
+
findings,
|
|
71
|
+
summary: { total: findings.length, bySeverity: countBy(findings, "severity") },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Semgrep: ERROR / WARNING / INFO → 我们的严重度
|
|
76
|
+
function mapSeverity(s) {
|
|
77
|
+
switch ((s || "").toUpperCase()) {
|
|
78
|
+
case "ERROR": return "HIGH";
|
|
79
|
+
case "WARNING": return "MEDIUM";
|
|
80
|
+
case "INFO": return "LOW";
|
|
81
|
+
default: return "UNKNOWN";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shortRule(id) {
|
|
86
|
+
if (!id) return "rule";
|
|
87
|
+
const parts = id.split(".");
|
|
88
|
+
return parts.slice(-2).join(".");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toArray(v) {
|
|
92
|
+
if (v == null) return [];
|
|
93
|
+
return Array.isArray(v) ? v : [v];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function which(cmd) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const p = spawn(process.platform === "win32" ? "where" : "which", [cmd], { stdio: ["ignore", "pipe", "ignore"] });
|
|
99
|
+
let out = "";
|
|
100
|
+
p.stdout.on("data", (d) => (out += d));
|
|
101
|
+
p.on("close", (code) => resolve(code === 0 ? out.trim().split("\n")[0] : null));
|
|
102
|
+
p.on("error", () => resolve(null));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function run(cmd, args) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
109
|
+
let out = "";
|
|
110
|
+
let err = "";
|
|
111
|
+
p.stdout.on("data", (d) => (out += d));
|
|
112
|
+
p.stderr.on("data", (d) => (err += d));
|
|
113
|
+
p.on("close", (code) => {
|
|
114
|
+
// semgrep: 0 = 无发现, 1 = 有发现, 其它 = 错误
|
|
115
|
+
if (code === 0 || code === 1 || out.trim().startsWith("{")) resolve(out);
|
|
116
|
+
else reject(new Error(err.trim().slice(0, 300) || `exit ${code}`));
|
|
117
|
+
});
|
|
118
|
+
p.on("error", reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const SEV = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, UNKNOWN: 0 };
|
|
123
|
+
function sevRank(s) { return SEV[s] ?? 0; }
|
|
124
|
+
function countBy(arr, key) {
|
|
125
|
+
const m = {};
|
|
126
|
+
for (const x of arr) m[x[key]] = (m[x[key]] || 0) + 1;
|
|
127
|
+
return m;
|
|
128
|
+
}
|