codesentry 0.1.11 → 0.2.1

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 +47 -2
  2. package/dist/index.js +659 -141
  3. package/package.json +7 -4
package/README.md CHANGED
@@ -16,7 +16,7 @@ Semgrep CE para as linguagens suportadas por ele.
16
16
 
17
17
  ## O que o CodeSentry faz
18
18
 
19
- Rodando `codesentry scan` num projeto, dois motores de análise trabalham
19
+ Rodando `codesentry scan` num projeto, três motores de análise trabalham
20
20
  juntos e o resultado sai unificado em um único relatório:
21
21
 
22
22
  - **Motor nativo** — regras próprias em TypeScript, sem dependências
@@ -29,6 +29,12 @@ juntos e o resultado sai unificado em um único relatório:
29
29
  as linguagens que o Semgrep suporta (não só JS/TS), cobrindo os riscos
30
30
  do OWASP Top 10 de forma mais ampla que regras hand-rolled sozinhas
31
31
  conseguiriam.
32
+ - **Auditoria de dependências** (`npm audit` + [OSV.dev](https://osv.dev)) —
33
+ identifica vulnerabilidades conhecidas nas dependências reais do
34
+ projeto (via `package-lock.json`), com a sugestão de correção vindo
35
+ diretamente do banco consultado. Roda por padrão a partir desta versão
36
+ e exige rede; use `--no-deps` para um scan 100% offline. Ver
37
+ [ADR 0005](docs/adr/0005-osv-dependency-database.md).
32
38
 
33
39
  Cada achado no relatório mostra o arquivo, a linha, a severidade e qual
34
40
  motor encontrou o problema (prefixo `semgrep/` para achados do Semgrep).
@@ -141,6 +147,7 @@ codesentry scan . --json
141
147
  codesentry scan . --concurrency 4
142
148
  codesentry scan . --config ./rules/security.yml
143
149
  codesentry scan . --tests
150
+ codesentry scan . --no-deps
144
151
  ```
145
152
 
146
153
  O Semgrep CE embutido é executado automaticamente depois das regras nativas,
@@ -148,6 +155,44 @@ usando um snapshot local do ruleset OWASP e `--metrics=off`. O comando não
148
155
  consulta a Semgrep Registry, não envia métricas e não requer internet após a
149
156
  instalação.
150
157
 
158
+ Por padrão, `scan` também roda a auditoria de dependências (`npm audit` +
159
+ OSV.dev) e funde os achados no mesmo relatório — isso exige `npm` no `PATH`
160
+ e acesso à rede. Use `--no-deps` para pular essa etapa e manter o scan
161
+ 100% offline (CI sem egress, ambientes air-gapped). Ver
162
+ [ADR 0005](docs/adr/0005-osv-dependency-database.md).
163
+
164
+ O console mostra quantos pacotes o `npm audit` cobriu e quantos deles o
165
+ OSV.dev conseguiu verificar de fato (`OSV.dev: 360/363 verificados`, por
166
+ exemplo — a diferença indica pacotes cuja consulta falhou, reportados
167
+ também como aviso). No relatório Markdown gerado automaticamente (mais de
168
+ 20 problemas), a lista completa de dependências verificadas no OSV.dev
169
+ (`nome@versão`) aparece numa seção própria ao final do arquivo.
170
+
171
+ ### Versões e atualização dos motores
172
+
173
+ Use `codesentry version --engines` para auditar exatamente os componentes
174
+ embutidos na sua instalação. O comando não executa o Semgrep nem acessa a
175
+ rede; ele mostra a versão do CodeSentry, a versão do Semgrep CE e Python do
176
+ runtime da plataforma, e a proveniência do snapshot `p/owasp-top-ten`
177
+ (origem, data de captura, revisão upstream quando disponível e SHA-256).
178
+
179
+ ```bash
180
+ codesentry version --engines
181
+ codesentry version --engines --json
182
+ ```
183
+
184
+ O snapshot do ruleset é identificado pela versão do pacote publicada junto ao
185
+ CodeSentry e pelo SHA-256 do seu conteúdo. A Semgrep Registry não fornece
186
+ necessariamente um commit estável para um ruleset público; nesse caso, o hash
187
+ é o identificador imutável que permite comparar o conteúdo auditado.
188
+
189
+ **Política de atualização:** revisamos semanalmente novas versões do Semgrep
190
+ e alterações no `p/owasp-top-ten`; atualizações regulares são publicadas em
191
+ até 30 dias. Correções upstream classificadas como críticas ou que afetem a
192
+ integridade da análise têm prioridade para uma release em até 48 horas. Toda
193
+ release que atualizar um motor ou ruleset registra as versões e hashes nos
194
+ artefatos publicados.
195
+
151
196
  `--concurrency <n>` limita o processamento paralelo do scanner nativo; use
152
197
  apenas inteiros positivos. Sem valor, o limite é ajustado para a máquina
153
198
  (`min(8, availableParallelism())`). Não há `--config` remoto: atualizações de
@@ -199,7 +244,7 @@ codesentry xss ./src --json # possíveis XSS (innerHTML, document.write
199
244
  codesentry unsafe-sql ./src # SQL injection por concatenação
200
245
  codesentry command-injection ./src # child_process com entrada não sanitizada
201
246
  codesentry weak-hash-algorithm ./src # uso de MD5/SHA-1 para hashing sensível
202
- codesentry dependency-audit . # `npm audit` das dependências do projeto (sem --tests: não lê arquivos-fonte)
247
+ codesentry dependency-audit . # `npm audit` + OSV.dev nas dependências do projeto (sem --tests: não lê arquivos-fonte)
203
248
  ```
204
249
 
205
250
  Assim como em `scan`, `--tests` inclui arquivos de teste na análise (por
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") {
@@ -146,7 +151,7 @@ var withScanOptions = (command) => command.option("--json", "exibe o resultado e
146
151
 
147
152
  // src/commands/scan/scan-runner.ts
148
153
  import { writeFile } from "fs/promises";
149
- import { join as join2 } from "path";
154
+ import { join as join3 } from "path";
150
155
  import chalk2 from "chalk";
151
156
  import { Listr } from "listr2";
152
157
 
@@ -185,9 +190,9 @@ var mergeScanResults = (nativeResult, semgrepResult) => ({
185
190
  semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
186
191
  }
187
192
  });
188
- var finalizeScanResult = (result) => ({
193
+ var finalizeScanResult = (result, dependencyAuditCoverage2 = false) => ({
189
194
  ...result,
190
- engines: { ...result.engines, dependencyAudit: false },
195
+ engines: { ...result.engines, dependencyAudit: dependencyAuditCoverage2 },
191
196
  warnings: result.engines?.semgrep === 0 ? [...result.warnings ?? [], ZERO_SEMGREP_COVERAGE_WARNING] : result.warnings
192
197
  });
193
198
 
@@ -198,7 +203,16 @@ var SEVERITY_COLOR = {
198
203
  high: (text2) => chalk.red(text2),
199
204
  critical: (text2) => chalk.bgRed.white(text2)
200
205
  };
201
- var coverageText = (result) => result.engines?.semgrep === void 0 ? "" : ` CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s).`;
206
+ var semgrepCoverage = (result) => result.engines?.semgrep === void 0 ? void 0 : `CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s)`;
207
+ var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) via npm audit` : void 0;
208
+ var osvCoverage = (result) => result.engines?.osv ? `OSV.dev: ${result.engines.osv.checked}/${result.engines.osv.total} verificados` : void 0;
209
+ var coverageParts = (result) => [semgrepCoverage(result), dependencyAuditCoverage(result), osvCoverage(result)].filter(
210
+ (part) => part !== void 0
211
+ );
212
+ var coverageText = (result) => {
213
+ const parts = coverageParts(result);
214
+ return parts.length ? ` ${parts.join("; ")}.` : "";
215
+ };
202
216
  var printNotes = (result) => {
203
217
  if (result.engines?.dependencyAudit === false) {
204
218
  console.log(chalk.cyan(DEPENDENCY_AUDIT_NOTE));
@@ -316,26 +330,360 @@ var severitySections = (bySeverity) => {
316
330
  }
317
331
  return lines;
318
332
  };
333
+ var osvCheckedSection = (result) => {
334
+ const { osvCheckedPackages } = result;
335
+ if (!osvCheckedPackages || osvCheckedPackages.length === 0) {
336
+ return [];
337
+ }
338
+ const checked = result.engines?.osv?.checked ?? osvCheckedPackages.length;
339
+ const total = result.engines?.osv?.total ?? osvCheckedPackages.length;
340
+ return [
341
+ `## Depend\xEAncias verificadas no OSV.dev (${checked}/${total})`,
342
+ "",
343
+ ...osvCheckedPackages.map((pkg) => `- ${pkg}`),
344
+ ""
345
+ ];
346
+ };
319
347
  var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
320
348
  const bySeverity = groupBy(result.findings, (f) => f.severity);
321
349
  return [
322
350
  ...reportHeader(result, generatedAt),
323
351
  ...warningsSection(result),
324
352
  ...summaryTable(bySeverity),
325
- ...severitySections(bySeverity)
353
+ ...severitySections(bySeverity),
354
+ ...osvCheckedSection(result)
326
355
  ].join("\n");
327
356
  };
328
357
 
329
- // src/scanner/scanner.ts
358
+ // src/scanner/dependency-audit.ts
359
+ import { execFile } from "child_process";
330
360
  import { readFile } from "fs/promises";
361
+ import { join } from "path";
362
+ import { promisify } from "util";
363
+
364
+ // src/scanner/run-with-concurrency-limit.ts
365
+ import pLimit from "p-limit";
366
+ var runWithConcurrencyLimit = async (items, concurrency, task) => {
367
+ const limit = pLimit(concurrency);
368
+ return Promise.all(items.map((item) => limit(() => task(item))));
369
+ };
370
+
371
+ // src/scanner/osv-client.ts
372
+ var OSV_BATCH_CHUNK_SIZE = 100;
373
+ var OSV_DETAIL_CONCURRENCY = 10;
374
+ var OSV_API_BASE = "https://api.osv.dev/v1";
375
+ var SEVERITY_MAP = {
376
+ LOW: "low",
377
+ MODERATE: "medium",
378
+ HIGH: "high",
379
+ CRITICAL: "critical"
380
+ };
381
+ var chunk = (items, size) => {
382
+ const chunks = [];
383
+ for (let i = 0; i < items.length; i += size) {
384
+ chunks.push(items.slice(i, i + size));
385
+ }
386
+ return chunks;
387
+ };
388
+ var zipVulnIdsByPackage = (packages, body) => {
389
+ const vulnIdsByPackage = /* @__PURE__ */ new Map();
390
+ packages.forEach((pkg, index) => {
391
+ const ids = body.results?.[index]?.vulns?.map((v) => v.id);
392
+ if (ids?.length) {
393
+ vulnIdsByPackage.set(`${pkg.name}@${pkg.version}`, ids);
394
+ }
395
+ });
396
+ return vulnIdsByPackage;
397
+ };
398
+ var queryBatchChunk = async (packages, fetchImpl) => {
399
+ try {
400
+ const response = await fetchImpl(`${OSV_API_BASE}/querybatch`, {
401
+ method: "POST",
402
+ headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify({
404
+ queries: packages.map((p) => ({
405
+ package: { name: p.name, ecosystem: "npm" },
406
+ version: p.version
407
+ }))
408
+ })
409
+ });
410
+ if (!response.ok) {
411
+ return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
412
+ }
413
+ const body = await response.json();
414
+ return { vulnIdsByPackage: zipVulnIdsByPackage(packages, body), checkedPackages: packages, failedCount: 0 };
415
+ } catch {
416
+ return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
417
+ }
418
+ };
419
+ var queryOsvBatch = async (packages, fetchImpl = fetch) => {
420
+ const vulnIdsByPackage = /* @__PURE__ */ new Map();
421
+ const checkedPackages = [];
422
+ let failedCount = 0;
423
+ try {
424
+ for (const packageChunk of chunk(packages, OSV_BATCH_CHUNK_SIZE)) {
425
+ const result = await queryBatchChunk(packageChunk, fetchImpl);
426
+ result.vulnIdsByPackage.forEach((ids, key) => vulnIdsByPackage.set(key, ids));
427
+ checkedPackages.push(...result.checkedPackages);
428
+ failedCount += result.failedCount;
429
+ }
430
+ } catch {
431
+ failedCount += packages.length - checkedPackages.length;
432
+ }
433
+ return {
434
+ vulnIdsByPackage,
435
+ checkedPackages,
436
+ warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel consultar o OSV.dev para ${failedCount} pacote(s).` : void 0
437
+ };
438
+ };
439
+ var fetchOneVulnerabilityDetail = async (id, fetchImpl, detailsById) => {
440
+ try {
441
+ const response = await fetchImpl(`${OSV_API_BASE}/vulns/${id}`);
442
+ if (!response.ok) {
443
+ return false;
444
+ }
445
+ detailsById.set(id, await response.json());
446
+ return true;
447
+ } catch {
448
+ return false;
449
+ }
450
+ };
451
+ var fetchOsvVulnerabilityDetails = async (ids, fetchImpl = fetch) => {
452
+ const uniqueIds = [...new Set(ids)];
453
+ const detailsById = /* @__PURE__ */ new Map();
454
+ let results;
455
+ try {
456
+ results = await runWithConcurrencyLimit(
457
+ uniqueIds,
458
+ OSV_DETAIL_CONCURRENCY,
459
+ (id) => fetchOneVulnerabilityDetail(id, fetchImpl, detailsById)
460
+ );
461
+ } catch (error) {
462
+ throw error;
463
+ }
464
+ const failedCount = results.filter((ok) => !ok).length;
465
+ return {
466
+ detailsById,
467
+ warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel obter detalhes do OSV.dev para ${failedCount} advisory(s).` : void 0
468
+ };
469
+ };
470
+ var isMatchingNpmPackage = (affected, packageName) => affected.package.ecosystem === "npm" && affected.package.name === packageName;
471
+ var extractFixedVersions = (vuln, packageName) => {
472
+ 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));
473
+ return [...new Set(fixedVersions)];
474
+ };
475
+ var mapOsvSeverity = (vuln) => {
476
+ const severity = vuln.database_specific?.severity;
477
+ if (!severity) {
478
+ return "medium";
479
+ }
480
+ return SEVERITY_MAP[severity] ?? "medium";
481
+ };
482
+
483
+ // src/scanner/package-lock-parser.ts
484
+ var NODE_MODULES_SEGMENT = "node_modules/";
485
+ var isUnresolvableSource = (resolved) => resolved !== void 0 && (resolved.startsWith("file:") || resolved.startsWith("git"));
486
+ var nameFromKey = (key) => {
487
+ const lastIndex = key.lastIndexOf(NODE_MODULES_SEGMENT);
488
+ return lastIndex === -1 ? void 0 : key.slice(lastIndex + NODE_MODULES_SEGMENT.length);
489
+ };
490
+ var toLockedPackage = (key, entry) => {
491
+ if (entry.link || !entry.version || isUnresolvableSource(entry.resolved)) {
492
+ return void 0;
493
+ }
494
+ const name = entry.name ?? nameFromKey(key);
495
+ return name ? { name, version: entry.version } : void 0;
496
+ };
497
+ var assertSupportedLockfileVersion = (lockfileVersion) => {
498
+ if (lockfileVersion !== 2 && lockfileVersion !== 3) {
499
+ throw new Error("lockfileVersion 1 n\xE3o \xE9 suportado \u2014 regenere o lockfile com npm 7+.");
500
+ }
501
+ };
502
+ var parsePackageLock = (rawJson) => {
503
+ const parsed = JSON.parse(rawJson);
504
+ assertSupportedLockfileVersion(parsed.lockfileVersion);
505
+ const deduped = /* @__PURE__ */ new Map();
506
+ Object.entries(parsed.packages ?? {}).filter(([key]) => key.includes(NODE_MODULES_SEGMENT)).forEach(([key, entry]) => {
507
+ const locked = toLockedPackage(key, entry);
508
+ if (locked) {
509
+ deduped.set(`${locked.name}@${locked.version}`, locked);
510
+ }
511
+ });
512
+ return [...deduped.values()];
513
+ };
514
+
515
+ // src/scanner/dependency-audit.ts
516
+ var execFileAsync = promisify(execFile);
517
+ var SEVERITY_MAP2 = {
518
+ info: "low",
519
+ low: "low",
520
+ moderate: "medium",
521
+ high: "high",
522
+ critical: "critical"
523
+ };
524
+ var vulnerabilityTitle = (vulnerability) => {
525
+ const firstVia = vulnerability.via[0];
526
+ if (typeof firstVia === "object" && firstVia?.title) {
527
+ return firstVia.title;
528
+ }
529
+ return 'ver "npm audit" para detalhes';
530
+ };
531
+ var npmFixSuggestion = (vulnerability) => {
532
+ const { fixAvailable } = vulnerability;
533
+ if (fixAvailable === false) {
534
+ return "sem corre\xE7\xE3o dispon\xEDvel ainda";
535
+ }
536
+ if (fixAvailable === true) {
537
+ return `atualize para uma vers\xE3o fora do intervalo vulner\xE1vel (${vulnerability.range})`;
538
+ }
539
+ return `atualize para ${fixAvailable.name}@${fixAvailable.version}`;
540
+ };
541
+ var mapAuditReportToFindings = (report) => {
542
+ return Object.values(report.vulnerabilities).map((vulnerability) => ({
543
+ ruleId: "dependency-audit",
544
+ message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)} \u2014 ${npmFixSuggestion(vulnerability)}`,
545
+ file: "package.json",
546
+ line: 1,
547
+ severity: SEVERITY_MAP2[vulnerability.severity]
548
+ }));
549
+ };
550
+ var osvFixSuggestion = (fixedVersions) => fixedVersions.length ? `atualize para ${fixedVersions.join(" ou ")}` : "nenhuma vers\xE3o corrigida publicada pelo OSV.dev ainda";
551
+ var buildOsvFinding = (pkg, vuln) => {
552
+ const fixedVersions = extractFixedVersions(vuln, pkg.name);
553
+ return {
554
+ ruleId: "dependency-audit",
555
+ message: `OSV ${vuln.id}: ${pkg.name}@${pkg.version} (${mapOsvSeverity(vuln)}) \u2014 ${vuln.summary ?? "ver OSV.dev para detalhes"} \u2014 ${osvFixSuggestion(fixedVersions)}`,
556
+ file: "package-lock.json",
557
+ line: 1,
558
+ severity: mapOsvSeverity(vuln)
559
+ };
560
+ };
561
+ var findingsForLockedPackage = (pkg, vulnIdsByPackage, detailsById) => {
562
+ const ids = vulnIdsByPackage.get(`${pkg.name}@${pkg.version}`) ?? [];
563
+ return ids.map((id) => detailsById.get(id)).filter((vuln) => vuln !== void 0).map((vuln) => buildOsvFinding(pkg, vuln));
564
+ };
565
+ var mapOsvFindingsToRuleFindings = (lockedPackages, vulnIdsByPackage, detailsById, npmFlaggedNames) => lockedPackages.filter((pkg) => !npmFlaggedNames.has(pkg.name)).flatMap((pkg) => findingsForLockedPackage(pkg, vulnIdsByPackage, detailsById));
566
+ var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
567
+ var hasVulnerabilitiesRecord = (value) => typeof value === "object" && value !== null && typeof value.vulnerabilities === "object" && value.vulnerabilities !== null;
568
+ var normalizeNpmAuditReport = (raw) => {
569
+ if (hasVulnerabilitiesRecord(raw)) {
570
+ return { report: { vulnerabilities: raw.vulnerabilities } };
571
+ }
572
+ const errorSummary = typeof raw === "object" && raw !== null && "error" in raw ? raw.error?.summary ?? "formato de resposta inesperado" : "formato de resposta inesperado";
573
+ return {
574
+ report: { vulnerabilities: {} },
575
+ warning: `"npm audit" n\xE3o retornou um relat\xF3rio v\xE1lido: ${errorSummary}.`
576
+ };
577
+ };
578
+ var combineWarnings = (...warnings) => {
579
+ const present = warnings.filter((warning) => Boolean(warning));
580
+ return present.length ? present.join(" ") : void 0;
581
+ };
582
+ var fetchDetailsForIds = (ids, fetchImpl) => ids.length ? fetchOsvVulnerabilityDetails(ids, fetchImpl) : Promise.resolve({ detailsById: /* @__PURE__ */ new Map(), warning: void 0 });
583
+ var auditPackagesWithOsv = async (lockedPackages, npmFlaggedNames, fetchImpl = fetch) => {
584
+ try {
585
+ return await collectOsvAuditResult(lockedPackages, npmFlaggedNames, fetchImpl);
586
+ } catch (error) {
587
+ return {
588
+ findings: [],
589
+ checkedPackages: [],
590
+ warning: `N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`
591
+ };
592
+ }
593
+ };
594
+ var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) => {
595
+ try {
596
+ const {
597
+ vulnIdsByPackage,
598
+ checkedPackages,
599
+ warning: batchWarning
600
+ } = await queryOsvBatch(lockedPackages, fetchImpl);
601
+ const ids = [...new Set([...vulnIdsByPackage.values()].flat())];
602
+ const { detailsById, warning: detailsWarning } = await fetchDetailsForIds(ids, fetchImpl);
603
+ return {
604
+ findings: mapOsvFindingsToRuleFindings(
605
+ lockedPackages,
606
+ vulnIdsByPackage,
607
+ detailsById,
608
+ npmFlaggedNames
609
+ ),
610
+ checkedPackages,
611
+ warning: combineWarnings(batchWarning, detailsWarning)
612
+ };
613
+ } catch (error) {
614
+ throw new Error("N\xE3o foi poss\xEDvel consolidar os resultados do OSV.dev.", { cause: error });
615
+ }
616
+ };
617
+ var runNpmAudit = async (targetDir) => {
618
+ let stdout;
619
+ try {
620
+ ({ stdout } = await execFileAsync("npm", ["audit", "--json"], {
621
+ cwd: targetDir,
622
+ maxBuffer: 10 * 1024 * 1024
623
+ }));
624
+ } catch (error) {
625
+ const stdoutFromError = error.stdout;
626
+ if (!stdoutFromError) {
627
+ throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
628
+ }
629
+ stdout = stdoutFromError;
630
+ }
631
+ return JSON.parse(stdout);
632
+ };
633
+ var auditPackagesFromLockfile = async (targetDir, npmFlaggedNames, fetchImpl) => {
634
+ try {
635
+ const raw = await readFile(join(targetDir, "package-lock.json"), "utf-8");
636
+ const lockedPackages = parsePackageLock(raw);
637
+ const osvResult = await auditPackagesWithOsv(lockedPackages, npmFlaggedNames, fetchImpl);
638
+ return {
639
+ findings: osvResult.findings,
640
+ packagesAudited: lockedPackages.length,
641
+ osvChecked: { checked: osvResult.checkedPackages.length, total: lockedPackages.length },
642
+ osvCheckedPackages: osvResult.checkedPackages.map((pkg) => `${pkg.name}@${pkg.version}`).sort(),
643
+ warning: osvResult.warning
644
+ };
645
+ } catch (error) {
646
+ return {
647
+ findings: [],
648
+ packagesAudited: false,
649
+ warning: `N\xE3o foi poss\xEDvel checar o OSV.dev: ${errorMessage(error)}.`
650
+ };
651
+ }
652
+ };
653
+ var runDependencyAudit = async (targetDir, fetchImpl = fetch) => {
654
+ const startedAt = Date.now();
655
+ try {
656
+ const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(await runNpmAudit(targetDir));
657
+ const npmFindings = mapAuditReportToFindings(npmReport);
658
+ const npmFlaggedNames = new Set(Object.keys(npmReport.vulnerabilities));
659
+ const osv = await auditPackagesFromLockfile(targetDir, npmFlaggedNames, fetchImpl);
660
+ const warnings = combineWarnings(npmWarning, osv.warning);
661
+ return {
662
+ scannedFiles: 1,
663
+ findings: [...npmFindings, ...osv.findings],
664
+ durationMs: Date.now() - startedAt,
665
+ engines: {
666
+ dependencyAudit: osv.packagesAudited === false ? npmFlaggedNames.size : osv.packagesAudited,
667
+ osv: osv.osvChecked
668
+ },
669
+ osvCheckedPackages: osv.osvCheckedPackages,
670
+ warnings: warnings ? [warnings] : void 0
671
+ };
672
+ } catch (error) {
673
+ throw new Error(`N\xE3o foi poss\xEDvel auditar depend\xEAncias em "${targetDir}".`, { cause: error });
674
+ }
675
+ };
676
+
677
+ // src/scanner/scanner.ts
678
+ import { readFile as readFile2 } from "fs/promises";
331
679
  import { availableParallelism } from "os";
332
680
 
333
681
  // src/scanner/file-finder.ts
334
682
  import { readdir } from "fs/promises";
335
- import { join } from "path";
683
+ import { join as join2 } from "path";
336
684
 
337
685
  // src/scanner/ignore-patterns.ts
338
- var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next"];
686
+ var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next", ".angular"];
339
687
  var TEST_DIR_NAMES = ["tests", "test", "__tests__"];
340
688
  var TEST_FILE_GLOBS = ["*.spec.*", "*.test.*"];
341
689
  var TEST_FILE_NAME_PATTERN = /\.(spec|test)\.[^./]+$/;
@@ -345,11 +693,17 @@ var isTestFileName = (fileName) => TEST_FILE_NAME_PATTERN.test(fileName);
345
693
  // src/scanner/file-finder.ts
346
694
  var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
347
695
  var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
696
+ var filesFromDirectory = async (currentDir, entry, includeTests) => {
697
+ return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join2(currentDir, entry.name), includeTests);
698
+ };
699
+ var filesFromFile = (currentDir, entry, includeTests) => {
700
+ return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join2(currentDir, entry.name)] : [];
701
+ };
348
702
  var filesFromEntry = async (currentDir, entry, includeTests) => {
349
703
  if (entry.isDirectory()) {
350
- return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join(currentDir, entry.name), includeTests);
704
+ return filesFromDirectory(currentDir, entry, includeTests);
351
705
  }
352
- return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join(currentDir, entry.name)] : [];
706
+ return filesFromFile(currentDir, entry, includeTests);
353
707
  };
354
708
  var walk = async (currentDir, includeTests) => {
355
709
  try {
@@ -367,26 +721,19 @@ var findFiles = async (targetDir, includeTests = false) => {
367
721
  }
368
722
  };
369
723
 
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
724
  // src/scanner/scanner.ts
378
725
  var DEFAULT_SCAN_CONCURRENCY = Math.max(1, Math.min(8, availableParallelism()));
379
- var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
726
+ var errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
380
727
  var parseErrorFinding = (filePath, error) => ({
381
728
  ruleId: "parse-error",
382
- message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${errorMessage(error)}`,
729
+ message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${errorMessage2(error)}`,
383
730
  file: filePath,
384
731
  line: 1,
385
732
  severity: "low"
386
733
  });
387
734
  var readFileContent = async (filePath) => {
388
735
  try {
389
- return await readFile(filePath, "utf-8");
736
+ return await readFile2(filePath, "utf-8");
390
737
  } catch (error) {
391
738
  throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
392
739
  }
@@ -433,9 +780,9 @@ var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY, i
433
780
  };
434
781
 
435
782
  // src/scanner/semgrep.ts
436
- import { execFile } from "child_process";
783
+ import { execFile as execFile2 } from "child_process";
437
784
  import { delimiter, dirname as dirname2 } from "path";
438
- import { promisify } from "util";
785
+ import { promisify as promisify2 } from "util";
439
786
 
440
787
  // src/scanner/semgrep-runtime.ts
441
788
  import { existsSync, readFileSync } from "fs";
@@ -452,6 +799,7 @@ var packageForPlatform = (platform, architecture) => {
452
799
  return void 0;
453
800
  };
454
801
  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.`;
802
+ var errorReason = (error) => error instanceof Error ? error.message : "erro desconhecido";
455
803
  var resolveBundledSemgrepRuntime = (platform = process.platform, architecture = process.arch, resolveManifestPath = (packageName) => require2.resolve(`${packageName}/runtime.json`)) => {
456
804
  const packageName = packageForPlatform(platform, architecture);
457
805
  if (!packageName) {
@@ -468,8 +816,7 @@ var resolveBundledSemgrepRuntime = (platform = process.platform, architecture =
468
816
  }
469
817
  return runtime;
470
818
  } 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 });
819
+ throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${errorReason(error)}`, { cause: error });
473
820
  }
474
821
  };
475
822
 
@@ -491,7 +838,7 @@ var resolveBundledSemgrepRuleset = () => {
491
838
  };
492
839
 
493
840
  // src/scanner/semgrep.ts
494
- var execFileAsync = promisify(execFile);
841
+ var execFileAsync2 = promisify2(execFile2);
495
842
  var SEMGREP_SEVERITIES = {
496
843
  INFO: "low",
497
844
  WARNING: "medium",
@@ -556,7 +903,7 @@ var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests)
556
903
  throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
557
904
  }
558
905
  };
559
- var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync, includeTests = false) => {
906
+ var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync2, includeTests = false) => {
560
907
  const startedAt = Date.now();
561
908
  try {
562
909
  const report = parseSemgrepReport(await executeSemgrep(targetDir, runtime, ruleset, execute, includeTests));
@@ -580,7 +927,7 @@ var generateMarkdownReportFilename = (date = /* @__PURE__ */ new Date()) => {
580
927
  };
581
928
 
582
929
  // src/commands/scan/scan-runner.ts
583
- var errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
930
+ var errorMessage3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
584
931
  var reportScanFailure = (error) => {
585
932
  process.exitCode = 1;
586
933
  console.error(`Falha ao executar o scan: ${formatErrorChain(error)}`);
@@ -596,26 +943,53 @@ var writeMarkdownReportIfNeeded = async (result, targetDir) => {
596
943
  if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
597
944
  return;
598
945
  }
599
- const filePath = join2(targetDir, generateMarkdownReportFilename());
946
+ const filePath = join3(targetDir, generateMarkdownReportFilename());
600
947
  try {
601
948
  await writeFile(filePath, toMarkdownReport(result), "utf-8");
602
949
  console.log(chalk2.cyan(`
603
950
  Relat\xF3rio detalhado gerado em: ${filePath}`));
604
951
  } catch (error) {
605
- console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage2(error)}`));
952
+ console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage3(error)}`));
606
953
  }
607
954
  };
608
- var runScanEngines = async (path, rules, options) => {
955
+ var runNativeAndSemgrep = async (path, rules, options) => {
609
956
  try {
610
957
  const includeTests = options.tests ?? false;
611
958
  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));
959
+ return options.semgrep ? mergeScanResults(
960
+ nativeResult,
961
+ await runBundledSemgrep(path, void 0, options.config, void 0, includeTests)
962
+ ) : nativeResult;
617
963
  } catch (error) {
618
- throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, { cause: error });
964
+ throw new Error(`Falha durante a an\xE1lise: ${errorMessage3(error)}`, { cause: error });
965
+ }
966
+ };
967
+ var runOptionalDependencyAudit = async (path, merged) => {
968
+ try {
969
+ const auditResult = await runDependencyAudit(path);
970
+ return finalizeScanResult(
971
+ {
972
+ ...merged,
973
+ findings: [...merged.findings, ...auditResult.findings],
974
+ warnings: [...merged.warnings ?? [], ...auditResult.warnings ?? []],
975
+ engines: { ...merged.engines, osv: auditResult.engines?.osv },
976
+ osvCheckedPackages: auditResult.osvCheckedPackages
977
+ },
978
+ auditResult.engines?.dependencyAudit ?? false
979
+ );
980
+ } catch (error) {
981
+ return finalizeScanResult({
982
+ ...merged,
983
+ warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`]
984
+ });
985
+ }
986
+ };
987
+ var runScanEngines = async (path, rules, options) => {
988
+ try {
989
+ const merged = await runNativeAndSemgrep(path, rules, options);
990
+ return options.deps ? runOptionalDependencyAudit(path, merged) : finalizeScanResult(merged);
991
+ } catch (error) {
992
+ throw error;
619
993
  }
620
994
  };
