codesentry 0.1.11 → 0.3.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 +89 -3
- package/dist/index.js +1688 -162
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { createRequire as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
+
import { createRequire as createRequire5 } from "module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import figlet from "figlet";
|
|
8
8
|
import gradient from "gradient-string";
|
|
@@ -45,13 +45,13 @@ var isExecImport = (specifier) => {
|
|
|
45
45
|
return specifier.type === "ImportSpecifier" && imported?.type === "Identifier" && EXEC_METHOD_NAMES.has(imported.name);
|
|
46
46
|
};
|
|
47
47
|
var isNamespaceImport = (specifier) => specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier";
|
|
48
|
+
var bindingSetForImportSpecifier = (specifier, bindings) => isExecImport(specifier) ? bindings.directCalls : isNamespaceImport(specifier) ? bindings.namespaces : void 0;
|
|
48
49
|
var collectImportBinding = (specifier, bindings) => {
|
|
49
50
|
const name = localName(specifier);
|
|
50
51
|
if (!name) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
bindingSet?.add(name);
|
|
54
|
+
bindingSetForImportSpecifier(specifier, bindings)?.add(name);
|
|
55
55
|
};
|
|
56
56
|
var collectFromImportDeclaration = (node, bindings) => {
|
|
57
57
|
if (!isChildProcessModuleSpecifier(node.source)) {
|
|
@@ -71,13 +71,15 @@ var collectDirectCallBindings = (id, bindings) => {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
+
var namespaceNameFromDeclaratorId = (id) => id?.type === "Identifier" ? id.name : void 0;
|
|
75
|
+
var destructuredBindingsFromDeclaratorId = (id) => id?.type === "ObjectPattern" ? id : void 0;
|
|
74
76
|
var collectFromVariableDeclarator = (node, bindings) => {
|
|
75
77
|
if (!isRequireCall(node.init)) {
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
const id = node.id;
|
|
79
|
-
const namespaceName = id
|
|
80
|
-
const destructuredBindings = id
|
|
81
|
+
const namespaceName = namespaceNameFromDeclaratorId(id);
|
|
82
|
+
const destructuredBindings = destructuredBindingsFromDeclaratorId(id);
|
|
81
83
|
namespaceName && bindings.namespaces.add(namespaceName);
|
|
82
84
|
destructuredBindings && collectDirectCallBindings(destructuredBindings, bindings);
|
|
83
85
|
};
|
|
@@ -92,11 +94,8 @@ var collectChildProcessBindings = (sourceFile) => {
|
|
|
92
94
|
});
|
|
93
95
|
return bindings;
|
|
94
96
|
};
|
|
95
|
-
var
|
|
96
|
-
if (callee
|
|
97
|
-
return bindings.directCalls.has(callee.name);
|
|
98
|
-
}
|
|
99
|
-
if (callee?.type !== "MemberExpression") {
|
|
97
|
+
var isKnownChildProcessMemberCallee = (callee, bindings) => {
|
|
98
|
+
if (callee.type !== "MemberExpression") {
|
|
100
99
|
return false;
|
|
101
100
|
}
|
|
102
101
|
const object = callee.object;
|
|
@@ -104,6 +103,12 @@ var isKnownChildProcessCallee = (callee, bindings) => {
|
|
|
104
103
|
const methodName = property?.type === "Identifier" ? property.name : void 0;
|
|
105
104
|
return !!methodName && EXEC_METHOD_NAMES.has(methodName) && object?.type === "Identifier" && bindings.namespaces.has(object.name);
|
|
106
105
|
};
|
|
106
|
+
var isKnownChildProcessCallee = (callee, bindings) => {
|
|
107
|
+
if (callee?.type === "Identifier") {
|
|
108
|
+
return bindings.directCalls.has(callee.name);
|
|
109
|
+
}
|
|
110
|
+
return callee !== void 0 && isKnownChildProcessMemberCallee(callee, bindings);
|
|
111
|
+
};
|
|
107
112
|
var isDynamicCommandArgument = (argument) => argument?.type === "StringLiteral" ? false : argument?.type === "TemplateLiteral" ? (argument.expressions?.length ?? 0) > 0 : argument !== void 0;
|
|
108
113
|
var isCommandInjectionCall = (node, bindings) => {
|
|
109
114
|
if (node.type !== "CallExpression") {
|
|
@@ -145,8 +150,8 @@ var commandInjectionRule = {
|
|
|
145
150
|
var withScanOptions = (command) => command.option("--json", "exibe o resultado em JSON").option("--tests", "inclui arquivos de teste na an\xE1lise (por padr\xE3o s\xE3o ignorados)");
|
|
146
151
|
|
|
147
152
|
// src/commands/scan/scan-runner.ts
|
|
148
|
-
import { writeFile } from "fs/promises";
|
|
149
|
-
import { join as
|
|
153
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
154
|
+
import { join as join4 } from "path";
|
|
150
155
|
import chalk2 from "chalk";
|
|
151
156
|
import { Listr } from "listr2";
|
|
152
157
|
|
|
@@ -181,13 +186,16 @@ var mergeScanResults = (nativeResult, semgrepResult) => ({
|
|
|
181
186
|
findings: [...nativeResult.findings, ...semgrepResult.findings],
|
|
182
187
|
durationMs: nativeResult.durationMs + semgrepResult.durationMs,
|
|
183
188
|
engines: {
|
|
189
|
+
...nativeResult.engines,
|
|
190
|
+
...semgrepResult.engines,
|
|
184
191
|
codesentry: nativeResult.engines?.codesentry ?? nativeResult.scannedFiles,
|
|
185
192
|
semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
|
|
186
|
-
}
|
|
193
|
+
},
|
|
194
|
+
warnings: [...nativeResult.warnings ?? [], ...semgrepResult.warnings ?? []]
|
|
187
195
|
});
|
|
188
|
-
var finalizeScanResult = (result) => ({
|
|
196
|
+
var finalizeScanResult = (result, dependencyAuditCoverage2 = false) => ({
|
|
189
197
|
...result,
|
|
190
|
-
engines: { ...result.engines, dependencyAudit:
|
|
198
|
+
engines: { ...result.engines, dependencyAudit: dependencyAuditCoverage2 },
|
|
191
199
|
warnings: result.engines?.semgrep === 0 ? [...result.warnings ?? [], ZERO_SEMGREP_COVERAGE_WARNING] : result.warnings
|
|
192
200
|
});
|
|
193
201
|
|
|
@@ -198,7 +206,21 @@ var SEVERITY_COLOR = {
|
|
|
198
206
|
high: (text2) => chalk.red(text2),
|
|
199
207
|
critical: (text2) => chalk.bgRed.white(text2)
|
|
200
208
|
};
|
|
201
|
-
var
|
|
209
|
+
var semgrepCoverage = (result) => result.engines?.semgrep === void 0 ? void 0 : `CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s)`;
|
|
210
|
+
var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) considerado(s)` : void 0;
|
|
211
|
+
var osvCoverage = (result) => result.engines?.osv ? `OSV.dev: ${result.engines.osv.checked}/${result.engines.osv.total} verificados` : void 0;
|
|
212
|
+
var nvdCoverage = (result) => {
|
|
213
|
+
const nvd = result.engines?.nvd;
|
|
214
|
+
if (!nvd || nvd.total === 0) return void 0;
|
|
215
|
+
return `NVD: ${nvd.total} CVE(s); ${nvd.enriched} enriquecido(s); ${nvd.notFound} sem resultado; ${nvd.failed} falha(s); ${nvd.cacheHits} cache hit(s)`;
|
|
216
|
+
};
|
|
217
|
+
var coverageParts = (result) => [semgrepCoverage(result), dependencyAuditCoverage(result), osvCoverage(result), nvdCoverage(result)].filter(
|
|
218
|
+
(part) => part !== void 0
|
|
219
|
+
);
|
|
220
|
+
var coverageText = (result) => {
|
|
221
|
+
const parts = coverageParts(result);
|
|
222
|
+
return parts.length ? ` ${parts.join("; ")}.` : "";
|
|
223
|
+
};
|
|
202
224
|
var printNotes = (result) => {
|
|
203
225
|
if (result.engines?.dependencyAudit === false) {
|
|
204
226
|
console.log(chalk.cyan(DEPENDENCY_AUDIT_NOTE));
|
|
@@ -211,9 +233,9 @@ var printCleanReport = (result, coverage) => {
|
|
|
211
233
|
console.log(chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`));
|
|
212
234
|
printNotes(result);
|
|
213
235
|
};
|
|
214
|
-
var findingsTable = (
|
|
236
|
+
var findingsTable = (findings) => {
|
|
215
237
|
const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
|
|
216
|
-
for (const finding of
|
|
238
|
+
for (const finding of findings) {
|
|
217
239
|
const colorize = SEVERITY_COLOR[finding.severity];
|
|
218
240
|
table.push([
|
|
219
241
|
colorize(finding.severity),
|
|
@@ -225,8 +247,132 @@ var findingsTable = (result) => {
|
|
|
225
247
|
}
|
|
226
248
|
return table;
|
|
227
249
|
};
|
|
250
|
+
var titleCaseMetric = (value) => value.toLowerCase().split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
251
|
+
var labeledLines = (pairs, format) => pairs.filter((pair) => Boolean(pair[1])).map(([label, value]) => format(label, value));
|
|
252
|
+
var cvssMetricLines = (cvss) => labeledLines(
|
|
253
|
+
[
|
|
254
|
+
["Vetor de ataque", cvss.attackVector],
|
|
255
|
+
["Complexidade", cvss.attackComplexity],
|
|
256
|
+
["Privil\xE9gios necess\xE1rios", cvss.privilegesRequired],
|
|
257
|
+
["Intera\xE7\xE3o do usu\xE1rio", cvss.userInteraction],
|
|
258
|
+
["Escopo", cvss.scope],
|
|
259
|
+
["Confidentiality", cvss.confidentialityImpact],
|
|
260
|
+
["Integrity", cvss.integrityImpact],
|
|
261
|
+
["Availability", cvss.availabilityImpact]
|
|
262
|
+
],
|
|
263
|
+
(label, value) => ` ${label}: ${titleCaseMetric(value)}`
|
|
264
|
+
);
|
|
265
|
+
var cvssLines = (cvss) => [
|
|
266
|
+
` Severidade: ${cvss.severity ?? "n\xE3o informada"}`,
|
|
267
|
+
` CVSS: ${cvss.score}`,
|
|
268
|
+
` Vers\xE3o CVSS: ${cvss.version}`,
|
|
269
|
+
` Vetor: ${cvss.vectorString}`,
|
|
270
|
+
...cvssMetricLines(cvss)
|
|
271
|
+
];
|
|
272
|
+
var cvssBlock = (data) => data.cvss ? cvssLines(data.cvss) : [" CVSS: registro encontrado, ainda sem m\xE9trica CVSS"];
|
|
273
|
+
var metadataLines = (data, summary) => labeledLines(
|
|
274
|
+
[
|
|
275
|
+
["CWE", data.cwes.join(", ") || void 0],
|
|
276
|
+
["Descri\xE7\xE3o NVD", data.description !== summary ? data.description : void 0],
|
|
277
|
+
["Publicado", data.published],
|
|
278
|
+
["Modificado", data.lastModified]
|
|
279
|
+
],
|
|
280
|
+
(label, value) => ` ${label}: ${value}`
|
|
281
|
+
);
|
|
282
|
+
var cisaKevLines = (kev) => [
|
|
283
|
+
` CISA KEV desde: ${kev.addedAt}`,
|
|
284
|
+
...labeledLines(
|
|
285
|
+
[
|
|
286
|
+
["Nome CISA", kev.vulnerabilityName],
|
|
287
|
+
["Prazo CISA", kev.actionDue],
|
|
288
|
+
["A\xE7\xE3o CISA", kev.requiredAction]
|
|
289
|
+
],
|
|
290
|
+
(label, value) => ` ${label}: ${value}`
|
|
291
|
+
)
|
|
292
|
+
];
|
|
293
|
+
var cisaSsvcLines = (ssvc) => {
|
|
294
|
+
const parts = labeledLines(
|
|
295
|
+
[
|
|
296
|
+
["explora\xE7\xE3o", ssvc.exploitation],
|
|
297
|
+
["automatiz\xE1vel", ssvc.automatable],
|
|
298
|
+
["impacto t\xE9cnico", ssvc.technicalImpact]
|
|
299
|
+
],
|
|
300
|
+
(label, value) => `${label}=${value}`
|
|
301
|
+
);
|
|
302
|
+
return labeledLines(
|
|
303
|
+
[
|
|
304
|
+
["CISA SSVC", parts.length > 0 ? parts.join("; ") : void 0],
|
|
305
|
+
["CISA SSVC atualizado", ssvc.timestamp]
|
|
306
|
+
],
|
|
307
|
+
(label, value) => ` ${label}: ${value}`
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
var cisaLines = (cisa) => [
|
|
311
|
+
...cisa?.kev ? cisaKevLines(cisa.kev) : [],
|
|
312
|
+
...cisa?.ssvc ? cisaSsvcLines(cisa.ssvc) : []
|
|
313
|
+
];
|
|
314
|
+
var referencesLines = (references) => {
|
|
315
|
+
const shown = references.slice(0, 5);
|
|
316
|
+
const extraCount = references.length - shown.length;
|
|
317
|
+
return labeledLines(
|
|
318
|
+
[
|
|
319
|
+
["Refer\xEAncias", shown.length > 0 ? shown.map((reference) => reference.url).join(", ") : void 0],
|
|
320
|
+
["Refer\xEAncias adicionais", extraCount > 0 ? `${extraCount}` : void 0]
|
|
321
|
+
],
|
|
322
|
+
(label, value) => ` ${label}: ${value}`
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
var foundNvdLines = (result, summary) => {
|
|
326
|
+
const { data } = result;
|
|
327
|
+
return [
|
|
328
|
+
` CVE: ${result.cveId}`,
|
|
329
|
+
` Status NVD: ${data.vulnerabilityStatus ?? "n\xE3o informado"}`,
|
|
330
|
+
...cvssBlock(data),
|
|
331
|
+
...metadataLines(data, summary),
|
|
332
|
+
...cisaLines(data.cisa),
|
|
333
|
+
...referencesLines(data.references)
|
|
334
|
+
];
|
|
335
|
+
};
|
|
336
|
+
var httpStatusSuffix = (httpStatus) => httpStatus ? ` HTTP ${httpStatus}` : "";
|
|
337
|
+
var nvdLines = (result, summary) => {
|
|
338
|
+
if (result.status === "found") return foundNvdLines(result, summary);
|
|
339
|
+
if (result.status === "not-found") return [` CVE: ${result.cveId}`, " NVD: sem resultado"];
|
|
340
|
+
return [
|
|
341
|
+
` CVE: ${result.cveId}`,
|
|
342
|
+
` NVD: falha ao consultar (${result.error.kind}${httpStatusSuffix(result.error.httpStatus)})`
|
|
343
|
+
];
|
|
344
|
+
};
|
|
345
|
+
var aliasesLine = (aliases) => aliases.length > 0 ? [`Aliases: ${aliases.join(", ")}`] : [];
|
|
346
|
+
var summaryLine = (summary, label) => summary ? [`${label}: ${summary}`] : [];
|
|
347
|
+
var nvdResultLines = (results, summary) => (results ?? []).flatMap((result) => ["", ...nvdLines(result, summary)]);
|
|
348
|
+
var sourcesLabel = (source, foundNvd) => source === "osv" ? `OSV${foundNvd ? ", NVD" : ""}` : "npm";
|
|
349
|
+
var dependencyBlock = (finding) => {
|
|
350
|
+
const dependency = finding.dependency;
|
|
351
|
+
if (!dependency) return "";
|
|
352
|
+
const foundNvd = (dependency.nvd ?? []).some((result) => result.status === "found");
|
|
353
|
+
const lines = [
|
|
354
|
+
`Pacote: ${dependency.package.name}`,
|
|
355
|
+
`Vers\xE3o instalada: ${dependency.package.installedVersion}`,
|
|
356
|
+
`Vers\xE3o corrigida: ${dependency.package.fixedVersions.join(" ou ") || "n\xE3o informada"}`,
|
|
357
|
+
`${dependency.advisory.source.toUpperCase()}: ${dependency.advisory.id ?? "identificador n\xE3o informado"}`,
|
|
358
|
+
...aliasesLine(dependency.advisory.aliases),
|
|
359
|
+
`Severidade do finding: ${finding.severity.toUpperCase()}`,
|
|
360
|
+
...summaryLine(dependency.advisory.summary, "Descri\xE7\xE3o OSV/npm"),
|
|
361
|
+
...nvdResultLines(dependency.nvd, dependency.advisory.summary),
|
|
362
|
+
`Fontes: ${sourcesLabel(dependency.advisory.source, foundNvd)}`
|
|
363
|
+
];
|
|
364
|
+
return lines.join("\n");
|
|
365
|
+
};
|
|
366
|
+
var printDependencyFindings = (findings) => {
|
|
367
|
+
if (findings.length === 0) return;
|
|
368
|
+
console.log(chalk.bold("\nDepend\xEAncias vulner\xE1veis"));
|
|
369
|
+
console.log(findings.map(dependencyBlock).join("\n\n"));
|
|
370
|
+
};
|
|
228
371
|
var printFindingsReport = (result, coverage) => {
|
|
229
|
-
|
|
372
|
+
const codeFindings = result.findings.filter((finding) => !finding.dependency);
|
|
373
|
+
const dependencyFindings = result.findings.filter((finding) => finding.dependency);
|
|
374
|
+
if (codeFindings.length) console.log(findingsTable(codeFindings).toString());
|
|
375
|
+
printDependencyFindings(dependencyFindings);
|
|
230
376
|
console.log(
|
|
231
377
|
chalk.bold(
|
|
232
378
|
`
|
|
@@ -250,6 +396,7 @@ var toJsonReport = (result) => {
|
|
|
250
396
|
};
|
|
251
397
|
|
|
252
398
|
// src/reporters/markdown.reporter.ts
|
|
399
|
+
var LOGO_URL = "https://raw.githubusercontent.com/Ivan-ReisDev/code-sentry/main/docs/assets/logo.png";
|
|
253
400
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
|
|
254
401
|
var SEVERITY_LABEL = /* @__PURE__ */ new Map([
|
|
255
402
|
["critical", "Critical"],
|
|
@@ -257,7 +404,14 @@ var SEVERITY_LABEL = /* @__PURE__ */ new Map([
|
|
|
257
404
|
["medium", "Medium"],
|
|
258
405
|
["low", "Low"]
|
|
259
406
|
]);
|
|
407
|
+
var SEVERITY_EMOJI = /* @__PURE__ */ new Map([
|
|
408
|
+
["critical", "\u{1F534}"],
|
|
409
|
+
["high", "\u{1F7E0}"],
|
|
410
|
+
["medium", "\u{1F7E1}"],
|
|
411
|
+
["low", "\u{1F535}"]
|
|
412
|
+
]);
|
|
260
413
|
var severityLabel = (severity) => SEVERITY_LABEL.get(severity) ?? severity;
|
|
414
|
+
var severityBadge = (severity) => `${SEVERITY_EMOJI.get(severity) ?? ""} ${severityLabel(severity)}`;
|
|
261
415
|
var escapeCell = (text2) => text2.replaceAll("|", "\\|");
|
|
262
416
|
var groupBy = (items, keyOf) => {
|
|
263
417
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -276,7 +430,7 @@ var findingsTable2 = (findings) => {
|
|
|
276
430
|
return lines;
|
|
277
431
|
};
|
|
278
432
|
var severitySection = (severity, findings) => {
|
|
279
|
-
const lines = [`## ${
|
|
433
|
+
const lines = [`## ${severityBadge(severity)} (${findings.length})`, ""];
|
|
280
434
|
const byRule = groupBy(findings, (f) => f.ruleId);
|
|
281
435
|
for (const ruleId of [...byRule.keys()].sort()) {
|
|
282
436
|
const ruleFindings = byRule.get(ruleId) ?? [];
|
|
@@ -284,24 +438,38 @@ var severitySection = (severity, findings) => {
|
|
|
284
438
|
}
|
|
285
439
|
return lines;
|
|
286
440
|
};
|
|
441
|
+
var semgrepCoverageLine = (result) => result.engines?.semgrep === void 0 ? [] : [
|
|
442
|
+
`- **Cobertura por motor:** CodeSentry ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep ${result.engines.semgrep} arquivos`
|
|
443
|
+
];
|
|
444
|
+
var dependencyAuditConsideredLine = (result) => typeof result.engines?.dependencyAudit === "number" ? [`- **Depend\xEAncias consideradas:** ${result.engines.dependencyAudit}`] : [];
|
|
445
|
+
var nvdCoverageLine = (result) => {
|
|
446
|
+
const nvd = result.engines?.nvd;
|
|
447
|
+
if (!nvd || nvd.total === 0) return [];
|
|
448
|
+
return [
|
|
449
|
+
`- **Cobertura NVD:** ${nvd.enriched}/${nvd.total} enriquecidos; ${nvd.notFound} sem resultado; ${nvd.failed} falhas; ${nvd.cacheHits} cache hits`
|
|
450
|
+
];
|
|
451
|
+
};
|
|
452
|
+
var dependencyAuditNoteLine = (result) => result.engines?.dependencyAudit === false ? [`- **Nota:** ${DEPENDENCY_AUDIT_NOTE}`] : [];
|
|
287
453
|
var reportHeader = (result, generatedAt) => [
|
|
454
|
+
`<p align="center"><img src="${LOGO_URL}" alt="CodeSentry" width="320"></p>`,
|
|
455
|
+
"",
|
|
288
456
|
"# Relat\xF3rio CodeSentry",
|
|
289
457
|
"",
|
|
290
458
|
`- **Gerado em:** ${generatedAt.toISOString()}`,
|
|
291
459
|
`- **Arquivos analisados:** ${result.scannedFiles}`,
|
|
292
|
-
...result
|
|
293
|
-
|
|
294
|
-
|
|
460
|
+
...semgrepCoverageLine(result),
|
|
461
|
+
...dependencyAuditConsideredLine(result),
|
|
462
|
+
...nvdCoverageLine(result),
|
|
295
463
|
`- **Dura\xE7\xE3o:** ${result.durationMs}ms`,
|
|
296
464
|
`- **Total de problemas:** ${result.findings.length}`,
|
|
297
|
-
...result
|
|
465
|
+
...dependencyAuditNoteLine(result),
|
|
298
466
|
""
|
|
299
467
|
];
|
|
300
468
|
var warningsSection = (result) => (result.warnings ?? []).length === 0 ? [] : ["## Avisos", "", ...(result.warnings ?? []).map((warning) => `- ${warning}`), ""];
|
|
301
469
|
var summaryTable = (bySeverity) => {
|
|
302
470
|
const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
|
|
303
471
|
for (const severity of SEVERITY_ORDER) {
|
|
304
|
-
lines.push(`| ${
|
|
472
|
+
lines.push(`| ${severityBadge(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
|
|
305
473
|
}
|
|
306
474
|
lines.push("");
|
|
307
475
|
return lines;
|
|
@@ -316,26 +484,1207 @@ var severitySections = (bySeverity) => {
|
|
|
316
484
|
}
|
|
317
485
|
return lines;
|
|
318
486
|
};
|
|
487
|
+
var labeledLines2 = (pairs, format) => pairs.filter((pair) => Boolean(pair[1])).map(([label, value]) => format(label, value));
|
|
488
|
+
var mdCvssMetricLines = (cvss) => labeledLines2(
|
|
489
|
+
[
|
|
490
|
+
["Attack Vector", cvss.attackVector],
|
|
491
|
+
["Attack Complexity", cvss.attackComplexity],
|
|
492
|
+
["Privileges Required", cvss.privilegesRequired],
|
|
493
|
+
["User Interaction", cvss.userInteraction],
|
|
494
|
+
["Scope", cvss.scope],
|
|
495
|
+
["Confidentiality", cvss.confidentialityImpact],
|
|
496
|
+
["Integrity", cvss.integrityImpact],
|
|
497
|
+
["Availability", cvss.availabilityImpact]
|
|
498
|
+
],
|
|
499
|
+
(key, value) => `${key}=${value}`
|
|
500
|
+
);
|
|
501
|
+
var mdCvssLines = (cvss) => {
|
|
502
|
+
const lines = [
|
|
503
|
+
`- **CVSS:** ${cvss.score} (${cvss.severity ?? "sem severidade"}), vers\xE3o ${cvss.version}`,
|
|
504
|
+
`- **Vetor:** \`${cvss.vectorString}\``
|
|
505
|
+
];
|
|
506
|
+
const metrics = mdCvssMetricLines(cvss);
|
|
507
|
+
return metrics.length ? [...lines, `- **M\xE9tricas:** ${metrics.join("; ")}`] : lines;
|
|
508
|
+
};
|
|
509
|
+
var mdCvssBlock = (data) => data.cvss ? mdCvssLines(data.cvss) : ["- **CVSS:** registro encontrado, ainda sem m\xE9trica CVSS"];
|
|
510
|
+
var mdMetadataLines = (data, summary) => labeledLines2(
|
|
511
|
+
[
|
|
512
|
+
["CWE", data.cwes.join(", ") || void 0],
|
|
513
|
+
["Descri\xE7\xE3o NVD", data.description !== summary ? data.description : void 0],
|
|
514
|
+
["Publicado", data.published],
|
|
515
|
+
["Modificado", data.lastModified]
|
|
516
|
+
],
|
|
517
|
+
(label, value) => `- **${label}:** ${value}`
|
|
518
|
+
);
|
|
519
|
+
var mdCisaKevLines = (kev) => [
|
|
520
|
+
`- **CISA KEV:** \u26A0\uFE0F inclu\xEDdo em ${kev.addedAt}`,
|
|
521
|
+
...labeledLines2(
|
|
522
|
+
[
|
|
523
|
+
["Nome CISA", kev.vulnerabilityName],
|
|
524
|
+
["Prazo CISA", kev.actionDue],
|
|
525
|
+
["A\xE7\xE3o requerida", kev.requiredAction]
|
|
526
|
+
],
|
|
527
|
+
(label, value) => `- **${label}:** ${value}`
|
|
528
|
+
)
|
|
529
|
+
];
|
|
530
|
+
var mdCisaSsvcLines = (ssvc) => {
|
|
531
|
+
const parts = labeledLines2(
|
|
532
|
+
[
|
|
533
|
+
["explora\xE7\xE3o", ssvc.exploitation],
|
|
534
|
+
["automatiz\xE1vel", ssvc.automatable],
|
|
535
|
+
["impacto t\xE9cnico", ssvc.technicalImpact]
|
|
536
|
+
],
|
|
537
|
+
(label, value) => `${label}=${value}`
|
|
538
|
+
);
|
|
539
|
+
return labeledLines2(
|
|
540
|
+
[
|
|
541
|
+
["CISA SSVC", parts.length > 0 ? parts.join("; ") : void 0],
|
|
542
|
+
["CISA SSVC atualizado", ssvc.timestamp]
|
|
543
|
+
],
|
|
544
|
+
(label, value) => `- **${label}:** ${value}`
|
|
545
|
+
);
|
|
546
|
+
};
|
|
547
|
+
var mdCisaLines = (cisa) => [
|
|
548
|
+
...cisa?.kev ? mdCisaKevLines(cisa.kev) : [],
|
|
549
|
+
...cisa?.ssvc ? mdCisaSsvcLines(cisa.ssvc) : []
|
|
550
|
+
];
|
|
551
|
+
var mdReferencesBlock = (references) => references.length ? [
|
|
552
|
+
"",
|
|
553
|
+
"**Refer\xEAncias:**",
|
|
554
|
+
"",
|
|
555
|
+
...references.map((reference) => {
|
|
556
|
+
const metadata = [reference.source, ...reference.tags].filter(Boolean);
|
|
557
|
+
return `- ${reference.url}${metadata.length ? ` \u2014 ${metadata.join(", ")}` : ""}`;
|
|
558
|
+
})
|
|
559
|
+
] : [];
|
|
560
|
+
var nvdHttpStatusSuffix = (httpStatus) => httpStatus ? `, HTTP ${httpStatus}` : "";
|
|
561
|
+
var nvdMarkdown = (result, advisorySummary) => {
|
|
562
|
+
if (result.status === "not-found") return [`- **${result.cveId}:** sem resultado no NVD`];
|
|
563
|
+
if (result.status === "error") {
|
|
564
|
+
return [
|
|
565
|
+
`- **${result.cveId}:** falha ao consultar (${result.error.kind}${nvdHttpStatusSuffix(result.error.httpStatus)})`
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
const { data } = result;
|
|
569
|
+
return [
|
|
570
|
+
`#### ${result.cveId}`,
|
|
571
|
+
"",
|
|
572
|
+
`- **Status NVD:** ${data.vulnerabilityStatus ?? "n\xE3o informado"}`,
|
|
573
|
+
...mdCvssBlock(data),
|
|
574
|
+
...mdMetadataLines(data, advisorySummary),
|
|
575
|
+
...mdCisaLines(data.cisa),
|
|
576
|
+
...mdReferencesBlock(data.references)
|
|
577
|
+
];
|
|
578
|
+
};
|
|
579
|
+
var mdSourcesLabel = (source, foundNvd) => source === "osv" ? `OSV${foundNvd ? ", NVD" : ""}` : "npm";
|
|
580
|
+
var mdSummaryLine = (summary) => summary ? [`- **Descri\xE7\xE3o OSV/npm:** ${summary}`] : [];
|
|
581
|
+
var firstCvssScore = (nvd) => {
|
|
582
|
+
const withCvss = (nvd ?? []).find(
|
|
583
|
+
(result) => result.status === "found" && Boolean(result.data.cvss)
|
|
584
|
+
);
|
|
585
|
+
return withCvss?.data.cvss ? `${withCvss.data.cvss.score}` : "\u2014";
|
|
586
|
+
};
|
|
587
|
+
var dependencySummaryRow = (finding, dependency) => `| ${dependency.package.name}@${dependency.package.installedVersion} | ${severityBadge(finding.severity)} | ${dependency.advisory.id ?? "\u2014"} | ${dependency.advisory.aliases.join(", ") || "\u2014"} | ${firstCvssScore(dependency.nvd)} | ${dependency.package.fixedVersions.join(" ou ") || "\u2014"} |`;
|
|
588
|
+
var dependencySummaryTable = (findings) => {
|
|
589
|
+
const rows = findings.filter((finding) => Boolean(finding.dependency)).map((finding) => dependencySummaryRow(finding, finding.dependency));
|
|
590
|
+
if (!rows.length) return [];
|
|
591
|
+
return [
|
|
592
|
+
"| Pacote | Severidade | Advisory | CVE(s) | CVSS | Corrigir para |",
|
|
593
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
594
|
+
...rows,
|
|
595
|
+
""
|
|
596
|
+
];
|
|
597
|
+
};
|
|
598
|
+
var dependencySection = (finding) => {
|
|
599
|
+
const dependency = finding.dependency;
|
|
600
|
+
if (!dependency) return [];
|
|
601
|
+
const foundNvd = (dependency.nvd ?? []).some((result) => result.status === "found");
|
|
602
|
+
return [
|
|
603
|
+
"<details>",
|
|
604
|
+
`<summary>${severityBadge(finding.severity)} <strong>${dependency.package.name}@${dependency.package.installedVersion}</strong></summary>`,
|
|
605
|
+
"",
|
|
606
|
+
`- **Fonte principal:** ${dependency.advisory.source.toUpperCase()}`,
|
|
607
|
+
`- **Advisory:** ${dependency.advisory.id ?? "n\xE3o informado"}`,
|
|
608
|
+
`- **Aliases:** ${dependency.advisory.aliases.join(", ") || "nenhum"}`,
|
|
609
|
+
`- **Vers\xF5es corrigidas:** ${dependency.package.fixedVersions.join(" ou ") || "n\xE3o informadas"}`,
|
|
610
|
+
`- **Fontes:** ${mdSourcesLabel(dependency.advisory.source, foundNvd)}`,
|
|
611
|
+
...mdSummaryLine(dependency.advisory.summary),
|
|
612
|
+
...(dependency.nvd ?? []).flatMap((result) => ["", ...nvdMarkdown(result, dependency.advisory.summary)]),
|
|
613
|
+
"",
|
|
614
|
+
"</details>",
|
|
615
|
+
""
|
|
616
|
+
];
|
|
617
|
+
};
|
|
618
|
+
var dependencySections = (findings) => {
|
|
619
|
+
const dependencies = findings.filter((finding) => finding.dependency);
|
|
620
|
+
if (dependencies.length === 0) return [];
|
|
621
|
+
return [
|
|
622
|
+
"## Depend\xEAncias vulner\xE1veis",
|
|
623
|
+
"",
|
|
624
|
+
...dependencySummaryTable(findings),
|
|
625
|
+
...dependencies.flatMap(dependencySection)
|
|
626
|
+
];
|
|
627
|
+
};
|
|
628
|
+
var osvCheckedSection = (result) => {
|
|
629
|
+
const { osvCheckedPackages } = result;
|
|
630
|
+
if (!osvCheckedPackages || osvCheckedPackages.length === 0) {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
const checked = result.engines?.osv?.checked ?? osvCheckedPackages.length;
|
|
634
|
+
const total = result.engines?.osv?.total ?? osvCheckedPackages.length;
|
|
635
|
+
return [
|
|
636
|
+
`## Depend\xEAncias verificadas no OSV.dev (${checked}/${total})`,
|
|
637
|
+
"",
|
|
638
|
+
"<details>",
|
|
639
|
+
"<summary>Ver lista completa</summary>",
|
|
640
|
+
"",
|
|
641
|
+
...osvCheckedPackages.map((pkg) => `- ${pkg}`),
|
|
642
|
+
"",
|
|
643
|
+
"</details>",
|
|
644
|
+
""
|
|
645
|
+
];
|
|
646
|
+
};
|
|
319
647
|
var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
|
|
648
|
+
const codeFindings = result.findings.filter((finding) => !finding.dependency);
|
|
320
649
|
const bySeverity = groupBy(result.findings, (f) => f.severity);
|
|
650
|
+
const codeBySeverity = groupBy(codeFindings, (f) => f.severity);
|
|
321
651
|
return [
|
|
322
652
|
...reportHeader(result, generatedAt),
|
|
323
653
|
...warningsSection(result),
|
|
324
654
|
...summaryTable(bySeverity),
|
|
325
|
-
...severitySections(
|
|
655
|
+
...severitySections(codeBySeverity),
|
|
656
|
+
...dependencySections(result.findings),
|
|
657
|
+
...osvCheckedSection(result)
|
|
326
658
|
].join("\n");
|
|
327
659
|
};
|
|
328
660
|
|
|
661
|
+
// src/scanner/dependency-audit.ts
|
|
662
|
+
import { execFile } from "child_process";
|
|
663
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
664
|
+
import { join as join2 } from "path";
|
|
665
|
+
import { promisify } from "util";
|
|
666
|
+
|
|
667
|
+
// src/scanner/nvd-cache.ts
|
|
668
|
+
import { homedir } from "os";
|
|
669
|
+
import { dirname, join } from "path";
|
|
670
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
671
|
+
var CACHE_SCHEMA_VERSION = 1;
|
|
672
|
+
var FOUND_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
673
|
+
var NOT_FOUND_TTL_MS = 60 * 60 * 1e3;
|
|
674
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
675
|
+
var defaultCacheFile = () => {
|
|
676
|
+
if (process.platform === "win32") {
|
|
677
|
+
return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "CodeSentry", "nvd-v1.json");
|
|
678
|
+
}
|
|
679
|
+
if (process.platform === "darwin") {
|
|
680
|
+
return join(homedir(), "Library", "Caches", "CodeSentry", "nvd-v1.json");
|
|
681
|
+
}
|
|
682
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "codesentry", "nvd-v1.json");
|
|
683
|
+
};
|
|
684
|
+
var isValidReference = (value) => isRecord(value) && typeof value.url === "string";
|
|
685
|
+
var isNvdData = (value, cveId) => isRecord(value) && value.id === cveId && Array.isArray(value.cwes) && value.cwes.every((item) => typeof item === "string") && Array.isArray(value.references) && value.references.every(isValidReference);
|
|
686
|
+
var isStoredEnvelope = (value) => isRecord(value) && typeof value.expiresAt === "number" && isRecord(value.result);
|
|
687
|
+
var parseFoundResult = (cveId, result) => result.status === "found" && isNvdData(result.data, cveId) ? { status: "found", cveId, data: result.data } : void 0;
|
|
688
|
+
var parseStoredResult = (cveId, result) => {
|
|
689
|
+
if (result.cveId !== cveId) return void 0;
|
|
690
|
+
if (result.status === "not-found") return { status: "not-found", cveId };
|
|
691
|
+
return parseFoundResult(cveId, result);
|
|
692
|
+
};
|
|
693
|
+
var parseStoredEntry = (cveId, value) => {
|
|
694
|
+
if (!isStoredEnvelope(value)) return void 0;
|
|
695
|
+
const result = parseStoredResult(cveId, value.result);
|
|
696
|
+
return result ? { expiresAt: value.expiresAt, result } : void 0;
|
|
697
|
+
};
|
|
698
|
+
var parseCacheFileBody = (parsed, now) => {
|
|
699
|
+
if (!isRecord(parsed) || parsed.schemaVersion !== CACHE_SCHEMA_VERSION || !isRecord(parsed.entries)) {
|
|
700
|
+
return { entries: /* @__PURE__ */ new Map(), incompatible: true };
|
|
701
|
+
}
|
|
702
|
+
const entries = /* @__PURE__ */ new Map();
|
|
703
|
+
for (const [cveId, value] of Object.entries(parsed.entries)) {
|
|
704
|
+
const entry = parseStoredEntry(cveId, value);
|
|
705
|
+
if (entry && entry.expiresAt > now) entries.set(cveId, entry);
|
|
706
|
+
}
|
|
707
|
+
return { entries, incompatible: false };
|
|
708
|
+
};
|
|
709
|
+
var cleanCacheableResult = (result) => result.status === "found" ? { status: "found", cveId: result.cveId, data: result.data } : { status: "not-found", cveId: result.cveId };
|
|
710
|
+
var toStoredEntry = (result, now) => ({
|
|
711
|
+
expiresAt: now + (result.status === "found" ? FOUND_TTL_MS : NOT_FOUND_TTL_MS),
|
|
712
|
+
result
|
|
713
|
+
});
|
|
714
|
+
var PersistentNvdCache = class {
|
|
715
|
+
#filePath;
|
|
716
|
+
#now;
|
|
717
|
+
#entries = /* @__PURE__ */ new Map();
|
|
718
|
+
#warnings = [];
|
|
719
|
+
#loadPromise;
|
|
720
|
+
#writeCounter = 0;
|
|
721
|
+
#writeQueue = Promise.resolve();
|
|
722
|
+
constructor(options) {
|
|
723
|
+
this.#filePath = options.filePath ?? defaultCacheFile();
|
|
724
|
+
this.#now = options.now ?? Date.now;
|
|
725
|
+
}
|
|
726
|
+
async #loadFromDisk() {
|
|
727
|
+
try {
|
|
728
|
+
const raw = await readFile(this.#filePath, "utf-8");
|
|
729
|
+
const parsed = JSON.parse(raw);
|
|
730
|
+
const { entries, incompatible } = parseCacheFileBody(parsed, this.#now());
|
|
731
|
+
if (incompatible) {
|
|
732
|
+
this.#warnings.push("O cache do NVD possui vers\xE3o ou formato incompat\xEDvel e foi ignorado.");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
entries.forEach((entry, cveId) => this.#entries.set(cveId, entry));
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error.code !== "ENOENT") {
|
|
738
|
+
this.#warnings.push("N\xE3o foi poss\xEDvel ler o cache do NVD; as consultas continuar\xE3o sem ele.");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async #load() {
|
|
743
|
+
this.#loadPromise ??= this.#loadFromDisk();
|
|
744
|
+
await this.#loadPromise;
|
|
745
|
+
}
|
|
746
|
+
async get(cveId) {
|
|
747
|
+
await this.#load();
|
|
748
|
+
const entry = this.#entries.get(cveId);
|
|
749
|
+
if (!entry) return void 0;
|
|
750
|
+
if (entry.expiresAt <= this.#now()) {
|
|
751
|
+
this.#entries.delete(cveId);
|
|
752
|
+
return void 0;
|
|
753
|
+
}
|
|
754
|
+
return { ...entry.result, fromCache: true };
|
|
755
|
+
}
|
|
756
|
+
async set(result) {
|
|
757
|
+
if (result.status === "error") return;
|
|
758
|
+
await this.#load();
|
|
759
|
+
const entry = toStoredEntry(cleanCacheableResult(result), this.#now());
|
|
760
|
+
this.#entries.set(result.cveId, entry);
|
|
761
|
+
this.#writeQueue = this.#writeQueue.then(() => this.#persist()).catch(() => void 0);
|
|
762
|
+
await this.#writeQueue;
|
|
763
|
+
}
|
|
764
|
+
async #persist() {
|
|
765
|
+
try {
|
|
766
|
+
const directory = dirname(this.#filePath);
|
|
767
|
+
await mkdir(directory, { recursive: true });
|
|
768
|
+
const entries = Object.fromEntries(this.#entries);
|
|
769
|
+
const body = { schemaVersion: CACHE_SCHEMA_VERSION, entries };
|
|
770
|
+
const temporary = `${this.#filePath}.${process.pid}.${this.#writeCounter++}.tmp`;
|
|
771
|
+
await writeFile(temporary, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
772
|
+
await rename(temporary, this.#filePath);
|
|
773
|
+
} catch {
|
|
774
|
+
this.#warnings.push("N\xE3o foi poss\xEDvel gravar o cache do NVD; o scan continuar\xE1 normalmente.");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
consumeWarnings() {
|
|
778
|
+
return this.#warnings.splice(0);
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
var createNvdCache = (options = {}) => new PersistentNvdCache(options);
|
|
782
|
+
|
|
783
|
+
// src/scanner/nvd-normalizer.ts
|
|
784
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
785
|
+
var asString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
786
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
787
|
+
var pickBy = (condition, whenTrue, whenFalse) => condition ? whenTrue : whenFalse;
|
|
788
|
+
var pickEnum = (value, accepted) => {
|
|
789
|
+
const candidate = asString(value)?.toUpperCase();
|
|
790
|
+
return candidate && accepted.includes(candidate) ? candidate : void 0;
|
|
791
|
+
};
|
|
792
|
+
var SEVERITIES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
793
|
+
var ATTACK_VECTORS = ["NETWORK", "ADJACENT", "LOCAL", "PHYSICAL"];
|
|
794
|
+
var ATTACK_COMPLEXITIES = ["LOW", "MEDIUM", "HIGH"];
|
|
795
|
+
var PRIVILEGES = ["NONE", "LOW", "HIGH"];
|
|
796
|
+
var USER_INTERACTIONS = ["NONE", "REQUIRED", "PASSIVE", "ACTIVE"];
|
|
797
|
+
var SCOPES = ["UNCHANGED", "CHANGED"];
|
|
798
|
+
var IMPACTS = ["NONE", "LOW", "HIGH", "PARTIAL", "COMPLETE"];
|
|
799
|
+
var METRIC_BUCKETS = [
|
|
800
|
+
{ key: "cvssMetricV40", version: "4.0" },
|
|
801
|
+
{ key: "cvssMetricV31", version: "3.1" },
|
|
802
|
+
{ key: "cvssMetricV30", version: "3.0" },
|
|
803
|
+
{ key: "cvssMetricV2", version: "2.0" }
|
|
804
|
+
];
|
|
805
|
+
var isNvdSource = (metric) => asString(metric.source)?.toLowerCase() === "nvd@nist.gov";
|
|
806
|
+
var isPrimaryType = (metric) => asString(metric.type) === "Primary";
|
|
807
|
+
var fallbackMetricPriority = (metric) => {
|
|
808
|
+
if (isPrimaryType(metric)) return 1;
|
|
809
|
+
if (isNvdSource(metric)) return 2;
|
|
810
|
+
return 3;
|
|
811
|
+
};
|
|
812
|
+
var metricPriority = (metric) => isNvdSource(metric) && isPrimaryType(metric) ? 0 : fallbackMetricPriority(metric);
|
|
813
|
+
var isValidCvssScore = (score, version) => score >= 0 && score <= 10 && (score !== 0 || version === "2.0");
|
|
814
|
+
var classifyHighOrCritical = (score, version) => version === "2.0" || score < 9 ? "HIGH" : "CRITICAL";
|
|
815
|
+
var classifySeverity = (score, version) => {
|
|
816
|
+
if (score < 4) return "LOW";
|
|
817
|
+
if (score < 7) return "MEDIUM";
|
|
818
|
+
return classifyHighOrCritical(score, version);
|
|
819
|
+
};
|
|
820
|
+
var deriveSeverity = (score, version) => isValidCvssScore(score, version) ? classifySeverity(score, version) : void 0;
|
|
821
|
+
var normalizeAttackVector = (value) => {
|
|
822
|
+
const normalized = asString(value)?.toUpperCase();
|
|
823
|
+
return pickEnum(normalized === "ADJACENT_NETWORK" ? "ADJACENT" : normalized, ATTACK_VECTORS);
|
|
824
|
+
};
|
|
825
|
+
var validatedCvssData = (metric, version) => {
|
|
826
|
+
if (!isRecord2(metric.cvssData)) return void 0;
|
|
827
|
+
const data = metric.cvssData;
|
|
828
|
+
const dataVersion = asString(data.version);
|
|
829
|
+
const vectorString = asString(data.vectorString);
|
|
830
|
+
const score = data.baseScore;
|
|
831
|
+
const scoreIsValidNumber = typeof score === "number" && Number.isFinite(score) && score >= 0 && score <= 10;
|
|
832
|
+
if (dataVersion !== version || !vectorString || !scoreIsValidNumber) return void 0;
|
|
833
|
+
return { data, vectorString, score };
|
|
834
|
+
};
|
|
835
|
+
var metricSeverity = (metric, data, score, version) => {
|
|
836
|
+
const wrapperSeverity = pickBy(version === "2.0", metric.baseSeverity, data.baseSeverity);
|
|
837
|
+
const rawSeverity = asString(wrapperSeverity);
|
|
838
|
+
return rawSeverity === void 0 ? deriveSeverity(score, version) : pickEnum(rawSeverity, SEVERITIES);
|
|
839
|
+
};
|
|
840
|
+
var metricUserInteraction = (metric, data, version) => {
|
|
841
|
+
if (version === "2.0" && typeof metric.userInteractionRequired === "boolean") {
|
|
842
|
+
return metric.userInteractionRequired ? "REQUIRED" : "NONE";
|
|
843
|
+
}
|
|
844
|
+
return pickEnum(data.userInteraction, USER_INTERACTIONS);
|
|
845
|
+
};
|
|
846
|
+
var metricType = (rawType) => rawType === "Primary" || rawType === "Secondary" ? rawType : void 0;
|
|
847
|
+
var privilegesRequiredValue = (data, version) => version === "2.0" ? void 0 : pickEnum(data.privilegesRequired, PRIVILEGES);
|
|
848
|
+
var scopeValue = (data, version) => version === "4.0" || version === "2.0" ? void 0 : pickEnum(data.scope, SCOPES);
|
|
849
|
+
var computeMetricFields = (metric, data, score, version) => ({
|
|
850
|
+
severity: metricSeverity(metric, data, score, version),
|
|
851
|
+
source: asString(metric.source),
|
|
852
|
+
type: metricType(asString(metric.type)),
|
|
853
|
+
attackVector: normalizeAttackVector(pickBy(version === "2.0", data.accessVector, data.attackVector)),
|
|
854
|
+
attackComplexity: pickEnum(
|
|
855
|
+
pickBy(version === "2.0", data.accessComplexity, data.attackComplexity),
|
|
856
|
+
ATTACK_COMPLEXITIES
|
|
857
|
+
),
|
|
858
|
+
privilegesRequired: privilegesRequiredValue(data, version),
|
|
859
|
+
userInteraction: metricUserInteraction(metric, data, version),
|
|
860
|
+
scope: scopeValue(data, version),
|
|
861
|
+
confidentialityImpact: pickEnum(
|
|
862
|
+
pickBy(version === "4.0", data.vulnConfidentialityImpact, data.confidentialityImpact),
|
|
863
|
+
IMPACTS
|
|
864
|
+
),
|
|
865
|
+
integrityImpact: pickEnum(pickBy(version === "4.0", data.vulnIntegrityImpact, data.integrityImpact), IMPACTS),
|
|
866
|
+
availabilityImpact: pickEnum(
|
|
867
|
+
pickBy(version === "4.0", data.vulnAvailabilityImpact, data.availabilityImpact),
|
|
868
|
+
IMPACTS
|
|
869
|
+
)
|
|
870
|
+
});
|
|
871
|
+
var withOptionalFields = (base, fields) => ({
|
|
872
|
+
...base,
|
|
873
|
+
...fields.severity && { severity: fields.severity },
|
|
874
|
+
...fields.source && { source: fields.source },
|
|
875
|
+
...fields.type && { type: fields.type },
|
|
876
|
+
...fields.attackVector && { attackVector: fields.attackVector },
|
|
877
|
+
...fields.attackComplexity && { attackComplexity: fields.attackComplexity },
|
|
878
|
+
...fields.privilegesRequired && { privilegesRequired: fields.privilegesRequired },
|
|
879
|
+
...fields.userInteraction && { userInteraction: fields.userInteraction },
|
|
880
|
+
...fields.scope && { scope: fields.scope },
|
|
881
|
+
...fields.confidentialityImpact && { confidentialityImpact: fields.confidentialityImpact },
|
|
882
|
+
...fields.integrityImpact && { integrityImpact: fields.integrityImpact },
|
|
883
|
+
...fields.availabilityImpact && { availabilityImpact: fields.availabilityImpact }
|
|
884
|
+
});
|
|
885
|
+
var normalizeMetric = (metric, version) => {
|
|
886
|
+
const validated = validatedCvssData(metric, version);
|
|
887
|
+
if (!validated) return void 0;
|
|
888
|
+
const { data, vectorString, score } = validated;
|
|
889
|
+
const fields = computeMetricFields(metric, data, score, version);
|
|
890
|
+
return withOptionalFields({ score, version, vectorString }, fields);
|
|
891
|
+
};
|
|
892
|
+
var rankedCandidates = (metrics, key) => (
|
|
893
|
+
// codesentry-disable-next-line security/detect-object-injection -- key always comes from the hardcoded METRIC_BUCKETS list, never attacker input.
|
|
894
|
+
asArray(metrics[key]).filter(isRecord2).map((metric, index) => ({ metric, index, priority: metricPriority(metric) })).sort((a, b) => a.priority - b.priority || a.index - b.index).map(({ metric }) => metric)
|
|
895
|
+
);
|
|
896
|
+
var firstNormalizedCandidate = (candidates, version) => {
|
|
897
|
+
for (const metric of candidates) {
|
|
898
|
+
const normalized = normalizeMetric(metric, version);
|
|
899
|
+
if (normalized) return normalized;
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
};
|
|
903
|
+
var selectCvss = (metrics) => {
|
|
904
|
+
if (!isRecord2(metrics)) return void 0;
|
|
905
|
+
for (const bucket of METRIC_BUCKETS) {
|
|
906
|
+
const normalized = firstNormalizedCandidate(rankedCandidates(metrics, bucket.key), bucket.version);
|
|
907
|
+
if (normalized) return normalized;
|
|
908
|
+
}
|
|
909
|
+
return void 0;
|
|
910
|
+
};
|
|
911
|
+
var selectDescription = (descriptions) => {
|
|
912
|
+
const candidates = asArray(descriptions).filter(isRecord2);
|
|
913
|
+
const selected = candidates.find((item) => asString(item.lang)?.toLowerCase() === "en") ?? candidates.find((item) => asString(item.lang)?.toLowerCase().startsWith("en-")) ?? candidates[0];
|
|
914
|
+
if (!selected) return {};
|
|
915
|
+
return { description: asString(selected.value), descriptionLanguage: asString(selected.lang) };
|
|
916
|
+
};
|
|
917
|
+
var normalizeCwes = (weaknesses) => {
|
|
918
|
+
const values = asArray(weaknesses).filter(isRecord2).flatMap((weakness) => asArray(weakness.description)).filter(isRecord2).map((description) => asString(description.value)?.toUpperCase()).filter((value) => value !== void 0 && /^CWE-\d+$/.test(value));
|
|
919
|
+
return [...new Set(values)];
|
|
920
|
+
};
|
|
921
|
+
var normalizeReferences = (references) => {
|
|
922
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
923
|
+
for (const reference of asArray(references).filter(isRecord2)) {
|
|
924
|
+
const url = asString(reference.url);
|
|
925
|
+
if (!url || deduped.has(url)) continue;
|
|
926
|
+
deduped.set(url, {
|
|
927
|
+
url,
|
|
928
|
+
source: asString(reference.source),
|
|
929
|
+
tags: asArray(reference.tags).map(asString).filter((tag) => tag !== void 0)
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return [...deduped.values()];
|
|
933
|
+
};
|
|
934
|
+
var ssvcOption = (options, key) => {
|
|
935
|
+
for (const option of asArray(options).filter(isRecord2)) {
|
|
936
|
+
const value = asString(option[key]);
|
|
937
|
+
if (value) return value;
|
|
938
|
+
}
|
|
939
|
+
return void 0;
|
|
940
|
+
};
|
|
941
|
+
var normalizeKev = (cve) => {
|
|
942
|
+
const addedAt = asString(cve.cisaExploitAdd);
|
|
943
|
+
return addedAt ? {
|
|
944
|
+
addedAt,
|
|
945
|
+
actionDue: asString(cve.cisaActionDue),
|
|
946
|
+
requiredAction: asString(cve.cisaRequiredAction),
|
|
947
|
+
vulnerabilityName: asString(cve.cisaVulnerabilityName)
|
|
948
|
+
} : void 0;
|
|
949
|
+
};
|
|
950
|
+
var findSsvcMetric = (cve) => {
|
|
951
|
+
const metrics = isRecord2(cve.metrics) ? cve.metrics : void 0;
|
|
952
|
+
return asArray(metrics?.ssvcV203).filter(isRecord2).map((entry) => entry.ssvcData).find((data) => isRecord2(data) && asString(data.role) === "CISA Coordinator");
|
|
953
|
+
};
|
|
954
|
+
var normalizeSsvc = (ssvcMetric) => ssvcMetric ? {
|
|
955
|
+
exploitation: ssvcOption(ssvcMetric.options, "exploitation"),
|
|
956
|
+
automatable: ssvcOption(ssvcMetric.options, "automatable"),
|
|
957
|
+
technicalImpact: ssvcOption(ssvcMetric.options, "technicalImpact"),
|
|
958
|
+
timestamp: asString(ssvcMetric.timestamp)
|
|
959
|
+
} : void 0;
|
|
960
|
+
var normalizeCisa = (cve) => {
|
|
961
|
+
const kev = normalizeKev(cve);
|
|
962
|
+
const ssvc = normalizeSsvc(findSsvcMetric(cve));
|
|
963
|
+
return kev || ssvc ? { kev, ssvc } : void 0;
|
|
964
|
+
};
|
|
965
|
+
var normalizeNvdCve = (value, expectedCveId) => {
|
|
966
|
+
if (!isRecord2(value)) return void 0;
|
|
967
|
+
const id = asString(value.id)?.toUpperCase();
|
|
968
|
+
if (!id || id !== expectedCveId.toUpperCase()) return void 0;
|
|
969
|
+
return {
|
|
970
|
+
id,
|
|
971
|
+
vulnerabilityStatus: asString(value.vulnStatus),
|
|
972
|
+
...selectDescription(value.descriptions),
|
|
973
|
+
published: asString(value.published),
|
|
974
|
+
lastModified: asString(value.lastModified),
|
|
975
|
+
cvss: selectCvss(value.metrics),
|
|
976
|
+
cwes: normalizeCwes(value.weaknesses),
|
|
977
|
+
references: normalizeReferences(value.references),
|
|
978
|
+
cisa: normalizeCisa(value)
|
|
979
|
+
};
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/scanner/nvd-client.ts
|
|
983
|
+
var NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0";
|
|
984
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
985
|
+
var DEFAULT_MAX_ATTEMPTS = 3;
|
|
986
|
+
var TRANSIENT_FAILURE_CIRCUIT_THRESHOLD = 3;
|
|
987
|
+
var CVE_PATTERN = /^CVE-\d{4}-\d{4,}$/;
|
|
988
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null;
|
|
989
|
+
var defaultSleep = (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
990
|
+
var isAttemptFailure = (outcome) => "result" in outcome;
|
|
991
|
+
var errorResult = (cveId, kind, httpStatus) => ({
|
|
992
|
+
status: "error",
|
|
993
|
+
cveId,
|
|
994
|
+
error: { kind, ...httpStatus === void 0 ? {} : { httpStatus } }
|
|
995
|
+
});
|
|
996
|
+
var parseRetryAfterDate = (value, now) => {
|
|
997
|
+
const date = Date.parse(value);
|
|
998
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
999
|
+
};
|
|
1000
|
+
var retryAfterMilliseconds = (value, now) => {
|
|
1001
|
+
if (!value) return void 0;
|
|
1002
|
+
const seconds = Number(value);
|
|
1003
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
1004
|
+
return parseRetryAfterDate(value, now);
|
|
1005
|
+
};
|
|
1006
|
+
var extractCveEntries = (body) => {
|
|
1007
|
+
if (!isRecord3(body) || !Array.isArray(body.vulnerabilities)) return void 0;
|
|
1008
|
+
return body.vulnerabilities;
|
|
1009
|
+
};
|
|
1010
|
+
var buildRequestHeaders = (apiKey) => {
|
|
1011
|
+
const headers = { Accept: "application/json" };
|
|
1012
|
+
if (apiKey) headers.apiKey = apiKey;
|
|
1013
|
+
return headers;
|
|
1014
|
+
};
|
|
1015
|
+
var errorKindForStatus = (status) => {
|
|
1016
|
+
if (status === 429) return "rate-limit";
|
|
1017
|
+
if (status >= 500) return "server";
|
|
1018
|
+
return "http";
|
|
1019
|
+
};
|
|
1020
|
+
var isRetryableStatus = (status) => status === 408 || status === 429 || status >= 500;
|
|
1021
|
+
var isEmptyResultBody = (body, entries) => entries.length === 0 || isRecord3(body) && body.totalResults === 0;
|
|
1022
|
+
var outcomeFromEntries = (entries, cveId) => {
|
|
1023
|
+
for (const entry of entries) {
|
|
1024
|
+
if (!isRecord3(entry)) continue;
|
|
1025
|
+
const normalized = normalizeNvdCve(entry.cve, cveId);
|
|
1026
|
+
if (normalized) return { status: "found", cveId, data: normalized };
|
|
1027
|
+
}
|
|
1028
|
+
return { result: errorResult(cveId, "invalid-response"), retryable: false };
|
|
1029
|
+
};
|
|
1030
|
+
var parseSuccessBody = (body, cveId) => {
|
|
1031
|
+
const entries = extractCveEntries(body);
|
|
1032
|
+
if (!entries) return { result: errorResult(cveId, "invalid-response"), retryable: false };
|
|
1033
|
+
if (isEmptyResultBody(body, entries)) return { status: "not-found", cveId };
|
|
1034
|
+
return outcomeFromEntries(entries, cveId);
|
|
1035
|
+
};
|
|
1036
|
+
var isTimeoutError = (error, signal) => signal.aborted || error instanceof Error && error.name === "AbortError";
|
|
1037
|
+
var attemptCatchResult = (error, signal, cveId) => ({
|
|
1038
|
+
result: errorResult(cveId, isTimeoutError(error, signal) ? "timeout" : "network"),
|
|
1039
|
+
retryable: true
|
|
1040
|
+
});
|
|
1041
|
+
var DefaultNvdClient = class {
|
|
1042
|
+
#fetch;
|
|
1043
|
+
#apiKey;
|
|
1044
|
+
#cache;
|
|
1045
|
+
#timeoutMs;
|
|
1046
|
+
#maxAttempts;
|
|
1047
|
+
#minIntervalMs;
|
|
1048
|
+
#sleep;
|
|
1049
|
+
#now;
|
|
1050
|
+
#random;
|
|
1051
|
+
#lookups = /* @__PURE__ */ new Map();
|
|
1052
|
+
#warnings = [];
|
|
1053
|
+
#requestQueue = Promise.resolve();
|
|
1054
|
+
#nextRequestAt = 0;
|
|
1055
|
+
#circuitOpen = false;
|
|
1056
|
+
#consecutiveTransientFailures = 0;
|
|
1057
|
+
constructor(options) {
|
|
1058
|
+
this.#fetch = options.fetchImpl ?? fetch;
|
|
1059
|
+
this.#apiKey = options.apiKey?.trim() || void 0;
|
|
1060
|
+
this.#cache = options.cache ?? createNvdCache();
|
|
1061
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1062
|
+
this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
1063
|
+
this.#minIntervalMs = options.minIntervalMs ?? (this.#apiKey ? 610 : 6100);
|
|
1064
|
+
this.#sleep = options.sleep ?? defaultSleep;
|
|
1065
|
+
this.#now = options.now ?? Date.now;
|
|
1066
|
+
this.#random = options.random ?? Math.random;
|
|
1067
|
+
}
|
|
1068
|
+
lookupCve(cveId) {
|
|
1069
|
+
const normalizedId = cveId.trim().toUpperCase();
|
|
1070
|
+
if (!CVE_PATTERN.test(normalizedId)) {
|
|
1071
|
+
return Promise.resolve(errorResult(normalizedId, "invalid-response"));
|
|
1072
|
+
}
|
|
1073
|
+
const existing = this.#lookups.get(normalizedId);
|
|
1074
|
+
if (existing) return existing;
|
|
1075
|
+
const lookup = this.#lookup(normalizedId);
|
|
1076
|
+
this.#lookups.set(normalizedId, lookup);
|
|
1077
|
+
return lookup;
|
|
1078
|
+
}
|
|
1079
|
+
async #shortCircuit(cveId) {
|
|
1080
|
+
const cached = await this.#cache.get(cveId);
|
|
1081
|
+
if (cached) return cached;
|
|
1082
|
+
if (this.#circuitOpen) return errorResult(cveId, "unavailable");
|
|
1083
|
+
return void 0;
|
|
1084
|
+
}
|
|
1085
|
+
async #retryUntilSuccess(cveId) {
|
|
1086
|
+
let finalFailure;
|
|
1087
|
+
for (let attempt = 0; attempt < this.#maxAttempts; attempt += 1) {
|
|
1088
|
+
const outcome = await this.#scheduleAttempt(cveId);
|
|
1089
|
+
if (!isAttemptFailure(outcome)) {
|
|
1090
|
+
this.#consecutiveTransientFailures = 0;
|
|
1091
|
+
await this.#cache.set(outcome);
|
|
1092
|
+
return { success: outcome };
|
|
1093
|
+
}
|
|
1094
|
+
finalFailure = outcome;
|
|
1095
|
+
if (!outcome.retryable || attempt === this.#maxAttempts - 1) break;
|
|
1096
|
+
const baseBackoff = Math.min(3e4, 1e3 * 2 ** attempt);
|
|
1097
|
+
const jitteredBackoff = baseBackoff * (0.8 + this.#random() * 0.4);
|
|
1098
|
+
await this.#sleep(Math.max(jitteredBackoff, outcome.retryAfterMs ?? 0));
|
|
1099
|
+
}
|
|
1100
|
+
return { failure: finalFailure ?? { result: errorResult(cveId, "unavailable"), retryable: false } };
|
|
1101
|
+
}
|
|
1102
|
+
#recordFailure(failure) {
|
|
1103
|
+
if (failure.retryable) {
|
|
1104
|
+
this.#consecutiveTransientFailures += 1;
|
|
1105
|
+
if (failure.result.error.kind === "rate-limit" || this.#consecutiveTransientFailures >= TRANSIENT_FAILURE_CIRCUIT_THRESHOLD) {
|
|
1106
|
+
this.#circuitOpen = true;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return failure.result;
|
|
1110
|
+
}
|
|
1111
|
+
async #lookup(cveId) {
|
|
1112
|
+
const shortCircuited = await this.#shortCircuit(cveId);
|
|
1113
|
+
if (shortCircuited) return shortCircuited;
|
|
1114
|
+
const outcome = await this.#retryUntilSuccess(cveId);
|
|
1115
|
+
return "success" in outcome ? outcome.success : this.#recordFailure(outcome.failure);
|
|
1116
|
+
}
|
|
1117
|
+
#scheduleAttempt(cveId) {
|
|
1118
|
+
const scheduled = this.#requestQueue.then(async () => {
|
|
1119
|
+
if (this.#circuitOpen) return errorResult(cveId, "unavailable");
|
|
1120
|
+
const waitMs = Math.max(0, this.#nextRequestAt - this.#now());
|
|
1121
|
+
if (waitMs > 0) await this.#sleep(waitMs);
|
|
1122
|
+
this.#nextRequestAt = this.#now() + this.#minIntervalMs;
|
|
1123
|
+
return this.#attempt(cveId);
|
|
1124
|
+
}).catch(() => ({ result: errorResult(cveId, "unavailable"), retryable: true }));
|
|
1125
|
+
this.#requestQueue = scheduled.then(
|
|
1126
|
+
() => void 0,
|
|
1127
|
+
() => void 0
|
|
1128
|
+
);
|
|
1129
|
+
return scheduled;
|
|
1130
|
+
}
|
|
1131
|
+
#failureForStatus(cveId, status, response) {
|
|
1132
|
+
if (status === 401 || status === 403) {
|
|
1133
|
+
this.#circuitOpen = true;
|
|
1134
|
+
return { result: errorResult(cveId, "forbidden", status), retryable: false };
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
result: errorResult(cveId, errorKindForStatus(status), status),
|
|
1138
|
+
retryable: isRetryableStatus(status),
|
|
1139
|
+
retryAfterMs: retryAfterMilliseconds(response.headers?.get("Retry-After"), this.#now())
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
async #safeFetch(url, headers, signal, cveId) {
|
|
1143
|
+
try {
|
|
1144
|
+
return await this.#fetch(url, { method: "GET", headers, signal });
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
return attemptCatchResult(error, signal, cveId);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async #parseBodySafely(response, cveId) {
|
|
1150
|
+
try {
|
|
1151
|
+
return parseSuccessBody(await response.json(), cveId);
|
|
1152
|
+
} catch {
|
|
1153
|
+
return { result: errorResult(cveId, "invalid-response"), retryable: false };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async #handleResponse(response, cveId) {
|
|
1157
|
+
const status = response.status ?? 0;
|
|
1158
|
+
if (status === 404) return { status: "not-found", cveId };
|
|
1159
|
+
if (!response.ok) return this.#failureForStatus(cveId, status, response);
|
|
1160
|
+
return this.#parseBodySafely(response, cveId);
|
|
1161
|
+
}
|
|
1162
|
+
async #handleFetchOutcome(fetched, cveId) {
|
|
1163
|
+
if ("result" in fetched) return fetched;
|
|
1164
|
+
return this.#handleResponse(fetched, cveId);
|
|
1165
|
+
}
|
|
1166
|
+
async #attempt(cveId) {
|
|
1167
|
+
const url = new URL(NVD_API_URL);
|
|
1168
|
+
url.searchParams.set("cveId", cveId);
|
|
1169
|
+
const headers = buildRequestHeaders(this.#apiKey);
|
|
1170
|
+
const signal = AbortSignal.timeout(this.#timeoutMs);
|
|
1171
|
+
const fetched = await this.#safeFetch(url.toString(), headers, signal, cveId);
|
|
1172
|
+
return this.#handleFetchOutcome(fetched, cveId);
|
|
1173
|
+
}
|
|
1174
|
+
consumeWarnings() {
|
|
1175
|
+
return [...this.#warnings.splice(0), ...this.#cache.consumeWarnings()];
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var createNvdClient = (options = {}) => new DefaultNvdClient(options);
|
|
1179
|
+
|
|
1180
|
+
// src/scanner/nvd-enrichment.ts
|
|
1181
|
+
var CVE_PATTERN2 = /^CVE-\d{4}-\d{4,}$/;
|
|
1182
|
+
var extractCveAliases = (aliases) => {
|
|
1183
|
+
const cves = (aliases ?? []).map((alias) => alias.trim().toUpperCase()).filter((alias) => CVE_PATTERN2.test(alias));
|
|
1184
|
+
return [...new Set(cves)];
|
|
1185
|
+
};
|
|
1186
|
+
var coverageFrom = (results) => ({
|
|
1187
|
+
total: results.length,
|
|
1188
|
+
enriched: results.filter((result) => result.status === "found").length,
|
|
1189
|
+
notFound: results.filter((result) => result.status === "not-found").length,
|
|
1190
|
+
failed: results.filter((result) => result.status === "error").length,
|
|
1191
|
+
cacheHits: results.filter((result) => result.status !== "error" && result.fromCache).length
|
|
1192
|
+
});
|
|
1193
|
+
var resolveLookups = async (lookups) => {
|
|
1194
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1195
|
+
let unexpectedFailure = false;
|
|
1196
|
+
try {
|
|
1197
|
+
await Promise.all(
|
|
1198
|
+
[...lookups].map(async ([cveId, lookup]) => {
|
|
1199
|
+
try {
|
|
1200
|
+
resolved.set(cveId, await lookup);
|
|
1201
|
+
} catch {
|
|
1202
|
+
resolved.set(cveId, { status: "error", cveId, error: { kind: "unavailable" } });
|
|
1203
|
+
}
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
} catch {
|
|
1207
|
+
unexpectedFailure = true;
|
|
1208
|
+
}
|
|
1209
|
+
return { resolved, unexpectedFailure };
|
|
1210
|
+
};
|
|
1211
|
+
var enrichMatch = (match, cveIds, resolved) => ({
|
|
1212
|
+
...match,
|
|
1213
|
+
cveIds,
|
|
1214
|
+
nvd: cveIds.map((cveId) => resolved.get(cveId)).filter((item) => item !== void 0)
|
|
1215
|
+
});
|
|
1216
|
+
var enrichmentWarnings = (client, coverage, unexpectedFailure) => [
|
|
1217
|
+
...client.consumeWarnings(),
|
|
1218
|
+
...coverage.failed > 0 ? [`N\xE3o foi poss\xEDvel consultar o NVD para ${coverage.failed} CVE(s); os findings OSV foram preservados.`] : [],
|
|
1219
|
+
...unexpectedFailure ? [
|
|
1220
|
+
"Falha interna inesperada ao consolidar as consultas do NVD; alguns CVEs podem n\xE3o ter sido enriquecidos."
|
|
1221
|
+
] : []
|
|
1222
|
+
];
|
|
1223
|
+
var enrichOsvMatchesWithNvd = async (matches, client) => {
|
|
1224
|
+
const cveIdsByMatch = matches.map((match) => extractCveAliases(match.vuln.aliases));
|
|
1225
|
+
const uniqueCveIds = [...new Set(cveIdsByMatch.flat())];
|
|
1226
|
+
const lookups = new Map(
|
|
1227
|
+
uniqueCveIds.map((cveId) => [cveId, client.lookupCve(cveId)])
|
|
1228
|
+
);
|
|
1229
|
+
let resolution;
|
|
1230
|
+
try {
|
|
1231
|
+
resolution = await resolveLookups(lookups);
|
|
1232
|
+
} catch {
|
|
1233
|
+
resolution = { resolved: /* @__PURE__ */ new Map(), unexpectedFailure: true };
|
|
1234
|
+
}
|
|
1235
|
+
const { resolved, unexpectedFailure } = resolution;
|
|
1236
|
+
const results = uniqueCveIds.map((cveId) => resolved.get(cveId)).filter((item) => Boolean(item));
|
|
1237
|
+
const coverage = coverageFrom(results);
|
|
1238
|
+
return {
|
|
1239
|
+
matches: matches.map(
|
|
1240
|
+
(match, index) => (
|
|
1241
|
+
// codesentry-disable-next-line security/detect-object-injection -- index originates from map over the same cveIdsByMatch array.
|
|
1242
|
+
enrichMatch(match, cveIdsByMatch[index] ?? [], resolved)
|
|
1243
|
+
)
|
|
1244
|
+
),
|
|
1245
|
+
coverage,
|
|
1246
|
+
warnings: enrichmentWarnings(client, coverage, unexpectedFailure)
|
|
1247
|
+
};
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/scanner/run-with-concurrency-limit.ts
|
|
1251
|
+
import pLimit from "p-limit";
|
|
1252
|
+
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
1253
|
+
const limit = pLimit(concurrency);
|
|
1254
|
+
return Promise.all(items.map((item) => limit(() => task(item))));
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/scanner/osv-client.ts
|
|
1258
|
+
var OSV_BATCH_CHUNK_SIZE = 100;
|
|
1259
|
+
var OSV_DETAIL_CONCURRENCY = 10;
|
|
1260
|
+
var OSV_API_BASE = "https://api.osv.dev/v1";
|
|
1261
|
+
var SEVERITY_MAP = {
|
|
1262
|
+
LOW: "low",
|
|
1263
|
+
MODERATE: "medium",
|
|
1264
|
+
HIGH: "high",
|
|
1265
|
+
CRITICAL: "critical"
|
|
1266
|
+
};
|
|
1267
|
+
var chunk = (items, size) => {
|
|
1268
|
+
const chunks = [];
|
|
1269
|
+
for (let i = 0; i < items.length; i += size) {
|
|
1270
|
+
chunks.push(items.slice(i, i + size));
|
|
1271
|
+
}
|
|
1272
|
+
return chunks;
|
|
1273
|
+
};
|
|
1274
|
+
var zipVulnIdsByPackage = (packages, body) => {
|
|
1275
|
+
const vulnIdsByPackage = /* @__PURE__ */ new Map();
|
|
1276
|
+
packages.forEach((pkg, index) => {
|
|
1277
|
+
const ids = body.results?.[index]?.vulns?.map((v) => v.id);
|
|
1278
|
+
if (ids?.length) {
|
|
1279
|
+
vulnIdsByPackage.set(`${pkg.name}@${pkg.version}`, ids);
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
return vulnIdsByPackage;
|
|
1283
|
+
};
|
|
1284
|
+
var queryBatchChunk = async (packages, fetchImpl) => {
|
|
1285
|
+
try {
|
|
1286
|
+
const response = await fetchImpl(`${OSV_API_BASE}/querybatch`, {
|
|
1287
|
+
method: "POST",
|
|
1288
|
+
headers: { "Content-Type": "application/json" },
|
|
1289
|
+
body: JSON.stringify({
|
|
1290
|
+
queries: packages.map((p) => ({
|
|
1291
|
+
package: { name: p.name, ecosystem: "npm" },
|
|
1292
|
+
version: p.version
|
|
1293
|
+
}))
|
|
1294
|
+
})
|
|
1295
|
+
});
|
|
1296
|
+
if (!response.ok) {
|
|
1297
|
+
return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
|
|
1298
|
+
}
|
|
1299
|
+
const body = await response.json();
|
|
1300
|
+
return { vulnIdsByPackage: zipVulnIdsByPackage(packages, body), checkedPackages: packages, failedCount: 0 };
|
|
1301
|
+
} catch {
|
|
1302
|
+
return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
var queryOsvBatch = async (packages, fetchImpl = fetch) => {
|
|
1306
|
+
const vulnIdsByPackage = /* @__PURE__ */ new Map();
|
|
1307
|
+
const checkedPackages = [];
|
|
1308
|
+
let failedCount = 0;
|
|
1309
|
+
try {
|
|
1310
|
+
for (const packageChunk of chunk(packages, OSV_BATCH_CHUNK_SIZE)) {
|
|
1311
|
+
const result = await queryBatchChunk(packageChunk, fetchImpl);
|
|
1312
|
+
result.vulnIdsByPackage.forEach((ids, key) => vulnIdsByPackage.set(key, ids));
|
|
1313
|
+
checkedPackages.push(...result.checkedPackages);
|
|
1314
|
+
failedCount += result.failedCount;
|
|
1315
|
+
}
|
|
1316
|
+
} catch {
|
|
1317
|
+
failedCount += packages.length - checkedPackages.length;
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
vulnIdsByPackage,
|
|
1321
|
+
checkedPackages,
|
|
1322
|
+
warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel consultar o OSV.dev para ${failedCount} pacote(s).` : void 0
|
|
1323
|
+
};
|
|
1324
|
+
};
|
|
1325
|
+
var fetchOneVulnerabilityDetail = async (id, fetchImpl, detailsById) => {
|
|
1326
|
+
try {
|
|
1327
|
+
const response = await fetchImpl(`${OSV_API_BASE}/vulns/${id}`);
|
|
1328
|
+
if (!response.ok) {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
detailsById.set(id, await response.json());
|
|
1332
|
+
return true;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
var fetchOsvVulnerabilityDetails = async (ids, fetchImpl = fetch) => {
|
|
1338
|
+
const uniqueIds = [...new Set(ids)];
|
|
1339
|
+
const detailsById = /* @__PURE__ */ new Map();
|
|
1340
|
+
let results;
|
|
1341
|
+
try {
|
|
1342
|
+
results = await runWithConcurrencyLimit(
|
|
1343
|
+
uniqueIds,
|
|
1344
|
+
OSV_DETAIL_CONCURRENCY,
|
|
1345
|
+
(id) => fetchOneVulnerabilityDetail(id, fetchImpl, detailsById)
|
|
1346
|
+
);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1350
|
+
const failedCount = results.filter((ok) => !ok).length;
|
|
1351
|
+
return {
|
|
1352
|
+
detailsById,
|
|
1353
|
+
warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel obter detalhes do OSV.dev para ${failedCount} advisory(s).` : void 0
|
|
1354
|
+
};
|
|
1355
|
+
};
|
|
1356
|
+
var isMatchingNpmPackage = (affected, packageName) => affected.package.ecosystem === "npm" && affected.package.name === packageName;
|
|
1357
|
+
var extractFixedVersions = (vuln, packageName) => {
|
|
1358
|
+
const fixedVersions = (vuln.affected ?? []).filter((affected) => isMatchingNpmPackage(affected, packageName)).flatMap((affected) => affected.ranges ?? []).flatMap((range) => range.events).map((event) => event.fixed).filter((fixed) => Boolean(fixed));
|
|
1359
|
+
return [...new Set(fixedVersions)];
|
|
1360
|
+
};
|
|
1361
|
+
var mapOsvSeverity = (vuln) => {
|
|
1362
|
+
const severity = vuln.database_specific?.severity;
|
|
1363
|
+
if (!severity) {
|
|
1364
|
+
return "medium";
|
|
1365
|
+
}
|
|
1366
|
+
return SEVERITY_MAP[severity] ?? "medium";
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/scanner/package-lock-parser.ts
|
|
1370
|
+
var NODE_MODULES_SEGMENT = "node_modules/";
|
|
1371
|
+
var isUnresolvableSource = (resolved) => resolved !== void 0 && (resolved.startsWith("file:") || resolved.startsWith("git"));
|
|
1372
|
+
var nameFromKey = (key) => {
|
|
1373
|
+
const lastIndex = key.lastIndexOf(NODE_MODULES_SEGMENT);
|
|
1374
|
+
return lastIndex === -1 ? void 0 : key.slice(lastIndex + NODE_MODULES_SEGMENT.length);
|
|
1375
|
+
};
|
|
1376
|
+
var toLockedPackage = (key, entry) => {
|
|
1377
|
+
if (entry.link || !entry.version || isUnresolvableSource(entry.resolved)) {
|
|
1378
|
+
return void 0;
|
|
1379
|
+
}
|
|
1380
|
+
const name = entry.name ?? nameFromKey(key);
|
|
1381
|
+
return name ? { name, version: entry.version } : void 0;
|
|
1382
|
+
};
|
|
1383
|
+
var assertSupportedLockfileVersion = (lockfileVersion) => {
|
|
1384
|
+
if (lockfileVersion !== 2 && lockfileVersion !== 3) {
|
|
1385
|
+
throw new Error("lockfileVersion 1 n\xE3o \xE9 suportado \u2014 regenere o lockfile com npm 7+.");
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var parsePackageLock = (rawJson) => {
|
|
1389
|
+
const parsed = JSON.parse(rawJson);
|
|
1390
|
+
assertSupportedLockfileVersion(parsed.lockfileVersion);
|
|
1391
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
1392
|
+
Object.entries(parsed.packages ?? {}).filter(([key]) => key.includes(NODE_MODULES_SEGMENT)).forEach(([key, entry]) => {
|
|
1393
|
+
const locked = toLockedPackage(key, entry);
|
|
1394
|
+
if (locked) {
|
|
1395
|
+
deduped.set(`${locked.name}@${locked.version}`, locked);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
return [...deduped.values()];
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// src/scanner/dependency-audit.ts
|
|
1402
|
+
var execFileAsync = promisify(execFile);
|
|
1403
|
+
var SEVERITY_MAP2 = {
|
|
1404
|
+
info: "low",
|
|
1405
|
+
low: "low",
|
|
1406
|
+
moderate: "medium",
|
|
1407
|
+
high: "high",
|
|
1408
|
+
critical: "critical"
|
|
1409
|
+
};
|
|
1410
|
+
var NVD_SEVERITY_MAP = {
|
|
1411
|
+
LOW: "low",
|
|
1412
|
+
MEDIUM: "medium",
|
|
1413
|
+
HIGH: "high",
|
|
1414
|
+
CRITICAL: "critical"
|
|
1415
|
+
};
|
|
1416
|
+
var SEVERITY_WEIGHT = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
1417
|
+
var CANONICAL_ADVISORY_PATTERN = /(?:CVE-\d{4}-\d{4,}|GHSA-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4})/gi;
|
|
1418
|
+
var npmFixSuggestion = (vulnerability) => {
|
|
1419
|
+
const { fixAvailable } = vulnerability;
|
|
1420
|
+
if (fixAvailable === false) return "sem corre\xE7\xE3o dispon\xEDvel ainda";
|
|
1421
|
+
if (fixAvailable === true)
|
|
1422
|
+
return `atualize para uma vers\xE3o fora do intervalo vulner\xE1vel (${vulnerability.range})`;
|
|
1423
|
+
return `atualize para ${fixAvailable.name}@${fixAvailable.version}`;
|
|
1424
|
+
};
|
|
1425
|
+
var canonicalIdsFrom = (...values) => [
|
|
1426
|
+
...new Set(
|
|
1427
|
+
values.flatMap((value) => String(value ?? "").match(CANONICAL_ADVISORY_PATTERN) ?? []).map((id) => id.toUpperCase())
|
|
1428
|
+
)
|
|
1429
|
+
];
|
|
1430
|
+
var npmAdvisoryTitle = (advisory) => {
|
|
1431
|
+
if (typeof advisory === "object" && advisory.title) return advisory.title;
|
|
1432
|
+
if (typeof advisory === "string") return `vulnerabilidade transitiva via ${advisory}`;
|
|
1433
|
+
return 'ver "npm audit" para detalhes';
|
|
1434
|
+
};
|
|
1435
|
+
var npmFixedVersions = (fixAvailable) => typeof fixAvailable === "object" ? [fixAvailable.version] : [];
|
|
1436
|
+
var npmFinding = (vulnerability, advisory, ids) => {
|
|
1437
|
+
const title = npmAdvisoryTitle(advisory);
|
|
1438
|
+
return {
|
|
1439
|
+
ruleId: "dependency-audit",
|
|
1440
|
+
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${title} \u2014 ${npmFixSuggestion(vulnerability)}`,
|
|
1441
|
+
file: "package.json",
|
|
1442
|
+
line: 1,
|
|
1443
|
+
// codesentry-disable-next-line security/detect-object-injection -- vulnerability.severity is npm audit's own NpmAuditSeverity union, not attacker input; unknown values just look up as undefined.
|
|
1444
|
+
severity: SEVERITY_MAP2[vulnerability.severity],
|
|
1445
|
+
dependency: {
|
|
1446
|
+
package: {
|
|
1447
|
+
name: vulnerability.name,
|
|
1448
|
+
installedVersion: "n\xE3o informada pelo npm audit",
|
|
1449
|
+
fixedVersions: npmFixedVersions(vulnerability.fixAvailable)
|
|
1450
|
+
},
|
|
1451
|
+
advisory: { source: "npm", id: ids[0], aliases: ids.slice(1), summary: title }
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
};
|
|
1455
|
+
var advisoryIds = (advisory) => typeof advisory === "object" ? canonicalIdsFrom(advisory.source, advisory.url, advisory.title) : canonicalIdsFrom(advisory);
|
|
1456
|
+
var advisoryIdentity = (advisory, ids) => {
|
|
1457
|
+
if (ids.length) return `ids:${[...ids].sort().join(",")}`;
|
|
1458
|
+
if (typeof advisory === "object")
|
|
1459
|
+
return `fields:${advisory.source ?? ""}|${advisory.url ?? ""}|${advisory.title ?? ""}`;
|
|
1460
|
+
return `via:${advisory ?? "unknown"}`;
|
|
1461
|
+
};
|
|
1462
|
+
var npmAdvisoryEntries = (vulnerability) => {
|
|
1463
|
+
const structured = vulnerability.via.filter(
|
|
1464
|
+
(advisory) => typeof advisory === "object"
|
|
1465
|
+
);
|
|
1466
|
+
const candidates = structured.length ? structured : [vulnerability.via.find((advisory) => typeof advisory === "string")];
|
|
1467
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1468
|
+
return candidates.filter((advisory) => {
|
|
1469
|
+
const identity = advisoryIdentity(advisory, advisoryIds(advisory));
|
|
1470
|
+
if (seen.has(identity)) return false;
|
|
1471
|
+
seen.add(identity);
|
|
1472
|
+
return true;
|
|
1473
|
+
});
|
|
1474
|
+
};
|
|
1475
|
+
var mapAuditReportToFindings = (report, osvIdsByPackage = /* @__PURE__ */ new Map()) => Object.values(report.vulnerabilities).flatMap((vulnerability) => {
|
|
1476
|
+
const osvIds = osvIdsByPackage.get(vulnerability.name) ?? /* @__PURE__ */ new Set();
|
|
1477
|
+
return npmAdvisoryEntries(vulnerability).flatMap((advisory) => {
|
|
1478
|
+
const ids = advisoryIds(advisory);
|
|
1479
|
+
return ids.some((id) => osvIds.has(id)) ? [] : [npmFinding(vulnerability, advisory, ids)];
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
var osvFixSuggestion = (fixedVersions) => fixedVersions.length ? `atualize para ${fixedVersions.join(" ou ")}` : "nenhuma vers\xE3o corrigida publicada pelo OSV.dev ainda";
|
|
1483
|
+
var normalizeOsvAliases = (aliases) => {
|
|
1484
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const alias of aliases ?? []) {
|
|
1486
|
+
const trimmed = alias.trim();
|
|
1487
|
+
if (!trimmed) continue;
|
|
1488
|
+
const value = extractCveAliases([trimmed])[0] ?? trimmed;
|
|
1489
|
+
const key = value.toUpperCase();
|
|
1490
|
+
if (!normalized.has(key)) normalized.set(key, value);
|
|
1491
|
+
}
|
|
1492
|
+
return [...normalized.values()];
|
|
1493
|
+
};
|
|
1494
|
+
var highestNvdSeverity = (results) => results.filter((result) => result.status === "found").map((result) => result.data.cvss?.severity).filter((severity) => severity !== void 0).map((severity) => NVD_SEVERITY_MAP[severity]).filter((severity) => severity !== void 0).sort((a, b) => SEVERITY_WEIGHT[b] - SEVERITY_WEIGHT[a])[0];
|
|
1495
|
+
var buildOsvFinding = (match) => ({
|
|
1496
|
+
ruleId: "dependency-audit",
|
|
1497
|
+
message: `OSV ${match.vuln.id}: ${match.pkg.name}@${match.pkg.version} \u2014 ${match.vuln.summary ?? "ver OSV.dev para detalhes"} \u2014 ${osvFixSuggestion(match.fixedVersions)}`,
|
|
1498
|
+
file: "package-lock.json",
|
|
1499
|
+
line: 1,
|
|
1500
|
+
severity: highestNvdSeverity(match.nvd) ?? mapOsvSeverity(match.vuln),
|
|
1501
|
+
dependency: {
|
|
1502
|
+
package: {
|
|
1503
|
+
name: match.pkg.name,
|
|
1504
|
+
installedVersion: match.pkg.version,
|
|
1505
|
+
fixedVersions: match.fixedVersions
|
|
1506
|
+
},
|
|
1507
|
+
advisory: {
|
|
1508
|
+
source: "osv",
|
|
1509
|
+
id: match.vuln.id,
|
|
1510
|
+
aliases: normalizeOsvAliases(match.vuln.aliases),
|
|
1511
|
+
summary: match.vuln.summary
|
|
1512
|
+
},
|
|
1513
|
+
nvd: match.nvd
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
var unenrichedMatch = (match) => ({
|
|
1517
|
+
...match,
|
|
1518
|
+
cveIds: extractCveAliases(match.vuln.aliases),
|
|
1519
|
+
nvd: []
|
|
1520
|
+
});
|
|
1521
|
+
var collectMatches = (lockedPackages, vulnIdsByPackage, detailsById) => lockedPackages.flatMap(
|
|
1522
|
+
(pkg) => [...new Set(vulnIdsByPackage.get(`${pkg.name}@${pkg.version}`) ?? [])].map((id) => detailsById.get(id)).filter((vuln) => vuln !== void 0).map((vuln) => ({ pkg, vuln, fixedVersions: extractFixedVersions(vuln, pkg.name) }))
|
|
1523
|
+
);
|
|
1524
|
+
var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
1525
|
+
var hasVulnerabilitiesRecord = (value) => typeof value === "object" && value !== null && typeof value.vulnerabilities === "object" && value.vulnerabilities !== null;
|
|
1526
|
+
var normalizeNpmAuditReport = (raw) => {
|
|
1527
|
+
if (hasVulnerabilitiesRecord(raw)) return { report: { vulnerabilities: raw.vulnerabilities } };
|
|
1528
|
+
const errorSummary = typeof raw === "object" && raw !== null && "error" in raw ? raw.error?.summary ?? "formato de resposta inesperado" : "formato de resposta inesperado";
|
|
1529
|
+
return {
|
|
1530
|
+
report: { vulnerabilities: {} },
|
|
1531
|
+
warning: `"npm audit" n\xE3o retornou um relat\xF3rio v\xE1lido: ${errorSummary}.`
|
|
1532
|
+
};
|
|
1533
|
+
};
|
|
1534
|
+
var collectOsvMatches = async (lockedPackages, fetchImpl) => {
|
|
1535
|
+
try {
|
|
1536
|
+
const {
|
|
1537
|
+
vulnIdsByPackage,
|
|
1538
|
+
checkedPackages,
|
|
1539
|
+
warning: batchWarning
|
|
1540
|
+
} = await queryOsvBatch(lockedPackages, fetchImpl);
|
|
1541
|
+
const ids = [...new Set([...vulnIdsByPackage.values()].flat())];
|
|
1542
|
+
const detailResult = ids.length ? await fetchOsvVulnerabilityDetails(ids, fetchImpl) : { detailsById: /* @__PURE__ */ new Map(), warning: void 0 };
|
|
1543
|
+
return {
|
|
1544
|
+
matches: collectMatches(lockedPackages, vulnIdsByPackage, detailResult.detailsById),
|
|
1545
|
+
checkedPackages,
|
|
1546
|
+
warnings: [batchWarning, detailResult.warning].filter(
|
|
1547
|
+
(warning) => Boolean(warning)
|
|
1548
|
+
)
|
|
1549
|
+
};
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
return {
|
|
1552
|
+
matches: [],
|
|
1553
|
+
checkedPackages: [],
|
|
1554
|
+
warnings: [`N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`]
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
var runNpmAudit = async (targetDir) => {
|
|
1559
|
+
let stdout;
|
|
1560
|
+
try {
|
|
1561
|
+
({ stdout } = await execFileAsync("npm", ["audit", "--json"], {
|
|
1562
|
+
cwd: targetDir,
|
|
1563
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1564
|
+
}));
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
const stdoutFromError = error.stdout;
|
|
1567
|
+
if (!stdoutFromError)
|
|
1568
|
+
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
1569
|
+
stdout = stdoutFromError;
|
|
1570
|
+
}
|
|
1571
|
+
return JSON.parse(stdout);
|
|
1572
|
+
};
|
|
1573
|
+
var osvIdentityIndex = (matches) => {
|
|
1574
|
+
const index = /* @__PURE__ */ new Map();
|
|
1575
|
+
matches.forEach((match) => {
|
|
1576
|
+
const ids = index.get(match.pkg.name) ?? /* @__PURE__ */ new Set();
|
|
1577
|
+
[match.vuln.id, ...match.vuln.aliases ?? []].forEach((id) => ids.add(id.trim().toUpperCase()));
|
|
1578
|
+
index.set(match.pkg.name, ids);
|
|
1579
|
+
});
|
|
1580
|
+
return index;
|
|
1581
|
+
};
|
|
1582
|
+
var emptyNvdCoverage = (total, failed) => ({
|
|
1583
|
+
total,
|
|
1584
|
+
enriched: 0,
|
|
1585
|
+
notFound: 0,
|
|
1586
|
+
failed,
|
|
1587
|
+
cacheHits: 0
|
|
1588
|
+
});
|
|
1589
|
+
var resolveNvdClient = (options) => options.nvdClient ?? createNvdClient({
|
|
1590
|
+
fetchImpl: options.fetchImpl,
|
|
1591
|
+
apiKey: options.nvdApiKey ?? process.env.NVD_API_KEY,
|
|
1592
|
+
cache: options.nvdCache ?? createNvdCache()
|
|
1593
|
+
});
|
|
1594
|
+
var enrichWithNvdIfEnabled = async (collected, options) => {
|
|
1595
|
+
if (!options.nvdEnabled) {
|
|
1596
|
+
return { matches: collected.matches.map(unenrichedMatch), nvd: false, warnings: [] };
|
|
1597
|
+
}
|
|
1598
|
+
const uniqueCveIds = new Set(collected.matches.flatMap((match) => extractCveAliases(match.vuln.aliases)));
|
|
1599
|
+
if (uniqueCveIds.size === 0) {
|
|
1600
|
+
return { matches: collected.matches.map(unenrichedMatch), nvd: emptyNvdCoverage(0, 0), warnings: [] };
|
|
1601
|
+
}
|
|
1602
|
+
try {
|
|
1603
|
+
const enrichment = await enrichOsvMatchesWithNvd(collected.matches, resolveNvdClient(options));
|
|
1604
|
+
return { matches: enrichment.matches, nvd: enrichment.coverage, warnings: enrichment.warnings };
|
|
1605
|
+
} catch {
|
|
1606
|
+
const total = uniqueCveIds.size;
|
|
1607
|
+
return {
|
|
1608
|
+
matches: collected.matches.map(unenrichedMatch),
|
|
1609
|
+
nvd: emptyNvdCoverage(total, total),
|
|
1610
|
+
warnings: [
|
|
1611
|
+
`N\xE3o foi poss\xEDvel consultar o NVD para ${total} CVE(s); os findings OSV foram preservados.`
|
|
1612
|
+
]
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
var successfulLockfileAudit = (lockedPackages, collected, enrichment) => ({
|
|
1617
|
+
findings: enrichment.matches.map(buildOsvFinding),
|
|
1618
|
+
matches: collected.matches,
|
|
1619
|
+
packagesAudited: lockedPackages.length,
|
|
1620
|
+
osvChecked: { checked: collected.checkedPackages.length, total: lockedPackages.length },
|
|
1621
|
+
osvCheckedPackages: collected.checkedPackages.map((pkg) => `${pkg.name}@${pkg.version}`).sort(),
|
|
1622
|
+
nvd: enrichment.nvd,
|
|
1623
|
+
warnings: [...collected.warnings, ...enrichment.warnings]
|
|
1624
|
+
});
|
|
1625
|
+
var auditPackagesFromLockfile = async (targetDir, options) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const raw = await readFile2(join2(targetDir, "package-lock.json"), "utf-8");
|
|
1628
|
+
const lockedPackages = parsePackageLock(raw);
|
|
1629
|
+
const collected = await collectOsvMatches(lockedPackages, options.fetchImpl);
|
|
1630
|
+
const enrichment = await enrichWithNvdIfEnabled(collected, options);
|
|
1631
|
+
return successfulLockfileAudit(lockedPackages, collected, enrichment);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
return {
|
|
1634
|
+
findings: [],
|
|
1635
|
+
matches: [],
|
|
1636
|
+
packagesAudited: false,
|
|
1637
|
+
nvd: options.nvdEnabled ? { total: 0, enriched: 0, notFound: 0, failed: 0, cacheHits: 0 } : false,
|
|
1638
|
+
warnings: [`N\xE3o foi poss\xEDvel checar o OSV.dev: ${errorMessage(error)}.`]
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
var normalizeOptions = (optionsOrFetch) => typeof optionsOrFetch === "function" ? { fetchImpl: optionsOrFetch } : optionsOrFetch ?? {};
|
|
1643
|
+
var buildDependencyAuditResult = (startedAt, npmReport, npmWarning, osv) => {
|
|
1644
|
+
const npmFindings = mapAuditReportToFindings(npmReport, osvIdentityIndex(osv.matches));
|
|
1645
|
+
const warnings = [npmWarning, ...osv.warnings].filter((warning) => Boolean(warning));
|
|
1646
|
+
return {
|
|
1647
|
+
scannedFiles: 1,
|
|
1648
|
+
findings: [...npmFindings, ...osv.findings],
|
|
1649
|
+
durationMs: Date.now() - startedAt,
|
|
1650
|
+
engines: {
|
|
1651
|
+
dependencyAudit: osv.packagesAudited === false ? Object.keys(npmReport.vulnerabilities).length : osv.packagesAudited,
|
|
1652
|
+
osv: osv.osvChecked,
|
|
1653
|
+
nvd: osv.nvd
|
|
1654
|
+
},
|
|
1655
|
+
osvCheckedPackages: osv.osvCheckedPackages,
|
|
1656
|
+
warnings: warnings.length ? warnings : void 0
|
|
1657
|
+
};
|
|
1658
|
+
};
|
|
1659
|
+
var runDependencyAudit = async (targetDir, optionsOrFetch) => {
|
|
1660
|
+
const startedAt = Date.now();
|
|
1661
|
+
const supplied = normalizeOptions(optionsOrFetch);
|
|
1662
|
+
const options = {
|
|
1663
|
+
...supplied,
|
|
1664
|
+
fetchImpl: supplied.fetchImpl ?? fetch,
|
|
1665
|
+
nvdEnabled: supplied.nvdEnabled ?? true
|
|
1666
|
+
};
|
|
1667
|
+
try {
|
|
1668
|
+
const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(
|
|
1669
|
+
await (options.npmAuditRunner ?? runNpmAudit)(targetDir)
|
|
1670
|
+
);
|
|
1671
|
+
const osv = await auditPackagesFromLockfile(targetDir, options);
|
|
1672
|
+
return buildDependencyAuditResult(startedAt, npmReport, npmWarning, osv);
|
|
1673
|
+
} catch (error) {
|
|
1674
|
+
throw new Error(`N\xE3o foi poss\xEDvel auditar depend\xEAncias em "${targetDir}".`, { cause: error });
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
|
|
329
1678
|
// src/scanner/scanner.ts
|
|
330
|
-
import { readFile } from "fs/promises";
|
|
1679
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
331
1680
|
import { availableParallelism } from "os";
|
|
332
1681
|
|
|
333
1682
|
// src/scanner/file-finder.ts
|
|
334
1683
|
import { readdir } from "fs/promises";
|
|
335
|
-
import { join } from "path";
|
|
1684
|
+
import { join as join3 } from "path";
|
|
336
1685
|
|
|
337
1686
|
// src/scanner/ignore-patterns.ts
|
|
338
|
-
var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next"];
|
|
1687
|
+
var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next", ".angular"];
|
|
339
1688
|
var TEST_DIR_NAMES = ["tests", "test", "__tests__"];
|
|
340
1689
|
var TEST_FILE_GLOBS = ["*.spec.*", "*.test.*"];
|
|
341
1690
|
var TEST_FILE_NAME_PATTERN = /\.(spec|test)\.[^./]+$/;
|
|
@@ -345,11 +1694,17 @@ var isTestFileName = (fileName) => TEST_FILE_NAME_PATTERN.test(fileName);
|
|
|
345
1694
|
// src/scanner/file-finder.ts
|
|
346
1695
|
var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
|
|
347
1696
|
var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
1697
|
+
var filesFromDirectory = async (currentDir, entry, includeTests) => {
|
|
1698
|
+
return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join3(currentDir, entry.name), includeTests);
|
|
1699
|
+
};
|
|
1700
|
+
var filesFromFile = (currentDir, entry, includeTests) => {
|
|
1701
|
+
return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join3(currentDir, entry.name)] : [];
|
|
1702
|
+
};
|
|
348
1703
|
var filesFromEntry = async (currentDir, entry, includeTests) => {
|
|
349
1704
|
if (entry.isDirectory()) {
|
|
350
|
-
return
|
|
1705
|
+
return filesFromDirectory(currentDir, entry, includeTests);
|
|
351
1706
|
}
|
|
352
|
-
return
|
|
1707
|
+
return filesFromFile(currentDir, entry, includeTests);
|
|
353
1708
|
};
|
|
354
1709
|
var walk = async (currentDir, includeTests) => {
|
|
355
1710
|
try {
|
|
@@ -367,26 +1722,19 @@ var findFiles = async (targetDir, includeTests = false) => {
|
|
|
367
1722
|
}
|
|
368
1723
|
};
|
|
369
1724
|
|
|
370
|
-
// src/scanner/run-with-concurrency-limit.ts
|
|
371
|
-
import pLimit from "p-limit";
|
|
372
|
-
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
373
|
-
const limit = pLimit(concurrency);
|
|
374
|
-
return Promise.all(items.map((item) => limit(() => task(item))));
|
|
375
|
-
};
|
|
376
|
-
|
|
377
1725
|
// src/scanner/scanner.ts
|
|
378
1726
|
var DEFAULT_SCAN_CONCURRENCY = Math.max(1, Math.min(8, availableParallelism()));
|
|
379
|
-
var
|
|
1727
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
380
1728
|
var parseErrorFinding = (filePath, error) => ({
|
|
381
1729
|
ruleId: "parse-error",
|
|
382
|
-
message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${
|
|
1730
|
+
message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${errorMessage2(error)}`,
|
|
383
1731
|
file: filePath,
|
|
384
1732
|
line: 1,
|
|
385
1733
|
severity: "low"
|
|
386
1734
|
});
|
|
387
1735
|
var readFileContent = async (filePath) => {
|
|
388
1736
|
try {
|
|
389
|
-
return await
|
|
1737
|
+
return await readFile3(filePath, "utf-8");
|
|
390
1738
|
} catch (error) {
|
|
391
1739
|
throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
|
|
392
1740
|
}
|
|
@@ -433,14 +1781,14 @@ var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY, i
|
|
|
433
1781
|
};
|
|
434
1782
|
|
|
435
1783
|
// src/scanner/semgrep.ts
|
|
436
|
-
import { execFile } from "child_process";
|
|
437
|
-
import { delimiter, dirname as
|
|
438
|
-
import { promisify } from "util";
|
|
1784
|
+
import { execFile as execFile2 } from "child_process";
|
|
1785
|
+
import { delimiter, dirname as dirname3 } from "path";
|
|
1786
|
+
import { promisify as promisify2 } from "util";
|
|
439
1787
|
|
|
440
1788
|
// src/scanner/semgrep-runtime.ts
|
|
441
1789
|
import { existsSync, readFileSync } from "fs";
|
|
442
1790
|
import { createRequire } from "module";
|
|
443
|
-
import { dirname, resolve } from "path";
|
|
1791
|
+
import { dirname as dirname2, resolve } from "path";
|
|
444
1792
|
var require2 = createRequire(import.meta.url);
|
|
445
1793
|
var packageForPlatform = (platform, architecture) => {
|
|
446
1794
|
if (platform === "linux" && architecture === "x64") {
|
|
@@ -452,6 +1800,7 @@ var packageForPlatform = (platform, architecture) => {
|
|
|
452
1800
|
return void 0;
|
|
453
1801
|
};
|
|
454
1802
|
var unsupportedRuntimeMessage = (platform, architecture) => `O runtime Semgrep embutido n\xE3o est\xE1 dispon\xEDvel para ${platform}-${architecture}. O CodeSentry oferece suporte a Linux x64 e Windows x64.`;
|
|
1803
|
+
var errorReason = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
455
1804
|
var resolveBundledSemgrepRuntime = (platform = process.platform, architecture = process.arch, resolveManifestPath = (packageName) => require2.resolve(`${packageName}/runtime.json`)) => {
|
|
456
1805
|
const packageName = packageForPlatform(platform, architecture);
|
|
457
1806
|
if (!packageName) {
|
|
@@ -461,15 +1810,14 @@ var resolveBundledSemgrepRuntime = (platform = process.platform, architecture =
|
|
|
461
1810
|
const manifestPath = resolveManifestPath(packageName);
|
|
462
1811
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
463
1812
|
const runtime = {
|
|
464
|
-
semgrep: resolve(
|
|
1813
|
+
semgrep: resolve(dirname2(manifestPath), manifest.semgrep)
|
|
465
1814
|
};
|
|
466
1815
|
if (!existsSync(runtime.semgrep)) {
|
|
467
1816
|
throw new Error("artefatos do runtime ausentes");
|
|
468
1817
|
}
|
|
469
1818
|
return runtime;
|
|
470
1819
|
} catch (error) {
|
|
471
|
-
|
|
472
|
-
throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${reason}`, { cause: error });
|
|
1820
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${errorReason(error)}`, { cause: error });
|
|
473
1821
|
}
|
|
474
1822
|
};
|
|
475
1823
|
|
|
@@ -491,7 +1839,7 @@ var resolveBundledSemgrepRuleset = () => {
|
|
|
491
1839
|
};
|
|
492
1840
|
|
|
493
1841
|
// src/scanner/semgrep.ts
|
|
494
|
-
var
|
|
1842
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
495
1843
|
var SEMGREP_SEVERITIES = {
|
|
496
1844
|
INFO: "low",
|
|
497
1845
|
WARNING: "medium",
|
|
@@ -533,11 +1881,11 @@ var semgrepArgs = (ruleset, includeTests) => [
|
|
|
533
1881
|
"."
|
|
534
1882
|
];
|
|
535
1883
|
var semgrepEnvironment = (runtime) => {
|
|
536
|
-
const semgrepDir =
|
|
1884
|
+
const semgrepDir = dirname3(runtime.semgrep);
|
|
537
1885
|
const systemPathFallback = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
|
|
538
1886
|
return {
|
|
539
1887
|
...process.env,
|
|
540
|
-
PATH: `${semgrepDir}${delimiter}${
|
|
1888
|
+
PATH: `${semgrepDir}${delimiter}${dirname3(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
|
|
541
1889
|
};
|
|
542
1890
|
};
|
|
543
1891
|
var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests) => {
|
|
@@ -556,7 +1904,7 @@ var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests)
|
|
|
556
1904
|
throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
|
|
557
1905
|
}
|
|
558
1906
|
};
|
|
559
|
-
var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute =
|
|
1907
|
+
var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync2, includeTests = false) => {
|
|
560
1908
|
const startedAt = Date.now();
|
|
561
1909
|
try {
|
|
562
1910
|
const report = parseSemgrepReport(await executeSemgrep(targetDir, runtime, ruleset, execute, includeTests));
|
|
@@ -580,7 +1928,7 @@ var generateMarkdownReportFilename = (date = /* @__PURE__ */ new Date()) => {
|
|
|
580
1928
|
};
|
|
581
1929
|
|
|
582
1930
|
// src/commands/scan/scan-runner.ts
|
|
583
|
-
var
|
|
1931
|
+
var errorMessage3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
584
1932
|
var reportScanFailure = (error) => {
|
|
585
1933
|
process.exitCode = 1;
|
|
586
1934
|
console.error(`Falha ao executar o scan: ${formatErrorChain(error)}`);
|
|
@@ -596,26 +1944,59 @@ var writeMarkdownReportIfNeeded = async (result, targetDir) => {
|
|
|
596
1944
|
if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
|
|
597
1945
|
return;
|
|
598
1946
|
}
|
|
599
|
-
const filePath =
|
|
1947
|
+
const filePath = join4(targetDir, generateMarkdownReportFilename());
|
|
600
1948
|
try {
|
|
601
|
-
await
|
|
1949
|
+
await writeFile2(filePath, toMarkdownReport(result), "utf-8");
|
|
602
1950
|
console.log(chalk2.cyan(`
|
|
603
1951
|
Relat\xF3rio detalhado gerado em: ${filePath}`));
|
|
604
1952
|
} catch (error) {
|
|
605
|
-
console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${
|
|
1953
|
+
console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage3(error)}`));
|
|
606
1954
|
}
|
|
607
1955
|
};
|
|
608
|
-
var
|
|
1956
|
+
var runNativeAndSemgrep = async (path, rules, options) => {
|
|
609
1957
|
try {
|
|
610
1958
|
const includeTests = options.tests ?? false;
|
|
611
1959
|
const nativeResult = await runScan(path, rules, options.concurrency, includeTests);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1960
|
+
return options.semgrep ? mergeScanResults(
|
|
1961
|
+
nativeResult,
|
|
1962
|
+
await runBundledSemgrep(path, void 0, options.config, void 0, includeTests)
|
|
1963
|
+
) : nativeResult;
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
throw new Error(`Falha durante a an\xE1lise: ${errorMessage3(error)}`, { cause: error });
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
var runOptionalDependencyAudit = async (path, merged, options) => {
|
|
1969
|
+
try {
|
|
1970
|
+
const auditResult = await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true });
|
|
1971
|
+
return finalizeScanResult(
|
|
1972
|
+
{
|
|
1973
|
+
...merged,
|
|
1974
|
+
findings: [...merged.findings, ...auditResult.findings],
|
|
1975
|
+
durationMs: merged.durationMs + auditResult.durationMs,
|
|
1976
|
+
warnings: [...merged.warnings ?? [], ...auditResult.warnings ?? []],
|
|
1977
|
+
engines: {
|
|
1978
|
+
...merged.engines,
|
|
1979
|
+
osv: auditResult.engines?.osv,
|
|
1980
|
+
nvd: auditResult.engines?.nvd
|
|
1981
|
+
},
|
|
1982
|
+
osvCheckedPackages: auditResult.osvCheckedPackages
|
|
1983
|
+
},
|
|
1984
|
+
auditResult.engines?.dependencyAudit ?? false
|
|
1985
|
+
);
|
|
617
1986
|
} catch (error) {
|
|
618
|
-
|
|
1987
|
+
return finalizeScanResult({
|
|
1988
|
+
...merged,
|
|
1989
|
+
warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`],
|
|
1990
|
+
engines: { ...merged.engines, nvd: false }
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
var runScanEngines = async (path, rules, options) => {
|
|
1995
|
+
try {
|
|
1996
|
+
const merged = await runNativeAndSemgrep(path, rules, options);
|
|
1997
|
+
return options.deps ? runOptionalDependencyAudit(path, merged, options) : finalizeScanResult(merged);
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
throw error;
|
|
619
2000
|
}
|
|
620
2001
|
};
|
|
621
2002
|
var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
|
|
@@ -721,8 +2102,11 @@ var walkNestingBlock = (node, depth, results) => {
|
|
|
721
2102
|
checkDepth(newDepth, node.loc?.start.line, results);
|
|
722
2103
|
walkChildren(node, newDepth, results);
|
|
723
2104
|
};
|
|
2105
|
+
var selectNonFunctionWalker = (node) => {
|
|
2106
|
+
return node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
|
|
2107
|
+
};
|
|
724
2108
|
var selectWalker = (node) => {
|
|
725
|
-
return FUNCTION_TYPES.has(node.type) ? walkFunction :
|
|
2109
|
+
return FUNCTION_TYPES.has(node.type) ? walkFunction : selectNonFunctionWalker(node);
|
|
726
2110
|
};
|
|
727
2111
|
var walk2 = (node, depth, results) => {
|
|
728
2112
|
selectWalker(node)(node, depth, results);
|
|
@@ -756,56 +2140,6 @@ var registerDeepNestingCommand = (program) => {
|
|
|
756
2140
|
);
|
|
757
2141
|
};
|
|
758
2142
|
|
|
759
|
-
// src/scanner/dependency-audit.ts
|
|
760
|
-
import { execFile as execFile2 } from "child_process";
|
|
761
|
-
import { promisify as promisify2 } from "util";
|
|
762
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
763
|
-
var SEVERITY_MAP = {
|
|
764
|
-
info: "low",
|
|
765
|
-
low: "low",
|
|
766
|
-
moderate: "medium",
|
|
767
|
-
high: "high",
|
|
768
|
-
critical: "critical"
|
|
769
|
-
};
|
|
770
|
-
var vulnerabilityTitle = (vulnerability) => {
|
|
771
|
-
const firstVia = vulnerability.via[0];
|
|
772
|
-
if (typeof firstVia === "object" && firstVia?.title) {
|
|
773
|
-
return firstVia.title;
|
|
774
|
-
}
|
|
775
|
-
return 'ver "npm audit" para detalhes';
|
|
776
|
-
};
|
|
777
|
-
var mapAuditReportToFindings = (report) => {
|
|
778
|
-
return Object.values(report.vulnerabilities).map((vulnerability) => ({
|
|
779
|
-
ruleId: "dependency-audit",
|
|
780
|
-
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)}`,
|
|
781
|
-
file: "package.json",
|
|
782
|
-
line: 1,
|
|
783
|
-
severity: SEVERITY_MAP[vulnerability.severity]
|
|
784
|
-
}));
|
|
785
|
-
};
|
|
786
|
-
var runDependencyAudit = async (targetDir) => {
|
|
787
|
-
const startedAt = Date.now();
|
|
788
|
-
let stdout;
|
|
789
|
-
try {
|
|
790
|
-
({ stdout } = await execFileAsync2("npm", ["audit", "--json"], {
|
|
791
|
-
cwd: targetDir,
|
|
792
|
-
maxBuffer: 10 * 1024 * 1024
|
|
793
|
-
}));
|
|
794
|
-
} catch (error) {
|
|
795
|
-
const stdoutFromError = error.stdout;
|
|
796
|
-
if (!stdoutFromError) {
|
|
797
|
-
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
798
|
-
}
|
|
799
|
-
stdout = stdoutFromError;
|
|
800
|
-
}
|
|
801
|
-
const report = JSON.parse(stdout);
|
|
802
|
-
return {
|
|
803
|
-
scannedFiles: 1,
|
|
804
|
-
findings: mapAuditReportToFindings(report),
|
|
805
|
-
durationMs: Date.now() - startedAt
|
|
806
|
-
};
|
|
807
|
-
};
|
|
808
|
-
|
|
809
2143
|
// src/commands/dependency-audit/dependency-audit.command.ts
|
|
810
2144
|
var printAuditResult = (result, json) => {
|
|
811
2145
|
if (json) {
|
|
@@ -816,7 +2150,10 @@ var printAuditResult = (result, json) => {
|
|
|
816
2150
|
};
|
|
817
2151
|
var auditAndReport = async (path, options) => {
|
|
818
2152
|
try {
|
|
819
|
-
printAuditResult(
|
|
2153
|
+
printAuditResult(
|
|
2154
|
+
await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true }),
|
|
2155
|
+
options.json ?? false
|
|
2156
|
+
);
|
|
820
2157
|
} catch (error) {
|
|
821
2158
|
const message = error instanceof Error ? error.message : "erro desconhecido";
|
|
822
2159
|
process.exitCode = 1;
|
|
@@ -824,9 +2161,7 @@ var auditAndReport = async (path, options) => {
|
|
|
824
2161
|
}
|
|
825
2162
|
};
|
|
826
2163
|
var registerDependencyAuditCommand = (program) => {
|
|
827
|
-
program.command("dependency-audit").description(
|
|
828
|
-
'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit"; requer npm no PATH e acesso \xE0 rede)'
|
|
829
|
-
).argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").action((path, options) => auditAndReport(path, options));
|
|
2164
|
+
program.command("dependency-audit").description("Audita depend\xEAncias via npm audit e OSV.dev, com enriquecimento opcional do NVD").argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").option("--no-nvd", "n\xE3o enriquece os resultados OSV com dados do NVD").action((path, options) => auditAndReport(path, options));
|
|
830
2165
|
};
|
|
831
2166
|
|
|
832
2167
|
// src/rules/empty-catch.rule.ts
|
|
@@ -883,12 +2218,24 @@ var hasLimitOption = (options) => {
|
|
|
883
2218
|
return key?.type === "Identifier" && key.name === "limit";
|
|
884
2219
|
}) ?? false;
|
|
885
2220
|
};
|
|
2221
|
+
var getMemberCallParts = (node) => {
|
|
2222
|
+
if (node.type !== "CallExpression") {
|
|
2223
|
+
return { object: void 0, property: void 0, args: void 0 };
|
|
2224
|
+
}
|
|
2225
|
+
const callee = node.callee;
|
|
2226
|
+
const args = node.arguments;
|
|
2227
|
+
if (callee?.type !== "MemberExpression") {
|
|
2228
|
+
return { object: void 0, property: void 0, args };
|
|
2229
|
+
}
|
|
2230
|
+
return {
|
|
2231
|
+
object: callee.object,
|
|
2232
|
+
property: callee.property,
|
|
2233
|
+
args
|
|
2234
|
+
};
|
|
2235
|
+
};
|
|
886
2236
|
var isBodyParserWithoutLimit = (node) => {
|
|
887
|
-
const
|
|
888
|
-
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
889
|
-
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
2237
|
+
const { object, property, args } = getMemberCallParts(node);
|
|
890
2238
|
const isBodyParserCall = object?.type === "Identifier" && BODY_PARSER_OBJECTS.has(object.name) && property?.type === "Identifier" && BODY_PARSER_METHODS.has(property.name);
|
|
891
|
-
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
892
2239
|
return isBodyParserCall && !hasLimitOption(args?.[0]);
|
|
893
2240
|
};
|
|
894
2241
|
var findMissingBodyLimitLines = (filePath, content) => {
|
|
@@ -1133,9 +2480,10 @@ var createFunctionStatementCountRule = (config) => ({
|
|
|
1133
2480
|
// src/rules/high-complexity.rule.ts
|
|
1134
2481
|
var highComplexityRule = createFunctionStatementCountRule({
|
|
1135
2482
|
id: "high-complexity",
|
|
1136
|
-
description: "Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)",
|
|
2483
|
+
description: "Detecta fun\xE7\xF5es com muitos condicionais/loops/tern\xE1rios (complexidade alta)",
|
|
1137
2484
|
statementTypes: /* @__PURE__ */ new Set([
|
|
1138
2485
|
"IfStatement",
|
|
2486
|
+
"ConditionalExpression",
|
|
1139
2487
|
"ForStatement",
|
|
1140
2488
|
"ForInStatement",
|
|
1141
2489
|
"ForOfStatement",
|
|
@@ -1145,7 +2493,7 @@ var highComplexityRule = createFunctionStatementCountRule({
|
|
|
1145
2493
|
"CatchClause"
|
|
1146
2494
|
]),
|
|
1147
2495
|
maxCount: 5,
|
|
1148
|
-
unitLabel: "condicionais/loops"
|
|
2496
|
+
unitLabel: "condicionais/loops/tern\xE1rios"
|
|
1149
2497
|
});
|
|
1150
2498
|
|
|
1151
2499
|
// src/commands/high-complexity/high-complexity.command.ts
|
|
@@ -1216,13 +2564,16 @@ var isMathRandomCall = (node) => {
|
|
|
1216
2564
|
return object?.type === "Identifier" && object.name === "Math" && property?.type === "Identifier" && property.name === "random";
|
|
1217
2565
|
};
|
|
1218
2566
|
var HASH_LIKE_NAME_PATTERN = /hash|md5|sha1/i;
|
|
2567
|
+
var memberPropertyName = (callee) => {
|
|
2568
|
+
const property = callee.property;
|
|
2569
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
2570
|
+
};
|
|
1219
2571
|
var calleeName = (callee) => {
|
|
1220
2572
|
if (callee?.type === "Identifier") {
|
|
1221
2573
|
return callee.name;
|
|
1222
2574
|
}
|
|
1223
2575
|
if (callee?.type === "MemberExpression") {
|
|
1224
|
-
|
|
1225
|
-
return property?.type === "Identifier" ? property.name : void 0;
|
|
2576
|
+
return memberPropertyName(callee);
|
|
1226
2577
|
}
|
|
1227
2578
|
return void 0;
|
|
1228
2579
|
};
|
|
@@ -1408,12 +2759,24 @@ var objectHasProperty = (object, propertyName) => {
|
|
|
1408
2759
|
return key?.type === "Identifier" && key.name === propertyName;
|
|
1409
2760
|
}) ?? false;
|
|
1410
2761
|
};
|
|
1411
|
-
var
|
|
2762
|
+
var getCallExpressionParts = (node) => {
|
|
1412
2763
|
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
2764
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
2765
|
+
return { callee, args };
|
|
2766
|
+
};
|
|
2767
|
+
var getMemberExpressionParts = (callee) => {
|
|
1413
2768
|
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
1414
2769
|
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
2770
|
+
return { object, property };
|
|
2771
|
+
};
|
|
2772
|
+
var getJwtSignCallParts = (node) => {
|
|
2773
|
+
const { callee, args } = getCallExpressionParts(node);
|
|
2774
|
+
const { object, property } = getMemberExpressionParts(callee);
|
|
2775
|
+
return { object, property, args };
|
|
2776
|
+
};
|
|
2777
|
+
var isJwtSignWithoutExpiration = (node) => {
|
|
2778
|
+
const { object, property, args } = getJwtSignCallParts(node);
|
|
1415
2779
|
const isJwtSignCall = object?.type === "Identifier" && object.name === "jwt" && property?.type === "Identifier" && property.name === "sign";
|
|
1416
|
-
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
1417
2780
|
const payload = args?.[0];
|
|
1418
2781
|
const options = args?.[2];
|
|
1419
2782
|
const hasExpInPayload = objectHasProperty(payload, "exp");
|
|
@@ -1686,12 +3049,26 @@ var isPermissiveCorsCall = (node) => {
|
|
|
1686
3049
|
const args = node.arguments;
|
|
1687
3050
|
return (args?.length ?? 0) === 0 || isWildcardOriginOption(args?.[0]);
|
|
1688
3051
|
};
|
|
3052
|
+
var callExpressionParts = (node) => {
|
|
3053
|
+
if (node.type !== "CallExpression") {
|
|
3054
|
+
return void 0;
|
|
3055
|
+
}
|
|
3056
|
+
return {
|
|
3057
|
+
callee: node.callee,
|
|
3058
|
+
args: node.arguments
|
|
3059
|
+
};
|
|
3060
|
+
};
|
|
3061
|
+
var memberCalleeProperty = (callee) => {
|
|
3062
|
+
if (callee?.type !== "MemberExpression") {
|
|
3063
|
+
return void 0;
|
|
3064
|
+
}
|
|
3065
|
+
return callee.property;
|
|
3066
|
+
};
|
|
1689
3067
|
var isWildcardOriginHeader = (node) => {
|
|
1690
|
-
const
|
|
1691
|
-
const property =
|
|
1692
|
-
const
|
|
1693
|
-
const
|
|
1694
|
-
const headerValue = args?.[1];
|
|
3068
|
+
const parts = callExpressionParts(node);
|
|
3069
|
+
const property = memberCalleeProperty(parts?.callee);
|
|
3070
|
+
const headerName = parts?.args?.[0];
|
|
3071
|
+
const headerValue = parts?.args?.[1];
|
|
1695
3072
|
return property?.type === "Identifier" && HEADER_SETTER_METHODS.has(property.name) && headerName?.type === "StringLiteral" && headerName.value === "Access-Control-Allow-Origin" && headerValue?.type === "StringLiteral" && headerValue.value === "*";
|
|
1696
3073
|
};
|
|
1697
3074
|
var findPermissiveCorsLines = (filePath, content) => {
|
|
@@ -1845,13 +3222,26 @@ var awaitNoTryCatchRule = {
|
|
|
1845
3222
|
|
|
1846
3223
|
// src/rules/floating-promise.rule.ts
|
|
1847
3224
|
var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set(["all", "race", "allSettled", "any"]);
|
|
1848
|
-
var
|
|
3225
|
+
var functionDeclarationName = (node) => {
|
|
1849
3226
|
const declarationId = node.type === "FunctionDeclaration" ? node.id : void 0;
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
3227
|
+
return declarationId?.type === "Identifier" ? declarationId.name : void 0;
|
|
3228
|
+
};
|
|
3229
|
+
var isAsyncFunctionValue = (init) => {
|
|
3230
|
+
return (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && init.async;
|
|
3231
|
+
};
|
|
3232
|
+
var asyncVariableDeclaratorId = (node) => {
|
|
3233
|
+
return node.type === "VariableDeclarator" ? node.id : void 0;
|
|
3234
|
+
};
|
|
3235
|
+
var asyncVariableDeclaratorInit = (node) => {
|
|
3236
|
+
return node.type === "VariableDeclarator" ? node.init : void 0;
|
|
3237
|
+
};
|
|
3238
|
+
var asyncVariableDeclaratorName = (node) => {
|
|
3239
|
+
const id = asyncVariableDeclaratorId(node);
|
|
3240
|
+
const init = asyncVariableDeclaratorInit(node);
|
|
3241
|
+
return id?.type === "Identifier" && isAsyncFunctionValue(init) ? id.name : void 0;
|
|
3242
|
+
};
|
|
3243
|
+
var asyncFunctionName = (node) => {
|
|
3244
|
+
return functionDeclarationName(node) ?? asyncVariableDeclaratorName(node);
|
|
1855
3245
|
};
|
|
1856
3246
|
var collectAsyncFunctionNames = (sourceFile) => {
|
|
1857
3247
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -1863,16 +3253,20 @@ var collectAsyncFunctionNames = (sourceFile) => {
|
|
|
1863
3253
|
});
|
|
1864
3254
|
return names;
|
|
1865
3255
|
};
|
|
3256
|
+
var isKnownFunctionCall = (callee, asyncFunctionNames) => {
|
|
3257
|
+
return callee.type === "Identifier" && (callee.name === "fetch" || asyncFunctionNames.has(callee.name));
|
|
3258
|
+
};
|
|
3259
|
+
var isPromiseStaticMethodCall = (callee) => {
|
|
3260
|
+
const object = callee.type === "MemberExpression" ? callee.object : void 0;
|
|
3261
|
+
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
3262
|
+
return object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
|
|
3263
|
+
};
|
|
1866
3264
|
var isKnownPromiseReturningCall = (call, asyncFunctionNames) => {
|
|
1867
3265
|
const callee = call.callee;
|
|
1868
3266
|
if (!callee) {
|
|
1869
3267
|
return false;
|
|
1870
3268
|
}
|
|
1871
|
-
|
|
1872
|
-
const object = callee.type === "MemberExpression" ? callee.object : void 0;
|
|
1873
|
-
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
1874
|
-
const isPromiseStaticMethod = object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
|
|
1875
|
-
return isKnownFunction || isPromiseStaticMethod;
|
|
3269
|
+
return isKnownFunctionCall(callee, asyncFunctionNames) || isPromiseStaticMethodCall(callee);
|
|
1876
3270
|
};
|
|
1877
3271
|
var findFloatingPromiseLines = (filePath, content) => {
|
|
1878
3272
|
const lines = /* @__PURE__ */ new Set();
|
|
@@ -1902,7 +3296,7 @@ var floatingPromiseRule = {
|
|
|
1902
3296
|
};
|
|
1903
3297
|
|
|
1904
3298
|
// src/rules/promise-no-catch.rule.ts
|
|
1905
|
-
var
|
|
3299
|
+
var memberPropertyName2 = (member) => {
|
|
1906
3300
|
const property = member.property;
|
|
1907
3301
|
return property?.type === "Identifier" ? property.name : void 0;
|
|
1908
3302
|
};
|
|
@@ -1915,7 +3309,11 @@ var isThenCall = (node) => {
|
|
|
1915
3309
|
}
|
|
1916
3310
|
const callee = node.callee;
|
|
1917
3311
|
const args = node.arguments;
|
|
1918
|
-
return callee?.type === "MemberExpression" &&
|
|
3312
|
+
return callee?.type === "MemberExpression" && memberPropertyName2(callee) === "then" && (args?.length ?? 0) < 2;
|
|
3313
|
+
};
|
|
3314
|
+
var chainMethodName = (currentCall, member, nextCall) => {
|
|
3315
|
+
const isChainMember = member !== void 0 && member.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
|
|
3316
|
+
return isChainMember ? memberPropertyName2(member) : void 0;
|
|
1919
3317
|
};
|
|
1920
3318
|
var chainReachesCatch = (thenCall, ancestors) => {
|
|
1921
3319
|
let currentCall = thenCall;
|
|
@@ -1923,8 +3321,7 @@ var chainReachesCatch = (thenCall, ancestors) => {
|
|
|
1923
3321
|
while (i < ancestors.length) {
|
|
1924
3322
|
const member = ancestors.at(i);
|
|
1925
3323
|
const nextCall = ancestors.at(i + 1);
|
|
1926
|
-
const
|
|
1927
|
-
const methodName = isChainMember ? memberPropertyName(member) : void 0;
|
|
3324
|
+
const methodName = chainMethodName(currentCall, member, nextCall);
|
|
1928
3325
|
const continuesChain = methodName === "then" || methodName === "finally";
|
|
1929
3326
|
if (methodName === "catch") {
|
|
1930
3327
|
return true;
|
|
@@ -2166,10 +3563,10 @@ var tooManyForLoopsRule = createFunctionStatementCountRule({
|
|
|
2166
3563
|
// src/rules/too-many-ifs.rule.ts
|
|
2167
3564
|
var tooManyIfsRule = createFunctionStatementCountRule({
|
|
2168
3565
|
id: "too-many-ifs",
|
|
2169
|
-
description: 'Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")',
|
|
2170
|
-
statementTypes: /* @__PURE__ */ new Set(["IfStatement"]),
|
|
3566
|
+
description: 'Detecta fun\xE7\xF5es com muitos "if" ou tern\xE1rios (incluindo "else if")',
|
|
3567
|
+
statementTypes: /* @__PURE__ */ new Set(["IfStatement", "ConditionalExpression"]),
|
|
2171
3568
|
maxCount: 2,
|
|
2172
|
-
unitLabel: "declara\xE7\xF5es if (incluindo else if)"
|
|
3569
|
+
unitLabel: "declara\xE7\xF5es if e express\xF5es tern\xE1rias (incluindo else if)"
|
|
2173
3570
|
});
|
|
2174
3571
|
|
|
2175
3572
|
// src/rules/too-many-switch-cases.rule.ts
|
|
@@ -2308,12 +3705,25 @@ var weakCipherModeRule = {
|
|
|
2308
3705
|
|
|
2309
3706
|
// src/rules/weak-hash-algorithm.rule.ts
|
|
2310
3707
|
var WEAK_ALGORITHMS = /^(md5|sha1)$/i;
|
|
3708
|
+
var getCallExpressionCallee = (node) => {
|
|
3709
|
+
return node.type === "CallExpression" ? node.callee : void 0;
|
|
3710
|
+
};
|
|
3711
|
+
var getCallExpressionArgs = (node) => {
|
|
3712
|
+
return node.type === "CallExpression" ? node.arguments : void 0;
|
|
3713
|
+
};
|
|
3714
|
+
var getCreateHashProperty = (callee) => {
|
|
3715
|
+
return callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
3716
|
+
};
|
|
3717
|
+
var isCreateHashProperty = (property) => {
|
|
3718
|
+
return property?.type === "Identifier" && property.name === "createHash";
|
|
3719
|
+
};
|
|
3720
|
+
var isWeakAlgorithmLiteral = (algorithm) => {
|
|
3721
|
+
return algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
|
|
3722
|
+
};
|
|
2311
3723
|
var isWeakCreateHashCall = (node) => {
|
|
2312
|
-
const
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
const algorithm = args?.[0];
|
|
2316
|
-
return property?.type === "Identifier" && property.name === "createHash" && algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
|
|
3724
|
+
const property = getCreateHashProperty(getCallExpressionCallee(node));
|
|
3725
|
+
const algorithm = getCallExpressionArgs(node)?.[0];
|
|
3726
|
+
return isCreateHashProperty(property) && isWeakAlgorithmLiteral(algorithm);
|
|
2317
3727
|
};
|
|
2318
3728
|
var findWeakHashLines = (filePath, content) => {
|
|
2319
3729
|
const lines = /* @__PURE__ */ new Set();
|
|
@@ -2548,8 +3958,11 @@ var parseLocalSemgrepConfig = (value) => {
|
|
|
2548
3958
|
};
|
|
2549
3959
|
var registerScanCommand = (program) => {
|
|
2550
3960
|
withScanOptions(
|
|
2551
|
-
program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig)
|
|
2552
|
-
|
|
3961
|
+
program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig).option(
|
|
3962
|
+
"--no-deps",
|
|
3963
|
+
"n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev + NVD) neste scan \u2014 permite rodar offline"
|
|
3964
|
+
)
|
|
3965
|
+
).option("--no-nvd", "n\xE3o enriquece os resultados OSV com dados do NVD").action(
|
|
2553
3966
|
(path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
|
|
2554
3967
|
);
|
|
2555
3968
|
};
|
|
@@ -2649,6 +4062,118 @@ var registerUnsafeSqlCommand = (program) => {
|
|
|
2649
4062
|
);
|
|
2650
4063
|
};
|
|
2651
4064
|
|
|
4065
|
+
// src/engines/engine-metadata.ts
|
|
4066
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4067
|
+
import { createRequire as createRequire4 } from "module";
|
|
4068
|
+
var require5 = createRequire4(import.meta.url);
|
|
4069
|
+
var packageForPlatform2 = (platform, architecture) => {
|
|
4070
|
+
if (platform === "linux" && architecture === "x64") {
|
|
4071
|
+
return "codesentry-semgrep-linux-x64";
|
|
4072
|
+
}
|
|
4073
|
+
if (platform === "win32" && architecture === "x64") {
|
|
4074
|
+
return "codesentry-semgrep-win32-x64";
|
|
4075
|
+
}
|
|
4076
|
+
return void 0;
|
|
4077
|
+
};
|
|
4078
|
+
var unsupportedRuntimeMessage2 = (platform, architecture) => `O runtime Semgrep embutido n\xE3o est\xE1 dispon\xEDvel para ${platform}-${architecture}. O CodeSentry oferece suporte a Linux x64 e Windows x64.`;
|
|
4079
|
+
var readJsonFile = (path) => {
|
|
4080
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
4081
|
+
};
|
|
4082
|
+
var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
4083
|
+
var errorReason2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
4084
|
+
var validRuntimeLock = (lock) => lock.schemaVersion === 1 && nonEmptyString(lock.semgrepVersion) && nonEmptyString(lock.pythonVersion) && /^[a-f0-9]{64}$/i.test(lock.runtimeSha256);
|
|
4085
|
+
var validRulesetLock = (lock) => lock.schemaVersion === 1 && nonEmptyString(lock.packageVersion) && nonEmptyString(lock.ruleset) && nonEmptyString(lock.source) && /^[a-f0-9]{64}$/i.test(lock.sha256) && nonEmptyString(lock.capturedAt) && !Number.isNaN(Date.parse(lock.capturedAt)) && (lock.upstreamRevision === void 0 || nonEmptyString(lock.upstreamRevision));
|
|
4086
|
+
var loadBundledSemgrepRuntimeMetadata = (platform = process.platform, architecture = process.arch, resolveLockPath = (packageName) => require5.resolve(`${packageName}/runtime.lock.json`)) => {
|
|
4087
|
+
const packageName = packageForPlatform2(platform, architecture);
|
|
4088
|
+
if (!packageName) {
|
|
4089
|
+
throw new Error(unsupportedRuntimeMessage2(platform, architecture));
|
|
4090
|
+
}
|
|
4091
|
+
try {
|
|
4092
|
+
const lock = readJsonFile(resolveLockPath(packageName));
|
|
4093
|
+
if (!validRuntimeLock(lock)) {
|
|
4094
|
+
throw new Error("manifesto de vers\xE3o inv\xE1lido");
|
|
4095
|
+
}
|
|
4096
|
+
return {
|
|
4097
|
+
packageName,
|
|
4098
|
+
semgrepVersion: lock.semgrepVersion,
|
|
4099
|
+
pythonVersion: lock.pythonVersion,
|
|
4100
|
+
runtimeSha256: lock.runtimeSha256
|
|
4101
|
+
};
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do runtime Semgrep embutido: ${errorReason2(error)}`, {
|
|
4104
|
+
cause: error
|
|
4105
|
+
});
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
var loadBundledOwaspRulesetMetadata = (resolveLockPath = () => require5.resolve("codesentry-semgrep-rules/rules/ruleset.lock.json")) => {
|
|
4109
|
+
try {
|
|
4110
|
+
const lock = readJsonFile(resolveLockPath());
|
|
4111
|
+
if (!validRulesetLock(lock)) {
|
|
4112
|
+
throw new Error("manifesto de vers\xE3o inv\xE1lido");
|
|
4113
|
+
}
|
|
4114
|
+
return {
|
|
4115
|
+
packageVersion: lock.packageVersion,
|
|
4116
|
+
ruleset: lock.ruleset,
|
|
4117
|
+
source: lock.source,
|
|
4118
|
+
sha256: lock.sha256,
|
|
4119
|
+
capturedAt: lock.capturedAt,
|
|
4120
|
+
...lock.upstreamRevision ? { upstreamRevision: lock.upstreamRevision } : {}
|
|
4121
|
+
};
|
|
4122
|
+
} catch (error) {
|
|
4123
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do ruleset OWASP embutido: ${errorReason2(error)}`, {
|
|
4124
|
+
cause: error
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
var loadEngineMetadata = () => ({
|
|
4129
|
+
semgrep: loadBundledSemgrepRuntimeMetadata(),
|
|
4130
|
+
owaspRuleset: loadBundledOwaspRulesetMetadata()
|
|
4131
|
+
});
|
|
4132
|
+
|
|
4133
|
+
// src/commands/version/version.command.ts
|
|
4134
|
+
var buildVersionReport = (codesentryVersion, engines) => ({
|
|
4135
|
+
codesentryVersion,
|
|
4136
|
+
engines
|
|
4137
|
+
});
|
|
4138
|
+
var formatEngineVersions = (report) => {
|
|
4139
|
+
const { semgrep, owaspRuleset } = report.engines;
|
|
4140
|
+
return [
|
|
4141
|
+
`CodeSentry: ${report.codesentryVersion}`,
|
|
4142
|
+
"",
|
|
4143
|
+
"Runtime Semgrep CE",
|
|
4144
|
+
` Vers\xE3o: ${semgrep.semgrepVersion}`,
|
|
4145
|
+
` Python: ${semgrep.pythonVersion}`,
|
|
4146
|
+
` Pacote: ${semgrep.packageName}`,
|
|
4147
|
+
` SHA-256: ${semgrep.runtimeSha256}`,
|
|
4148
|
+
"",
|
|
4149
|
+
"Ruleset OWASP",
|
|
4150
|
+
` Snapshot: ${owaspRuleset.ruleset}`,
|
|
4151
|
+
` Vers\xE3o do pacote: ${owaspRuleset.packageVersion}`,
|
|
4152
|
+
` Origem: ${owaspRuleset.source}`,
|
|
4153
|
+
` Capturado em: ${owaspRuleset.capturedAt}`,
|
|
4154
|
+
...owaspRuleset.upstreamRevision ? [` Revis\xE3o upstream: ${owaspRuleset.upstreamRevision}`] : [],
|
|
4155
|
+
` SHA-256: ${owaspRuleset.sha256}`
|
|
4156
|
+
].join("\n");
|
|
4157
|
+
};
|
|
4158
|
+
var errorReason3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
4159
|
+
var formatVersionOutput = (options, report) => options.json ? JSON.stringify(report, null, 2) : formatEngineVersions(report);
|
|
4160
|
+
var registerVersionCommand = (program, readPackageVersion2, readEngineMetadata = loadEngineMetadata) => {
|
|
4161
|
+
program.command("version").description("Exibe a vers\xE3o do CodeSentry e dos motores embutidos").option("--engines", "inclui vers\xF5es e proveni\xEAncia do Semgrep e ruleset OWASP").option("--json", "emite o relat\xF3rio de vers\xF5es em JSON").action((options) => {
|
|
4162
|
+
const codesentryVersion = readPackageVersion2();
|
|
4163
|
+
if (!options.engines && !options.json) {
|
|
4164
|
+
console.log(`CodeSentry: ${codesentryVersion}`);
|
|
4165
|
+
return;
|
|
4166
|
+
}
|
|
4167
|
+
try {
|
|
4168
|
+
const report = buildVersionReport(codesentryVersion, readEngineMetadata());
|
|
4169
|
+
console.log(formatVersionOutput(options, report));
|
|
4170
|
+
} catch (error) {
|
|
4171
|
+
process.exitCode = 1;
|
|
4172
|
+
console.error(`N\xE3o foi poss\xEDvel auditar as vers\xF5es dos motores: ${errorReason3(error)}`);
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
};
|
|
4176
|
+
|
|
2652
4177
|
// src/commands/weak-cipher-mode/weak-cipher-mode.command.ts
|
|
2653
4178
|
var registerWeakCipherModeCommand = (program) => {
|
|
2654
4179
|
withScanOptions(
|
|
@@ -2744,16 +4269,17 @@ var registerSecurityCommands = (program) => {
|
|
|
2744
4269
|
registerWeakCipherModeCommand(program);
|
|
2745
4270
|
registerHardcodedAuthorizationValueCommand(program);
|
|
2746
4271
|
};
|
|
2747
|
-
var
|
|
4272
|
+
var require6 = createRequire5(import.meta.url);
|
|
2748
4273
|
var readPackageVersion = () => {
|
|
2749
|
-
const packageJsonPath =
|
|
2750
|
-
const { version } = JSON.parse(
|
|
4274
|
+
const packageJsonPath = require6.resolve("../package.json");
|
|
4275
|
+
const { version } = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
2751
4276
|
return version;
|
|
2752
4277
|
};
|
|
2753
4278
|
var createCli = () => {
|
|
2754
4279
|
printBanner();
|
|
2755
4280
|
const program = new Command().name("codesentry").description("CLI de verifica\xE7\xE3o de vulnerabilidades e qualidade de c\xF3digo").version(readPackageVersion()).helpCommand(false);
|
|
2756
4281
|
registerInitCommand(program);
|
|
4282
|
+
registerVersionCommand(program, readPackageVersion);
|
|
2757
4283
|
registerAnalysisCommands(program);
|
|
2758
4284
|
registerQualityCommands(program);
|
|
2759
4285
|
registerSecurityCommands(program);
|