codesentry 0.2.1 → 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 +57 -16
- package/dist/index.js +1135 -127
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -150,8 +150,8 @@ var commandInjectionRule = {
|
|
|
150
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)");
|
|
151
151
|
|
|
152
152
|
// src/commands/scan/scan-runner.ts
|
|
153
|
-
import { writeFile } from "fs/promises";
|
|
154
|
-
import { join as
|
|
153
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
154
|
+
import { join as join4 } from "path";
|
|
155
155
|
import chalk2 from "chalk";
|
|
156
156
|
import { Listr } from "listr2";
|
|
157
157
|
|
|
@@ -186,9 +186,12 @@ var mergeScanResults = (nativeResult, semgrepResult) => ({
|
|
|
186
186
|
findings: [...nativeResult.findings, ...semgrepResult.findings],
|
|
187
187
|
durationMs: nativeResult.durationMs + semgrepResult.durationMs,
|
|
188
188
|
engines: {
|
|
189
|
+
...nativeResult.engines,
|
|
190
|
+
...semgrepResult.engines,
|
|
189
191
|
codesentry: nativeResult.engines?.codesentry ?? nativeResult.scannedFiles,
|
|
190
192
|
semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
|
|
191
|
-
}
|
|
193
|
+
},
|
|
194
|
+
warnings: [...nativeResult.warnings ?? [], ...semgrepResult.warnings ?? []]
|
|
192
195
|
});
|
|
193
196
|
var finalizeScanResult = (result, dependencyAuditCoverage2 = false) => ({
|
|
194
197
|
...result,
|
|
@@ -204,9 +207,14 @@ var SEVERITY_COLOR = {
|
|
|
204
207
|
critical: (text2) => chalk.bgRed.white(text2)
|
|
205
208
|
};
|
|
206
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)`;
|
|
207
|
-
var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s)
|
|
210
|
+
var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) considerado(s)` : void 0;
|
|
208
211
|
var osvCoverage = (result) => result.engines?.osv ? `OSV.dev: ${result.engines.osv.checked}/${result.engines.osv.total} verificados` : void 0;
|
|
209
|
-
var
|
|
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(
|
|
210
218
|
(part) => part !== void 0
|
|
211
219
|
);
|
|
212
220
|
var coverageText = (result) => {
|
|
@@ -225,9 +233,9 @@ var printCleanReport = (result, coverage) => {
|
|
|
225
233
|
console.log(chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`));
|
|
226
234
|
printNotes(result);
|
|
227
235
|
};
|
|
228
|
-
var findingsTable = (
|
|
236
|
+
var findingsTable = (findings) => {
|
|
229
237
|
const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
|
|
230
|
-
for (const finding of
|
|
238
|
+
for (const finding of findings) {
|
|
231
239
|
const colorize = SEVERITY_COLOR[finding.severity];
|
|
232
240
|
table.push([
|
|
233
241
|
colorize(finding.severity),
|
|
@@ -239,8 +247,132 @@ var findingsTable = (result) => {
|
|
|
239
247
|
}
|
|
240
248
|
return table;
|
|
241
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
|
+
};
|
|
242
371
|
var printFindingsReport = (result, coverage) => {
|
|
243
|
-
|
|
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);
|
|
244
376
|
console.log(
|
|
245
377
|
chalk.bold(
|
|
246
378
|
`
|
|
@@ -264,6 +396,7 @@ var toJsonReport = (result) => {
|
|
|
264
396
|
};
|
|
265
397
|
|
|
266
398
|
// src/reporters/markdown.reporter.ts
|
|
399
|
+
var LOGO_URL = "https://raw.githubusercontent.com/Ivan-ReisDev/code-sentry/main/docs/assets/logo.png";
|
|
267
400
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
|
|
268
401
|
var SEVERITY_LABEL = /* @__PURE__ */ new Map([
|
|
269
402
|
["critical", "Critical"],
|
|
@@ -271,7 +404,14 @@ var SEVERITY_LABEL = /* @__PURE__ */ new Map([
|
|
|
271
404
|
["medium", "Medium"],
|
|
272
405
|
["low", "Low"]
|
|
273
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
|
+
]);
|
|
274
413
|
var severityLabel = (severity) => SEVERITY_LABEL.get(severity) ?? severity;
|
|
414
|
+
var severityBadge = (severity) => `${SEVERITY_EMOJI.get(severity) ?? ""} ${severityLabel(severity)}`;
|
|
275
415
|
var escapeCell = (text2) => text2.replaceAll("|", "\\|");
|
|
276
416
|
var groupBy = (items, keyOf) => {
|
|
277
417
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -290,7 +430,7 @@ var findingsTable2 = (findings) => {
|
|
|
290
430
|
return lines;
|
|
291
431
|
};
|
|
292
432
|
var severitySection = (severity, findings) => {
|
|
293
|
-
const lines = [`## ${
|
|
433
|
+
const lines = [`## ${severityBadge(severity)} (${findings.length})`, ""];
|
|
294
434
|
const byRule = groupBy(findings, (f) => f.ruleId);
|
|
295
435
|
for (const ruleId of [...byRule.keys()].sort()) {
|
|
296
436
|
const ruleFindings = byRule.get(ruleId) ?? [];
|
|
@@ -298,24 +438,38 @@ var severitySection = (severity, findings) => {
|
|
|
298
438
|
}
|
|
299
439
|
return lines;
|
|
300
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}`] : [];
|
|
301
453
|
var reportHeader = (result, generatedAt) => [
|
|
454
|
+
`<p align="center"><img src="${LOGO_URL}" alt="CodeSentry" width="320"></p>`,
|
|
455
|
+
"",
|
|
302
456
|
"# Relat\xF3rio CodeSentry",
|
|
303
457
|
"",
|
|
304
458
|
`- **Gerado em:** ${generatedAt.toISOString()}`,
|
|
305
459
|
`- **Arquivos analisados:** ${result.scannedFiles}`,
|
|
306
|
-
...result
|
|
307
|
-
|
|
308
|
-
|
|
460
|
+
...semgrepCoverageLine(result),
|
|
461
|
+
...dependencyAuditConsideredLine(result),
|
|
462
|
+
...nvdCoverageLine(result),
|
|
309
463
|
`- **Dura\xE7\xE3o:** ${result.durationMs}ms`,
|
|
310
464
|
`- **Total de problemas:** ${result.findings.length}`,
|
|
311
|
-
...result
|
|
465
|
+
...dependencyAuditNoteLine(result),
|
|
312
466
|
""
|
|
313
467
|
];
|
|
314
468
|
var warningsSection = (result) => (result.warnings ?? []).length === 0 ? [] : ["## Avisos", "", ...(result.warnings ?? []).map((warning) => `- ${warning}`), ""];
|
|
315
469
|
var summaryTable = (bySeverity) => {
|
|
316
470
|
const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
|
|
317
471
|
for (const severity of SEVERITY_ORDER) {
|
|
318
|
-
lines.push(`| ${
|
|
472
|
+
lines.push(`| ${severityBadge(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
|
|
319
473
|
}
|
|
320
474
|
lines.push("");
|
|
321
475
|
return lines;
|
|
@@ -330,6 +484,147 @@ var severitySections = (bySeverity) => {
|
|
|
330
484
|
}
|
|
331
485
|
return lines;
|
|
332
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
|
+
};
|
|
333
628
|
var osvCheckedSection = (result) => {
|
|
334
629
|
const { osvCheckedPackages } = result;
|
|
335
630
|
if (!osvCheckedPackages || osvCheckedPackages.length === 0) {
|
|
@@ -340,27 +635,618 @@ var osvCheckedSection = (result) => {
|
|
|
340
635
|
return [
|
|
341
636
|
`## Depend\xEAncias verificadas no OSV.dev (${checked}/${total})`,
|
|
342
637
|
"",
|
|
638
|
+
"<details>",
|
|
639
|
+
"<summary>Ver lista completa</summary>",
|
|
640
|
+
"",
|
|
343
641
|
...osvCheckedPackages.map((pkg) => `- ${pkg}`),
|
|
642
|
+
"",
|
|
643
|
+
"</details>",
|
|
344
644
|
""
|
|
345
645
|
];
|
|
346
646
|
};
|
|
347
647
|
var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
|
|
648
|
+
const codeFindings = result.findings.filter((finding) => !finding.dependency);
|
|
348
649
|
const bySeverity = groupBy(result.findings, (f) => f.severity);
|
|
650
|
+
const codeBySeverity = groupBy(codeFindings, (f) => f.severity);
|
|
349
651
|
return [
|
|
350
652
|
...reportHeader(result, generatedAt),
|
|
351
653
|
...warningsSection(result),
|
|
352
654
|
...summaryTable(bySeverity),
|
|
353
|
-
...severitySections(
|
|
655
|
+
...severitySections(codeBySeverity),
|
|
656
|
+
...dependencySections(result.findings),
|
|
354
657
|
...osvCheckedSection(result)
|
|
355
658
|
].join("\n");
|
|
356
659
|
};
|
|
357
660
|
|
|
358
661
|
// src/scanner/dependency-audit.ts
|
|
359
662
|
import { execFile } from "child_process";
|
|
360
|
-
import { readFile } from "fs/promises";
|
|
361
|
-
import { join } from "path";
|
|
663
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
664
|
+
import { join as join2 } from "path";
|
|
362
665
|
import { promisify } from "util";
|
|
363
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
|
+
|
|
364
1250
|
// src/scanner/run-with-concurrency-limit.ts
|
|
365
1251
|
import pLimit from "p-limit";
|
|
366
1252
|
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
@@ -521,77 +1407,131 @@ var SEVERITY_MAP2 = {
|
|
|
521
1407
|
high: "high",
|
|
522
1408
|
critical: "critical"
|
|
523
1409
|
};
|
|
524
|
-
var
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return 'ver "npm audit" para detalhes';
|
|
1410
|
+
var NVD_SEVERITY_MAP = {
|
|
1411
|
+
LOW: "low",
|
|
1412
|
+
MEDIUM: "medium",
|
|
1413
|
+
HIGH: "high",
|
|
1414
|
+
CRITICAL: "critical"
|
|
530
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;
|
|
531
1418
|
var npmFixSuggestion = (vulnerability) => {
|
|
532
1419
|
const { fixAvailable } = vulnerability;
|
|
533
|
-
if (fixAvailable === false)
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
if (fixAvailable === true) {
|
|
1420
|
+
if (fixAvailable === false) return "sem corre\xE7\xE3o dispon\xEDvel ainda";
|
|
1421
|
+
if (fixAvailable === true)
|
|
537
1422
|
return `atualize para uma vers\xE3o fora do intervalo vulner\xE1vel (${vulnerability.range})`;
|
|
538
|
-
}
|
|
539
1423
|
return `atualize para ${fixAvailable.name}@${fixAvailable.version}`;
|
|
540
1424
|
};
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
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';
|
|
549
1434
|
};
|
|
550
|
-
var
|
|
551
|
-
var
|
|
552
|
-
const
|
|
1435
|
+
var npmFixedVersions = (fixAvailable) => typeof fixAvailable === "object" ? [fixAvailable.version] : [];
|
|
1436
|
+
var npmFinding = (vulnerability, advisory, ids) => {
|
|
1437
|
+
const title = npmAdvisoryTitle(advisory);
|
|
553
1438
|
return {
|
|
554
1439
|
ruleId: "dependency-audit",
|
|
555
|
-
message: `
|
|
556
|
-
file: "package
|
|
1440
|
+
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${title} \u2014 ${npmFixSuggestion(vulnerability)}`,
|
|
1441
|
+
file: "package.json",
|
|
557
1442
|
line: 1,
|
|
558
|
-
severity
|
|
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
|
+
}
|
|
559
1453
|
};
|
|
560
1454
|
};
|
|
561
|
-
var
|
|
562
|
-
|
|
563
|
-
|
|
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"}`;
|
|
564
1461
|
};
|
|
565
|
-
var
|
|
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
|
+
);
|
|
566
1524
|
var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
567
1525
|
var hasVulnerabilitiesRecord = (value) => typeof value === "object" && value !== null && typeof value.vulnerabilities === "object" && value.vulnerabilities !== null;
|
|
568
1526
|
var normalizeNpmAuditReport = (raw) => {
|
|
569
|
-
if (hasVulnerabilitiesRecord(raw)) {
|
|
570
|
-
return { report: { vulnerabilities: raw.vulnerabilities } };
|
|
571
|
-
}
|
|
1527
|
+
if (hasVulnerabilitiesRecord(raw)) return { report: { vulnerabilities: raw.vulnerabilities } };
|
|
572
1528
|
const errorSummary = typeof raw === "object" && raw !== null && "error" in raw ? raw.error?.summary ?? "formato de resposta inesperado" : "formato de resposta inesperado";
|
|
573
1529
|
return {
|
|
574
1530
|
report: { vulnerabilities: {} },
|
|
575
1531
|
warning: `"npm audit" n\xE3o retornou um relat\xF3rio v\xE1lido: ${errorSummary}.`
|
|
576
1532
|
};
|
|
577
1533
|
};
|
|
578
|
-
var
|
|
579
|
-
const present = warnings.filter((warning) => Boolean(warning));
|
|
580
|
-
return present.length ? present.join(" ") : void 0;
|
|
581
|
-
};
|
|
582
|
-
var fetchDetailsForIds = (ids, fetchImpl) => ids.length ? fetchOsvVulnerabilityDetails(ids, fetchImpl) : Promise.resolve({ detailsById: /* @__PURE__ */ new Map(), warning: void 0 });
|
|
583
|
-
var auditPackagesWithOsv = async (lockedPackages, npmFlaggedNames, fetchImpl = fetch) => {
|
|
584
|
-
try {
|
|
585
|
-
return await collectOsvAuditResult(lockedPackages, npmFlaggedNames, fetchImpl);
|
|
586
|
-
} catch (error) {
|
|
587
|
-
return {
|
|
588
|
-
findings: [],
|
|
589
|
-
checkedPackages: [],
|
|
590
|
-
warning: `N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) => {
|
|
1534
|
+
var collectOsvMatches = async (lockedPackages, fetchImpl) => {
|
|
595
1535
|
try {
|
|
596
1536
|
const {
|
|
597
1537
|
vulnIdsByPackage,
|
|
@@ -599,19 +1539,20 @@ var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) =
|
|
|
599
1539
|
warning: batchWarning
|
|
600
1540
|
} = await queryOsvBatch(lockedPackages, fetchImpl);
|
|
601
1541
|
const ids = [...new Set([...vulnIdsByPackage.values()].flat())];
|
|
602
|
-
const
|
|
1542
|
+
const detailResult = ids.length ? await fetchOsvVulnerabilityDetails(ids, fetchImpl) : { detailsById: /* @__PURE__ */ new Map(), warning: void 0 };
|
|
603
1543
|
return {
|
|
604
|
-
|
|
605
|
-
lockedPackages,
|
|
606
|
-
vulnIdsByPackage,
|
|
607
|
-
detailsById,
|
|
608
|
-
npmFlaggedNames
|
|
609
|
-
),
|
|
1544
|
+
matches: collectMatches(lockedPackages, vulnIdsByPackage, detailResult.detailsById),
|
|
610
1545
|
checkedPackages,
|
|
611
|
-
|
|
1546
|
+
warnings: [batchWarning, detailResult.warning].filter(
|
|
1547
|
+
(warning) => Boolean(warning)
|
|
1548
|
+
)
|
|
612
1549
|
};
|
|
613
1550
|
} catch (error) {
|
|
614
|
-
|
|
1551
|
+
return {
|
|
1552
|
+
matches: [],
|
|
1553
|
+
checkedPackages: [],
|
|
1554
|
+
warnings: [`N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`]
|
|
1555
|
+
};
|
|
615
1556
|
}
|
|
616
1557
|
};
|
|
617
1558
|
var runNpmAudit = async (targetDir) => {
|
|
@@ -623,64 +1564,124 @@ var runNpmAudit = async (targetDir) => {
|
|
|
623
1564
|
}));
|
|
624
1565
|
} catch (error) {
|
|
625
1566
|
const stdoutFromError = error.stdout;
|
|
626
|
-
if (!stdoutFromError)
|
|
1567
|
+
if (!stdoutFromError)
|
|
627
1568
|
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
628
|
-
}
|
|
629
1569
|
stdout = stdoutFromError;
|
|
630
1570
|
}
|
|
631
1571
|
return JSON.parse(stdout);
|
|
632
1572
|
};
|
|
633
|
-
var
|
|
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
|
+
}
|
|
634
1602
|
try {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
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;
|
|
638
1607
|
return {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
]
|
|
644
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);
|
|
645
1632
|
} catch (error) {
|
|
646
1633
|
return {
|
|
647
1634
|
findings: [],
|
|
1635
|
+
matches: [],
|
|
648
1636
|
packagesAudited: false,
|
|
649
|
-
|
|
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)}.`]
|
|
650
1639
|
};
|
|
651
1640
|
}
|
|
652
1641
|
};
|
|
653
|
-
var
|
|
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) => {
|
|
654
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
|
+
};
|
|
655
1667
|
try {
|
|
656
|
-
const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const osv = await auditPackagesFromLockfile(targetDir,
|
|
660
|
-
|
|
661
|
-
return {
|
|
662
|
-
scannedFiles: 1,
|
|
663
|
-
findings: [...npmFindings, ...osv.findings],
|
|
664
|
-
durationMs: Date.now() - startedAt,
|
|
665
|
-
engines: {
|
|
666
|
-
dependencyAudit: osv.packagesAudited === false ? npmFlaggedNames.size : osv.packagesAudited,
|
|
667
|
-
osv: osv.osvChecked
|
|
668
|
-
},
|
|
669
|
-
osvCheckedPackages: osv.osvCheckedPackages,
|
|
670
|
-
warnings: warnings ? [warnings] : void 0
|
|
671
|
-
};
|
|
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);
|
|
672
1673
|
} catch (error) {
|
|
673
1674
|
throw new Error(`N\xE3o foi poss\xEDvel auditar depend\xEAncias em "${targetDir}".`, { cause: error });
|
|
674
1675
|
}
|
|
675
1676
|
};
|
|
676
1677
|
|
|
677
1678
|
// src/scanner/scanner.ts
|
|
678
|
-
import { readFile as
|
|
1679
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
679
1680
|
import { availableParallelism } from "os";
|
|
680
1681
|
|
|
681
1682
|
// src/scanner/file-finder.ts
|
|
682
1683
|
import { readdir } from "fs/promises";
|
|
683
|
-
import { join as
|
|
1684
|
+
import { join as join3 } from "path";
|
|
684
1685
|
|
|
685
1686
|
// src/scanner/ignore-patterns.ts
|
|
686
1687
|
var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next", ".angular"];
|
|
@@ -694,10 +1695,10 @@ var isTestFileName = (fileName) => TEST_FILE_NAME_PATTERN.test(fileName);
|
|
|
694
1695
|
var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
|
|
695
1696
|
var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
696
1697
|
var filesFromDirectory = async (currentDir, entry, includeTests) => {
|
|
697
|
-
return isIgnoredDirName(entry.name, includeTests) ? [] : walk(
|
|
1698
|
+
return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join3(currentDir, entry.name), includeTests);
|
|
698
1699
|
};
|
|
699
1700
|
var filesFromFile = (currentDir, entry, includeTests) => {
|
|
700
|
-
return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [
|
|
1701
|
+
return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join3(currentDir, entry.name)] : [];
|
|
701
1702
|
};
|
|
702
1703
|
var filesFromEntry = async (currentDir, entry, includeTests) => {
|
|
703
1704
|
if (entry.isDirectory()) {
|
|
@@ -733,7 +1734,7 @@ var parseErrorFinding = (filePath, error) => ({
|
|
|
733
1734
|
});
|
|
734
1735
|
var readFileContent = async (filePath) => {
|
|
735
1736
|
try {
|
|
736
|
-
return await
|
|
1737
|
+
return await readFile3(filePath, "utf-8");
|
|
737
1738
|
} catch (error) {
|
|
738
1739
|
throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
|
|
739
1740
|
}
|
|
@@ -781,13 +1782,13 @@ var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY, i
|
|
|
781
1782
|
|
|
782
1783
|
// src/scanner/semgrep.ts
|
|
783
1784
|
import { execFile as execFile2 } from "child_process";
|
|
784
|
-
import { delimiter, dirname as
|
|
1785
|
+
import { delimiter, dirname as dirname3 } from "path";
|
|
785
1786
|
import { promisify as promisify2 } from "util";
|
|
786
1787
|
|
|
787
1788
|
// src/scanner/semgrep-runtime.ts
|
|
788
1789
|
import { existsSync, readFileSync } from "fs";
|
|
789
1790
|
import { createRequire } from "module";
|
|
790
|
-
import { dirname, resolve } from "path";
|
|
1791
|
+
import { dirname as dirname2, resolve } from "path";
|
|
791
1792
|
var require2 = createRequire(import.meta.url);
|
|
792
1793
|
var packageForPlatform = (platform, architecture) => {
|
|
793
1794
|
if (platform === "linux" && architecture === "x64") {
|
|
@@ -809,7 +1810,7 @@ var resolveBundledSemgrepRuntime = (platform = process.platform, architecture =
|
|
|
809
1810
|
const manifestPath = resolveManifestPath(packageName);
|
|
810
1811
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
811
1812
|
const runtime = {
|
|
812
|
-
semgrep: resolve(
|
|
1813
|
+
semgrep: resolve(dirname2(manifestPath), manifest.semgrep)
|
|
813
1814
|
};
|
|
814
1815
|
if (!existsSync(runtime.semgrep)) {
|
|
815
1816
|
throw new Error("artefatos do runtime ausentes");
|
|
@@ -880,11 +1881,11 @@ var semgrepArgs = (ruleset, includeTests) => [
|
|
|
880
1881
|
"."
|
|
881
1882
|
];
|
|
882
1883
|
var semgrepEnvironment = (runtime) => {
|
|
883
|
-
const semgrepDir =
|
|
1884
|
+
const semgrepDir = dirname3(runtime.semgrep);
|
|
884
1885
|
const systemPathFallback = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
|
|
885
1886
|
return {
|
|
886
1887
|
...process.env,
|
|
887
|
-
PATH: `${semgrepDir}${delimiter}${
|
|
1888
|
+
PATH: `${semgrepDir}${delimiter}${dirname3(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
|
|
888
1889
|
};
|
|
889
1890
|
};
|
|
890
1891
|
var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests) => {
|
|
@@ -943,9 +1944,9 @@ var writeMarkdownReportIfNeeded = async (result, targetDir) => {
|
|
|
943
1944
|
if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
|
|
944
1945
|
return;
|
|
945
1946
|
}
|
|
946
|
-
const filePath =
|
|
1947
|
+
const filePath = join4(targetDir, generateMarkdownReportFilename());
|
|
947
1948
|
try {
|
|
948
|
-
await
|
|
1949
|
+
await writeFile2(filePath, toMarkdownReport(result), "utf-8");
|
|
949
1950
|
console.log(chalk2.cyan(`
|
|
950
1951
|
Relat\xF3rio detalhado gerado em: ${filePath}`));
|
|
951
1952
|
} catch (error) {
|
|
@@ -964,15 +1965,20 @@ var runNativeAndSemgrep = async (path, rules, options) => {
|
|
|
964
1965
|
throw new Error(`Falha durante a an\xE1lise: ${errorMessage3(error)}`, { cause: error });
|
|
965
1966
|
}
|
|
966
1967
|
};
|
|
967
|
-
var runOptionalDependencyAudit = async (path, merged) => {
|
|
1968
|
+
var runOptionalDependencyAudit = async (path, merged, options) => {
|
|
968
1969
|
try {
|
|
969
|
-
const auditResult = await runDependencyAudit(path);
|
|
1970
|
+
const auditResult = await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true });
|
|
970
1971
|
return finalizeScanResult(
|
|
971
1972
|
{
|
|
972
1973
|
...merged,
|
|
973
1974
|
findings: [...merged.findings, ...auditResult.findings],
|
|
1975
|
+
durationMs: merged.durationMs + auditResult.durationMs,
|
|
974
1976
|
warnings: [...merged.warnings ?? [], ...auditResult.warnings ?? []],
|
|
975
|
-
engines: {
|
|
1977
|
+
engines: {
|
|
1978
|
+
...merged.engines,
|
|
1979
|
+
osv: auditResult.engines?.osv,
|
|
1980
|
+
nvd: auditResult.engines?.nvd
|
|
1981
|
+
},
|
|
976
1982
|
osvCheckedPackages: auditResult.osvCheckedPackages
|
|
977
1983
|
},
|
|
978
1984
|
auditResult.engines?.dependencyAudit ?? false
|
|
@@ -980,14 +1986,15 @@ var runOptionalDependencyAudit = async (path, merged) => {
|
|
|
980
1986
|
} catch (error) {
|
|
981
1987
|
return finalizeScanResult({
|
|
982
1988
|
...merged,
|
|
983
|
-
warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`]
|
|
1989
|
+
warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`],
|
|
1990
|
+
engines: { ...merged.engines, nvd: false }
|
|
984
1991
|
});
|
|
985
1992
|
}
|
|
986
1993
|
};
|
|
987
1994
|
var runScanEngines = async (path, rules, options) => {
|
|
988
1995
|
try {
|
|
989
1996
|
const merged = await runNativeAndSemgrep(path, rules, options);
|
|
990
|
-
return options.deps ? runOptionalDependencyAudit(path, merged) : finalizeScanResult(merged);
|
|
1997
|
+
return options.deps ? runOptionalDependencyAudit(path, merged, options) : finalizeScanResult(merged);
|
|
991
1998
|
} catch (error) {
|
|
992
1999
|
throw error;
|
|
993
2000
|
}
|
|
@@ -1143,7 +2150,10 @@ var printAuditResult = (result, json) => {
|
|
|
1143
2150
|
};
|
|
1144
2151
|
var auditAndReport = async (path, options) => {
|
|
1145
2152
|
try {
|
|
1146
|
-
printAuditResult(
|
|
2153
|
+
printAuditResult(
|
|
2154
|
+
await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true }),
|
|
2155
|
+
options.json ?? false
|
|
2156
|
+
);
|
|
1147
2157
|
} catch (error) {
|
|
1148
2158
|
const message = error instanceof Error ? error.message : "erro desconhecido";
|
|
1149
2159
|
process.exitCode = 1;
|
|
@@ -1151,9 +2161,7 @@ var auditAndReport = async (path, options) => {
|
|
|
1151
2161
|
}
|
|
1152
2162
|
};
|
|
1153
2163
|
var registerDependencyAuditCommand = (program) => {
|
|
1154
|
-
program.command("dependency-audit").description(
|
|
1155
|
-
'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit" e OSV.dev; requer npm no PATH e acesso \xE0 rede)'
|
|
1156
|
-
).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));
|
|
1157
2165
|
};
|
|
1158
2166
|
|
|
1159
2167
|
// src/rules/empty-catch.rule.ts
|
|
@@ -2952,9 +3960,9 @@ var registerScanCommand = (program) => {
|
|
|
2952
3960
|
withScanOptions(
|
|
2953
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(
|
|
2954
3962
|
"--no-deps",
|
|
2955
|
-
"n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev) neste scan \u2014 permite rodar offline"
|
|
3963
|
+
"n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev + NVD) neste scan \u2014 permite rodar offline"
|
|
2956
3964
|
)
|
|
2957
|
-
).action(
|
|
3965
|
+
).option("--no-nvd", "n\xE3o enriquece os resultados OSV com dados do NVD").action(
|
|
2958
3966
|
(path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
|
|
2959
3967
|
);
|
|
2960
3968
|
};
|