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.
Files changed (3) hide show
  1. package/README.md +89 -3
  2. package/dist/index.js +1688 -162
  3. 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 readFileSync2 } from "fs";
5
- import { createRequire as createRequire4 } from "module";
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
- const bindingSet = isExecImport(specifier) ? bindings.directCalls : isNamespaceImport(specifier) ? bindings.namespaces : void 0;
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?.type === "Identifier" ? id.name : void 0;
80
- const destructuredBindings = id?.type === "ObjectPattern" ? id : void 0;
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 isKnownChildProcessCallee = (callee, bindings) => {
96
- if (callee?.type === "Identifier") {
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 join2 } from "path";
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: false },
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 coverageText = (result) => result.engines?.semgrep === void 0 ? "" : ` CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s).`;
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 = (result) => {
236
+ var findingsTable = (findings) => {
215
237
  const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
216
- for (const finding of result.findings) {
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
- console.log(findingsTable(result).toString());
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 = [`## ${severityLabel(severity)} (${findings.length})`, ""];
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.engines?.semgrep === void 0 ? [] : [
293
- `- **Cobertura por motor:** CodeSentry ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep ${result.engines.semgrep} arquivos`
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.engines?.dependencyAudit === false ? [`- **Nota:** ${DEPENDENCY_AUDIT_NOTE}`] : [],
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(`| ${severityLabel(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
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(bySeverity)
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 isIgnoredDirName(entry.name, includeTests) ? [] : walk(join(currentDir, entry.name), includeTests);
1705
+ return filesFromDirectory(currentDir, entry, includeTests);
351
1706
  }
352
- return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join(currentDir, entry.name)] : [];
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 errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
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): ${errorMessage(error)}`,
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 readFile(filePath, "utf-8");
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 dirname2 } from "path";
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(dirname(manifestPath), manifest.semgrep)
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
- const reason = error instanceof Error ? error.message : "erro desconhecido";
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 execFileAsync = promisify(execFile);
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 = dirname2(runtime.semgrep);
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}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
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 = execFileAsync, includeTests = false) => {
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 errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
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 = join2(targetDir, generateMarkdownReportFilename());
1947
+ const filePath = join4(targetDir, generateMarkdownReportFilename());
600
1948
  try {
601
- await writeFile(filePath, toMarkdownReport(result), "utf-8");
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: ${errorMessage2(error)}`));
1953
+ console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage3(error)}`));
606
1954
  }
607
1955
  };
608
- var runScanEngines = async (path, rules, options) => {
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
- if (!options.semgrep) {
613
- return finalizeScanResult(nativeResult);
614
- }
615
- const semgrepResult = await runBundledSemgrep(path, void 0, options.config, void 0, includeTests);
616
- return finalizeScanResult(mergeScanResults(nativeResult, semgrepResult));
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
- throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, { cause: error });
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 : node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
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(await runDependencyAudit(path), options.json ?? false);
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 callee = node.type === "CallExpression" ? node.callee : void 0;
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
- const property = callee.property;
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 isJwtSignWithoutExpiration = (node) => {
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 callee = node.type === "CallExpression" ? node.callee : void 0;
1691
- const property = callee?.type === "MemberExpression" ? callee.property : void 0;
1692
- const args = node.type === "CallExpression" ? node.arguments : void 0;
1693
- const headerName = args?.[0];
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 asyncFunctionName = (node) => {
3225
+ var functionDeclarationName = (node) => {
1849
3226
  const declarationId = node.type === "FunctionDeclaration" ? node.id : void 0;
1850
- const declarationName = declarationId?.type === "Identifier" ? declarationId.name : void 0;
1851
- const id = node.type === "VariableDeclarator" ? node.id : void 0;
1852
- const init = node.type === "VariableDeclarator" ? node.init : void 0;
1853
- const hasAsyncFunctionValue = (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && init.async;
1854
- return declarationName ?? (id?.type === "Identifier" && hasAsyncFunctionValue ? id.name : void 0);
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
- const isKnownFunction = callee.type === "Identifier" && (callee.name === "fetch" || asyncFunctionNames.has(callee.name));
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 memberPropertyName = (member) => {
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" && memberPropertyName(callee) === "then" && (args?.length ?? 0) < 2;
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 isChainMember = member !== void 0 && member.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
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 callee = node.type === "CallExpression" ? node.callee : void 0;
2313
- const property = callee?.type === "MemberExpression" ? callee.property : void 0;
2314
- const args = node.type === "CallExpression" ? node.arguments : void 0;
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
- ).action(
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 require5 = createRequire4(import.meta.url);
4272
+ var require6 = createRequire5(import.meta.url);
2748
4273
  var readPackageVersion = () => {
2749
- const packageJsonPath = require5.resolve("../package.json");
2750
- const { version } = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
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);