621
995
  var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
@@ -721,8 +1095,11 @@ var walkNestingBlock = (node, depth, results) => {
721
1095
  checkDepth(newDepth, node.loc?.start.line, results);
722
1096
  walkChildren(node, newDepth, results);
723
1097
  };
1098
+ var selectNonFunctionWalker = (node) => {
1099
+ return node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
1100
+ };
724
1101
  var selectWalker = (node) => {
725
- return FUNCTION_TYPES.has(node.type) ? walkFunction : node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
1102
+ return FUNCTION_TYPES.has(node.type) ? walkFunction : selectNonFunctionWalker(node);
726
1103
  };
727
1104
  var walk2 = (node, depth, results) => {
728
1105
  selectWalker(node)(node, depth, results);
@@ -756,56 +1133,6 @@ var registerDeepNestingCommand = (program) => {
756
1133
  );
757
1134
  };
758
1135
 
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
1136
  // src/commands/dependency-audit/dependency-audit.command.ts
810
1137
  var printAuditResult = (result, json) => {
811
1138
  if (json) {
@@ -825,7 +1152,7 @@ var auditAndReport = async (path, options) => {
825
1152
  };
826
1153
  var registerDependencyAuditCommand = (program) => {
827
1154
  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)'
1155
+ 'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit" e OSV.dev; requer npm no PATH e acesso \xE0 rede)'
829
1156
  ).argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").action((path, options) => auditAndReport(path, options));
830
1157
  };
831
1158
 
@@ -883,12 +1210,24 @@ var hasLimitOption = (options) => {
883
1210
  return key?.type === "Identifier" && key.name === "limit";
884
1211
  }) ?? false;
885
1212
  };
1213
+ var getMemberCallParts = (node) => {
1214
+ if (node.type !== "CallExpression") {
1215
+ return { object: void 0, property: void 0, args: void 0 };
1216
+ }
1217
+ const callee = node.callee;
1218
+ const args = node.arguments;
1219
+ if (callee?.type !== "MemberExpression") {
1220
+ return { object: void 0, property: void 0, args };
1221
+ }
1222
+ return {
1223
+ object: callee.object,
1224
+ property: callee.property,
1225
+ args
1226
+ };
1227
+ };
886
1228
  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;
1229
+ const { object, property, args } = getMemberCallParts(node);
890
1230
  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
1231
  return isBodyParserCall && !hasLimitOption(args?.[0]);
893
1232
  };
894
1233
  var findMissingBodyLimitLines = (filePath, content) => {
@@ -1133,9 +1472,10 @@ var createFunctionStatementCountRule = (config) => ({
1133
1472
  // src/rules/high-complexity.rule.ts
1134
1473
  var highComplexityRule = createFunctionStatementCountRule({
1135
1474
  id: "high-complexity",
1136
- description: "Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)",
1475
+ description: "Detecta fun\xE7\xF5es com muitos condicionais/loops/tern\xE1rios (complexidade alta)",
1137
1476
  statementTypes: /* @__PURE__ */ new Set([
1138
1477
  "IfStatement",
1478
+ "ConditionalExpression",
1139
1479
  "ForStatement",
1140
1480
  "ForInStatement",
1141
1481
  "ForOfStatement",
@@ -1145,7 +1485,7 @@ var highComplexityRule = createFunctionStatementCountRule({
1145
1485
  "CatchClause"
1146
1486
  ]),
1147
1487
  maxCount: 5,
1148
- unitLabel: "condicionais/loops"
1488
+ unitLabel: "condicionais/loops/tern\xE1rios"
1149
1489
  });
1150
1490
 
1151
1491
  // src/commands/high-complexity/high-complexity.command.ts
@@ -1216,13 +1556,16 @@ var isMathRandomCall = (node) => {
1216
1556
  return object?.type === "Identifier" && object.name === "Math" && property?.type === "Identifier" && property.name === "random";
1217
1557
  };
1218
1558
  var HASH_LIKE_NAME_PATTERN = /hash|md5|sha1/i;
1559
+ var memberPropertyName = (callee) => {
1560
+ const property = callee.property;
1561
+ return property?.type === "Identifier" ? property.name : void 0;
1562
+ };
1219
1563
  var calleeName = (callee) => {
1220
1564
  if (callee?.type === "Identifier") {
1221
1565
  return callee.name;
1222
1566
  }
1223
1567
  if (callee?.type === "MemberExpression") {
1224
- const property = callee.property;
1225
- return property?.type === "Identifier" ? property.name : void 0;
1568
+ return memberPropertyName(callee);
1226
1569
  }
1227
1570
  return void 0;
1228
1571
  };
@@ -1408,12 +1751,24 @@ var objectHasProperty = (object, propertyName) => {
1408
1751
  return key?.type === "Identifier" && key.name === propertyName;
1409
1752
  }) ?? false;
1410
1753
  };
1411
- var isJwtSignWithoutExpiration = (node) => {
1754
+ var getCallExpressionParts = (node) => {
1412
1755
  const callee = node.type === "CallExpression" ? node.callee : void 0;
1756
+ const args = node.type === "CallExpression" ? node.arguments : void 0;
1757
+ return { callee, args };
1758
+ };
1759
+ var getMemberExpressionParts = (callee) => {
1413
1760
  const object = callee?.type === "MemberExpression" ? callee.object : void 0;
1414
1761
  const property = callee?.type === "MemberExpression" ? callee.property : void 0;
1762
+ return { object, property };
1763
+ };
1764
+ var getJwtSignCallParts = (node) => {
1765
+ const { callee, args } = getCallExpressionParts(node);
1766
+ const { object, property } = getMemberExpressionParts(callee);
1767
+ return { object, property, args };
1768
+ };
1769
+ var isJwtSignWithoutExpiration = (node) => {
1770
+ const { object, property, args } = getJwtSignCallParts(node);
1415
1771
  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
1772
  const payload = args?.[0];
1418
1773
  const options = args?.[2];
1419
1774
  const hasExpInPayload = objectHasProperty(payload, "exp");
@@ -1686,12 +2041,26 @@ var isPermissiveCorsCall = (node) => {
1686
2041
  const args = node.arguments;
1687
2042
  return (args?.length ?? 0) === 0 || isWildcardOriginOption(args?.[0]);
1688
2043
  };
2044
+ var callExpressionParts = (node) => {
2045
+ if (node.type !== "CallExpression") {
2046
+ return void 0;
2047
+ }
2048
+ return {
2049
+ callee: node.callee,
2050
+ args: node.arguments
2051
+ };
2052
+ };
2053
+ var memberCalleeProperty = (callee) => {
2054
+ if (callee?.type !== "MemberExpression") {
2055
+ return void 0;
2056
+ }
2057
+ return callee.property;
2058
+ };
1689
2059
  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];
2060
+ const parts = callExpressionParts(node);
2061
+ const property = memberCalleeProperty(parts?.callee);
2062
+ const headerName = parts?.args?.[0];
2063
+ const headerValue = parts?.args?.[1];
1695
2064
  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
2065
  };
1697
2066
  var findPermissiveCorsLines = (filePath, content) => {
@@ -1845,13 +2214,26 @@ var awaitNoTryCatchRule = {
1845
2214
 
1846
2215
  // src/rules/floating-promise.rule.ts
1847
2216
  var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set(["all", "race", "allSettled", "any"]);
1848
- var asyncFunctionName = (node) => {
2217
+ var functionDeclarationName = (node) => {
1849
2218
  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);
2219
+ return declarationId?.type === "Identifier" ? declarationId.name : void 0;
2220
+ };
2221
+ var isAsyncFunctionValue = (init) => {
2222
+ return (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && init.async;
2223
+ };
2224
+ var asyncVariableDeclaratorId = (node) => {
2225
+ return node.type === "VariableDeclarator" ? node.id : void 0;
2226
+ };
2227
+ var asyncVariableDeclaratorInit = (node) => {
2228
+ return node.type === "VariableDeclarator" ? node.init : void 0;
2229
+ };
2230
+ var asyncVariableDeclaratorName = (node) => {
2231
+ const id = asyncVariableDeclaratorId(node);
2232
+ const init = asyncVariableDeclaratorInit(node);
2233
+ return id?.type === "Identifier" && isAsyncFunctionValue(init) ? id.name : void 0;
2234
+ };
2235
+ var asyncFunctionName = (node) => {
2236
+ return functionDeclarationName(node) ?? asyncVariableDeclaratorName(node);
1855
2237
  };
1856
2238
  var collectAsyncFunctionNames = (sourceFile) => {
1857
2239
  const names = /* @__PURE__ */ new Set();
@@ -1863,16 +2245,20 @@ var collectAsyncFunctionNames = (sourceFile) => {
1863
2245
  });
1864
2246
  return names;
1865
2247
  };
2248
+ var isKnownFunctionCall = (callee, asyncFunctionNames) => {
2249
+ return callee.type === "Identifier" && (callee.name === "fetch" || asyncFunctionNames.has(callee.name));
2250
+ };
2251
+ var isPromiseStaticMethodCall = (callee) => {
2252
+ const object = callee.type === "MemberExpression" ? callee.object : void 0;
2253
+ const property = callee.type === "MemberExpression" ? callee.property : void 0;
2254
+ return object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
2255
+ };
1866
2256
  var isKnownPromiseReturningCall = (call, asyncFunctionNames) => {
1867
2257
  const callee = call.callee;
1868
2258
  if (!callee) {
1869
2259
  return false;
1870
2260
  }
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;
2261
+ return isKnownFunctionCall(callee, asyncFunctionNames) || isPromiseStaticMethodCall(callee);
1876
2262
  };
1877
2263
  var findFloatingPromiseLines = (filePath, content) => {
1878
2264
  const lines = /* @__PURE__ */ new Set();
@@ -1902,7 +2288,7 @@ var floatingPromiseRule = {
1902
2288
  };
1903
2289
 
1904
2290
  // src/rules/promise-no-catch.rule.ts
1905
- var memberPropertyName = (member) => {
2291
+ var memberPropertyName2 = (member) => {
1906
2292
  const property = member.property;
1907
2293
  return property?.type === "Identifier" ? property.name : void 0;
1908
2294
  };
@@ -1915,7 +2301,11 @@ var isThenCall = (node) => {
1915
2301
  }
1916
2302
  const callee = node.callee;
1917
2303
  const args = node.arguments;
1918
- return callee?.type === "MemberExpression" && memberPropertyName(callee) === "then" && (args?.length ?? 0) < 2;
2304
+ return callee?.type === "MemberExpression" && memberPropertyName2(callee) === "then" && (args?.length ?? 0) < 2;
2305
+ };
2306
+ var chainMethodName = (currentCall, member, nextCall) => {
2307
+ const isChainMember = member !== void 0 && member.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
2308
+ return isChainMember ? memberPropertyName2(member) : void 0;
1919
2309
  };
1920
2310
  var chainReachesCatch = (thenCall, ancestors) => {
1921
2311
  let currentCall = thenCall;
@@ -1923,8 +2313,7 @@ var chainReachesCatch = (thenCall, ancestors) => {
1923
2313
  while (i < ancestors.length) {
1924
2314
  const member = ancestors.at(i);
1925
2315
  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;
2316
+ const methodName = chainMethodName(currentCall, member, nextCall);
1928
2317
  const continuesChain = methodName === "then" || methodName === "finally";
1929
2318
  if (methodName === "catch") {
1930
2319
  return true;
@@ -2166,10 +2555,10 @@ var tooManyForLoopsRule = createFunctionStatementCountRule({
2166
2555
  // src/rules/too-many-ifs.rule.ts
2167
2556
  var tooManyIfsRule = createFunctionStatementCountRule({
2168
2557
  id: "too-many-ifs",
2169
- description: 'Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")',
2170
- statementTypes: /* @__PURE__ */ new Set(["IfStatement"]),
2558
+ description: 'Detecta fun\xE7\xF5es com muitos "if" ou tern\xE1rios (incluindo "else if")',
2559
+ statementTypes: /* @__PURE__ */ new Set(["IfStatement", "ConditionalExpression"]),
2171
2560
  maxCount: 2,
2172
- unitLabel: "declara\xE7\xF5es if (incluindo else if)"
2561
+ unitLabel: "declara\xE7\xF5es if e express\xF5es tern\xE1rias (incluindo else if)"
2173
2562
  });
2174
2563
 
2175
2564
  // src/rules/too-many-switch-cases.rule.ts
@@ -2308,12 +2697,25 @@ var weakCipherModeRule = {
2308
2697
 
2309
2698
  // src/rules/weak-hash-algorithm.rule.ts
2310
2699
  var WEAK_ALGORITHMS = /^(md5|sha1)$/i;
2700
+ var getCallExpressionCallee = (node) => {
2701
+ return node.type === "CallExpression" ? node.callee : void 0;
2702
+ };
2703
+ var getCallExpressionArgs = (node) => {
2704
+ return node.type === "CallExpression" ? node.arguments : void 0;
2705
+ };
2706
+ var getCreateHashProperty = (callee) => {
2707
+ return callee?.type === "MemberExpression" ? callee.property : void 0;
2708
+ };
2709
+ var isCreateHashProperty = (property) => {
2710
+ return property?.type === "Identifier" && property.name === "createHash";
2711
+ };
2712
+ var isWeakAlgorithmLiteral = (algorithm) => {
2713
+ return algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
2714
+ };
2311
2715
  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);
2716
+ const property = getCreateHashProperty(getCallExpressionCallee(node));
2717
+ const algorithm = getCallExpressionArgs(node)?.[0];
2718
+ return isCreateHashProperty(property) && isWeakAlgorithmLiteral(algorithm);
2317
2719
  };
2318
2720
  var findWeakHashLines = (filePath, content) => {
2319
2721
  const lines = /* @__PURE__ */ new Set();
@@ -2548,7 +2950,10 @@ var parseLocalSemgrepConfig = (value) => {
2548
2950
  };
2549
2951
  var registerScanCommand = (program) => {
2550
2952
  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)
2953
+ program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig).option(
2954
+ "--no-deps",
2955
+ "n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev) neste scan \u2014 permite rodar offline"
2956
+ )
2552
2957
  ).action(
2553
2958
  (path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
2554
2959
  );
@@ -2649,6 +3054,118 @@ var registerUnsafeSqlCommand = (program) => {
2649
3054
  );
2650
3055
  };
2651
3056
 
3057
+ // src/engines/engine-metadata.ts
3058
+ import { readFileSync as readFileSync2 } from "fs";
3059
+ import { createRequire as createRequire4 } from "module";
3060
+ var require5 = createRequire4(import.meta.url);
3061
+ var packageForPlatform2 = (platform, architecture) => {
3062
+ if (platform === "linux" && architecture === "x64") {
3063
+ return "codesentry-semgrep-linux-x64";
3064
+ }
3065
+ if (platform === "win32" && architecture === "x64") {
3066
+ return "codesentry-semgrep-win32-x64";
3067
+ }
3068
+ return void 0;
3069
+ };
3070
+ 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.`;
3071
+ var readJsonFile = (path) => {
3072
+ return JSON.parse(readFileSync2(path, "utf-8"));
3073
+ };
3074
+ var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
3075
+ var errorReason2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
3076
+ var validRuntimeLock = (lock) => lock.schemaVersion === 1 && nonEmptyString(lock.semgrepVersion) && nonEmptyString(lock.pythonVersion) && /^[a-f0-9]{64}$/i.test(lock.runtimeSha256);
3077
+ 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));
3078
+ var loadBundledSemgrepRuntimeMetadata = (platform = process.platform, architecture = process.arch, resolveLockPath = (packageName) => require5.resolve(`${packageName}/runtime.lock.json`)) => {
3079
+ const packageName = packageForPlatform2(platform, architecture);
3080
+ if (!packageName) {
3081
+ throw new Error(unsupportedRuntimeMessage2(platform, architecture));
3082
+ }
3083
+ try {
3084
+ const lock = readJsonFile(resolveLockPath(packageName));
3085
+ if (!validRuntimeLock(lock)) {
3086
+ throw new Error("manifesto de vers\xE3o inv\xE1lido");
3087
+ }
3088
+ return {
3089
+ packageName,
3090
+ semgrepVersion: lock.semgrepVersion,
3091
+ pythonVersion: lock.pythonVersion,
3092
+ runtimeSha256: lock.runtimeSha256
3093
+ };
3094
+ } catch (error) {
3095
+ throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do runtime Semgrep embutido: ${errorReason2(error)}`, {
3096
+ cause: error
3097
+ });
3098
+ }
3099
+ };
3100
+ var loadBundledOwaspRulesetMetadata = (resolveLockPath = () => require5.resolve("codesentry-semgrep-rules/rules/ruleset.lock.json")) => {
3101
+ try {
3102
+ const lock = readJsonFile(resolveLockPath());
3103
+ if (!validRulesetLock(lock)) {
3104
+ throw new Error("manifesto de vers\xE3o inv\xE1lido");
3105
+ }
3106
+ return {
3107
+ packageVersion: lock.packageVersion,
3108
+ ruleset: lock.ruleset,
3109
+ source: lock.source,
3110
+ sha256: lock.sha256,
3111
+ capturedAt: lock.capturedAt,
3112
+ ...lock.upstreamRevision ? { upstreamRevision: lock.upstreamRevision } : {}
3113
+ };
3114
+ } catch (error) {
3115
+ throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do ruleset OWASP embutido: ${errorReason2(error)}`, {
3116
+ cause: error
3117
+ });
3118
+ }
3119
+ };
3120
+ var loadEngineMetadata = () => ({
3121
+ semgrep: loadBundledSemgrepRuntimeMetadata(),
3122
+ owaspRuleset: loadBundledOwaspRulesetMetadata()
3123
+ });
3124
+
3125
+ // src/commands/version/version.command.ts
3126
+ var buildVersionReport = (codesentryVersion, engines) => ({
3127
+ codesentryVersion,
3128
+ engines
3129
+ });
3130
+ var formatEngineVersions = (report) => {
3131
+ const { semgrep, owaspRuleset } = report.engines;
3132
+ return [
3133
+ `CodeSentry: ${report.codesentryVersion}`,
3134
+ "",
3135
+ "Runtime Semgrep CE",
3136
+ ` Vers\xE3o: ${semgrep.semgrepVersion}`,
3137
+ ` Python: ${semgrep.pythonVersion}`,
3138
+ ` Pacote: ${semgrep.packageName}`,
3139
+ ` SHA-256: ${semgrep.runtimeSha256}`,
3140
+ "",
3141
+ "Ruleset OWASP",
3142
+ ` Snapshot: ${owaspRuleset.ruleset}`,
3143
+ ` Vers\xE3o do pacote: ${owaspRuleset.packageVersion}`,
3144
+ ` Origem: ${owaspRuleset.source}`,
3145
+ ` Capturado em: ${owaspRuleset.capturedAt}`,
3146
+ ...owaspRuleset.upstreamRevision ? [` Revis\xE3o upstream: ${owaspRuleset.upstreamRevision}`] : [],
3147
+ ` SHA-256: ${owaspRuleset.sha256}`
3148
+ ].join("\n");
3149
+ };
3150
+ var errorReason3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
3151
+ var formatVersionOutput = (options, report) => options.json ? JSON.stringify(report, null, 2) : formatEngineVersions(report);
3152
+ var registerVersionCommand = (program, readPackageVersion2, readEngineMetadata = loadEngineMetadata) => {
3153
+ 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) => {
3154
+ const codesentryVersion = readPackageVersion2();
3155
+ if (!options.engines && !options.json) {
3156
+ console.log(`CodeSentry: ${codesentryVersion}`);
3157
+ return;
3158
+ }
3159
+ try {
3160
+ const report = buildVersionReport(codesentryVersion, readEngineMetadata());
3161
+ console.log(formatVersionOutput(options, report));
3162
+ } catch (error) {
3163
+ process.exitCode = 1;
3164
+ console.error(`N\xE3o foi poss\xEDvel auditar as vers\xF5es dos motores: ${errorReason3(error)}`);
3165
+ }
3166
+ });
3167
+ };
3168
+
2652
3169
  // src/commands/weak-cipher-mode/weak-cipher-mode.command.ts
2653
3170
  var registerWeakCipherModeCommand = (program) => {
2654
3171
  withScanOptions(
@@ -2744,16 +3261,17 @@ var registerSecurityCommands = (program) => {
2744
3261
  registerWeakCipherModeCommand(program);
2745
3262
  registerHardcodedAuthorizationValueCommand(program);
2746
3263
  };
2747
- var require5 = createRequire4(import.meta.url);
3264
+ var require6 = createRequire5(import.meta.url);
2748
3265
  var readPackageVersion = () => {
2749
- const packageJsonPath = require5.resolve("../package.json");
2750
- const { version } = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
3266
+ const packageJsonPath = require6.resolve("../package.json");
3267
+ const { version } = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
2751
3268
  return version;
2752
3269
  };
2753
3270
  var createCli = () => {
2754
3271
  printBanner();
2755
3272
  const program = new Command().name("codesentry").description("CLI de verifica\xE7\xE3o de vulnerabilidades e qualidade de c\xF3digo").version(readPackageVersion()).helpCommand(false);
2756
3273
  registerInitCommand(program);
3274
+ registerVersionCommand(program, readPackageVersion);
2757
3275
  registerAnalysisCommands(program);
2758
3276
  registerQualityCommands(program);
2759
3277
  registerSecurityCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesentry",
3
- "version": "0.1.11",
3
+ "version": "0.2.1",
4
4
  "workspaces": [
5
5
  "packages/semgrep-rules"
6
6
  ],
@@ -48,7 +48,7 @@
48
48
  "@clack/prompts": "^1.7.0",
49
49
  "chalk": "^6.0.0",
50
50
  "cli-table3": "^0.6.5",
51
- "codesentry-semgrep-rules": "0.1.11",
51
+ "codesentry-semgrep-rules": "0.2.1",
52
52
  "commander": "^15.0.0",
53
53
  "eslint": "^10.10.0",
54
54
  "eslint-plugin-no-unsanitized": "^4.1.5",
@@ -59,8 +59,8 @@
59
59
  "p-limit": "^7.3.2"
60
60
  },
61
61
  "optionalDependencies": {
62
- "codesentry-semgrep-linux-x64": "0.1.11",
63
- "codesentry-semgrep-win32-x64": "0.1.11"
62
+ "codesentry-semgrep-linux-x64": "0.2.1",
63
+ "codesentry-semgrep-win32-x64": "0.2.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^26.4.1",
@@ -70,5 +70,8 @@
70
70
  "tsx": "^4.23.13",
71
71
  "typescript": "^7.0.2",
72
72
  "vitest": "^5.0.0"
73
+ },
74
+ "overrides": {
75
+ "esbuild": "^0.28.2"
73
76
  }
74
77
  }