codesentry 0.1.9 → 0.1.10

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 +93 -18
  2. package/dist/index.js +210 -200
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,9 +1,19 @@
1
+ <p align="center">
2
+ <img src="docs/assets/logo.png" alt="Logo do CodeSentry" width="640">
3
+ </p>
4
+
1
5
  # CodeSentry
2
6
 
3
7
  CLI de verificação de vulnerabilidades e qualidade de código. O scanner
4
8
  combina regras próprias para JavaScript/TypeScript e o ruleset OWASP do
5
9
  Semgrep CE para as linguagens suportadas por ele.
6
10
 
11
+ - 🔎 **Dois motores num só comando** — regras próprias em TS/JS + Semgrep CE (OWASP Top 10) para dezenas de outras linguagens.
12
+ - 📦 **Uma instalação, zero fricção** — `npm install -g codesentry` e pronto: sem Python, Docker, Semgrep ou conta em lugar nenhum.
13
+ - 🔌 **100% offline depois de instalado** — nunca consulta a Semgrep Registry nem envia métricas.
14
+ - 🪟🐧 **Windows e Linux nativamente** — sem WSL, sem container.
15
+ - 📊 **Console, JSON ou Markdown** — saída pronta tanto para ler no terminal quanto para plugar em CI.
16
+
7
17
  ## O que o CodeSentry faz
8
18
 
9
19
  Rodando `codesentry scan` num projeto, dois motores de análise trabalham
@@ -25,6 +35,22 @@ motor encontrou o problema (prefixo `semgrep/` para achados do Semgrep).
25
35
  Saídas disponíveis: tabela no console, JSON (`--json`) e, quando há mais
26
36
  de 20 problemas, um relatório Markdown detalhado é gerado automaticamente.
27
37
 
38
+ Exemplo de saída no console:
39
+
40
+ ```text
41
+ ❯ codesentry scan .
42
+ ✔ Scanning files...
43
+ ┌──────────┬──────────────────────┬─────────────────┬──────┬──────────────────────────────────────────┐
44
+ │ Severity │ Rule │ File │ Line │ Message │
45
+ ├──────────┼──────────────────────┼─────────────────┼──────┼──────────────────────────────────────────┤
46
+ │ high │ no-hardcoded-secret │ src/config.ts │ 12 │ Possível segredo hardcoded na variável... │
47
+ │ high │ semgrep/...shell-true│ scripts/run.py │ 8 │ subprocess com shell=True é perigoso... │
48
+ │ medium │ jwt-no-expiration │ src/auth.ts │ 34 │ Token JWT assinado sem "expiresIn"... │
49
+ └──────────┴──────────────────────┴─────────────────┴──────┴──────────────────────────────────────────┘
50
+
51
+ 3 problema(s) encontrado(s) em 87 arquivo(s) (4213ms). CodeSentry: 87 JS/TS; Semgrep: 64 arquivo(s).
52
+ ```
53
+
28
54
  ## Como funciona por baixo dos panos
29
55
 
30
56
  O ponto central do design é **zero fricção de instalação**: o usuário final
@@ -42,20 +68,43 @@ O racional completo está na [ADR 0004](docs/adr/0004-bundled-semgrep-runtime.md
42
68
 
43
69
  ## Instalação
44
70
 
45
- Em uma release publicada, basta uma instalação:
71
+ Pré-requisito único: [Node.js](https://nodejs.org) 22.12 ou mais recente
72
+ (`node --version` para conferir). Nenhum outro requisito — não precisa
73
+ instalar Python, Docker, Semgrep, criar conta ou autenticar em nada.
74
+
75
+ ### Windows
76
+
77
+ No PowerShell ou no Prompt de Comando (não precisa de WSL):
78
+
79
+ ```powershell
80
+ npm install -g codesentry
81
+ codesentry scan .
82
+ ```
83
+
84
+ ### Linux
46
85
 
47
86
  ```bash
48
87
  npm install -g codesentry
88
+ codesentry scan .
49
89
  ```
50
90
 
51
- O pacote compatível de Semgrep CE e Python portátil é instalado como
52
- dependência opcional automaticamente. Não é necessário instalar Python,
53
- Docker, Semgrep, criar conta ou autenticar em um site.
91
+ Em ambos os casos, o `npm install` resolve automaticamente o pacote de
92
+ runtime compatível com a sua plataforma (Semgrep CE + Python portátil) como
93
+ dependência opcional é isso que faz `codesentry scan` funcionar com
94
+ cobertura OWASP completa sem nenhuma instalação manual. Plataformas com
95
+ runtime publicado hoje: Linux x64 e Windows x64. Os pacotes têm dezenas de
96
+ MB porque incluem esse runtime embutido; essa é a troca para o scan
97
+ funcionar 100% offline depois de instalado.
98
+
99
+ Se `codesentry` não for encontrado no terminal depois de instalado
100
+ globalmente, feche e reabra o terminal (ou rode `npx codesentry scan .`) —
101
+ alguns terminais não recarregam o PATH do npm automaticamente na mesma
102
+ sessão.
54
103
 
55
- As plataformas inicialmente suportadas são Linux x64 e Windows x64. Os
56
- pacotes têm tamanho de dezenas de MB porque incluem o runtime; essa é a troca
57
- para o scan funcionar offline após a instalação. Durante o desenvolvimento,
58
- use `npm link`:
104
+ ### A partir do código-fonte (desenvolvimento)
105
+
106
+ Para rodar a partir do repositório clonado, em vez do pacote publicado, use
107
+ `npm link`:
59
108
 
60
109
  ```bash
61
110
  git clone git@github.com:Ivan-ReisDev/code-sentry.git
@@ -68,6 +117,16 @@ npm link
68
117
  Depois disso, o comando `codesentry` fica disponível em qualquer
69
118
  diretório do seu terminal.
70
119
 
120
+ > **Atenção:** `npx codesentry` rodado de dentro deste repositório clonado
121
+ > executa o `dist/index.js` local (o `package.json` daqui se chama
122
+ > `codesentry`, e o `npx` prioriza isso sobre a instalação global/publicada).
123
+ > O workspace de desenvolvimento sempre tem o ruleset do Semgrep vazio por
124
+ > design (populado só via `npm run prepare:owasp-rules` ou durante a release —
125
+ > ver [ADR 0004](docs/adr/0004-bundled-semgrep-runtime.md)), então o scan
126
+ > roda sem erro mas sempre reporta zero arquivos analisados pelo Semgrep.
127
+ > Para testar o pacote publicado de verdade, rode `codesentry scan` (sem
128
+ > `npx`) fora deste diretório.
129
+
71
130
  ## Uso
72
131
 
73
132
  ### `codesentry scan [path]`
@@ -99,31 +158,47 @@ explicitamente em vez de declarar uma análise parcial como completa.
99
158
 
100
159
  ### `codesentry rules`
101
160
 
102
- Lista as regras de análise disponíveis.
161
+ Lista as regras de análise disponíveis (id e descrição de cada uma).
103
162
 
104
163
  ```bash
105
164
  codesentry rules
106
165
  ```
107
166
 
108
- ### `codesentry long-functions [path]`
167
+ ### `codesentry help`
109
168
 
110
- Analisa um diretório (padrão: diretório atual) em busca apenas de
111
- funções com mais de 30 linhas (severidade `low`). Essa mesma regra
112
- também roda automaticamente como parte do `codesentry scan`.
169
+ Lista **todos** os comandos disponíveis, sempre atualizada inclui tanto
170
+ os comandos gerais quanto os individuais listados a seguir.
113
171
 
114
172
  ```bash
115
- codesentry long-functions .
116
- codesentry long-functions ./src --json
173
+ codesentry help
117
174
  ```
118
175
 
119
- ### `codesentry help`
176
+ ### Comandos individuais por regra
120
177
 
121
- Lista os comandos disponíveis.
178
+ Cada regra nativa também tem um comando próprio, que roda **só ela** sobre
179
+ um diretório — útil para focar em um tipo de problema específico sem esperar
180
+ o scan completo (e sem o Semgrep, que só roda como parte de `scan`). Todos
181
+ seguem o mesmo formato:
122
182
 
123
183
  ```bash
124
- codesentry help
184
+ codesentry <comando> [path] [--json]
185
+ ```
186
+
187
+ Alguns exemplos:
188
+
189
+ ```bash
190
+ codesentry long-functions . # funções com mais de 30 linhas
191
+ codesentry no-eval ./src # uso de eval()
192
+ codesentry xss ./src --json # possíveis XSS (innerHTML, document.write, dangerouslySetInnerHTML)
193
+ codesentry unsafe-sql ./src # SQL injection por concatenação
194
+ codesentry command-injection ./src # child_process com entrada não sanitizada
195
+ codesentry weak-hash-algorithm ./src # uso de MD5/SHA-1 para hashing sensível
196
+ codesentry dependency-audit . # `npm audit` das dependências do projeto
125
197
  ```
126
198
 
199
+ A lista completa (30+ comandos, um por regra) sai de `codesentry help` —
200
+ mantê-la sempre em sincronia aqui manualmente não seria viável.
201
+
127
202
  ### `codesentry init`
128
203
 
129
204
  Assistente interativo para configurar o CodeSentry no projeto atual.
package/dist/index.js CHANGED
@@ -36,38 +36,33 @@ var visitSourceNodes = (node, visitor, parent) => {
36
36
  var EXEC_METHOD_NAMES = /* @__PURE__ */ new Set(["exec", "execSync"]);
37
37
  var CHILD_PROCESS_MODULE_NAMES = /* @__PURE__ */ new Set(["child_process", "node:child_process"]);
38
38
  var isChildProcessModuleSpecifier = (node) => node?.type === "StringLiteral" && CHILD_PROCESS_MODULE_NAMES.has(node.value);
39
+ var localName = (specifier) => {
40
+ const local = specifier.local;
41
+ return local?.type === "Identifier" ? local.name : void 0;
42
+ };
43
+ var isExecImport = (specifier) => {
44
+ const imported = specifier.imported;
45
+ return specifier.type === "ImportSpecifier" && imported?.type === "Identifier" && EXEC_METHOD_NAMES.has(imported.name);
46
+ };
47
+ var isNamespaceImport = (specifier) => specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier";
48
+ var collectImportBinding = (specifier, bindings) => {
49
+ const name = localName(specifier);
50
+ if (!name) {
51
+ return;
52
+ }
53
+ const bindingSet = isExecImport(specifier) ? bindings.directCalls : isNamespaceImport(specifier) ? bindings.namespaces : void 0;
54
+ bindingSet?.add(name);
55
+ };
39
56
  var collectFromImportDeclaration = (node, bindings) => {
40
57
  if (!isChildProcessModuleSpecifier(node.source)) {
41
58
  return;
42
59
  }
43
60
  for (const specifier of node.specifiers ?? []) {
44
- const local = specifier.local;
45
- if (local?.type !== "Identifier") {
46
- continue;
47
- }
48
- if (specifier.type === "ImportSpecifier") {
49
- const imported = specifier.imported;
50
- if (imported?.type === "Identifier" && EXEC_METHOD_NAMES.has(imported.name)) {
51
- bindings.directCalls.add(local.name);
52
- }
53
- } else if (specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") {
54
- bindings.namespaces.add(local.name);
55
- }
61
+ collectImportBinding(specifier, bindings);
56
62
  }
57
63
  };
58
64
  var isRequireCall = (node) => node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "require" && isChildProcessModuleSpecifier(node.arguments?.[0]);
59
- var collectFromVariableDeclarator = (node, bindings) => {
60
- if (!isRequireCall(node.init)) {
61
- return;
62
- }
63
- const id = node.id;
64
- if (id?.type === "Identifier") {
65
- bindings.namespaces.add(id.name);
66
- return;
67
- }
68
- if (id?.type !== "ObjectPattern") {
69
- return;
70
- }
65
+ var collectDirectCallBindings = (id, bindings) => {
71
66
  for (const property of id.properties ?? []) {
72
67
  const key = property.key;
73
68
  const value = property.value;
@@ -76,6 +71,16 @@ var collectFromVariableDeclarator = (node, bindings) => {
76
71
  }
77
72
  }
78
73
  };
74
+ var collectFromVariableDeclarator = (node, bindings) => {
75
+ if (!isRequireCall(node.init)) {
76
+ return;
77
+ }
78
+ const id = node.id;
79
+ const namespaceName = id?.type === "Identifier" ? id.name : void 0;
80
+ const destructuredBindings = id?.type === "ObjectPattern" ? id : void 0;
81
+ namespaceName && bindings.namespaces.add(namespaceName);
82
+ destructuredBindings && collectDirectCallBindings(destructuredBindings, bindings);
83
+ };
79
84
  var collectChildProcessBindings = (sourceFile) => {
80
85
  const bindings = { directCalls: /* @__PURE__ */ new Set(), namespaces: /* @__PURE__ */ new Set() };
81
86
  visitSourceNodes(sourceFile, (node) => {
@@ -170,17 +175,12 @@ var SEVERITY_COLOR = {
170
175
  high: (text2) => chalk.red(text2),
171
176
  critical: (text2) => chalk.bgRed.white(text2)
172
177
  };
173
- var printConsoleReport = (result) => {
174
- const coverage = result.engines?.semgrep === void 0 ? "" : ` CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s).`;
175
- if (result.findings.length === 0) {
176
- console.log(
177
- chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`)
178
- );
179
- return;
180
- }
181
- const table = new Table({
182
- head: ["Severity", "Rule", "File", "Line", "Message"]
183
- });
178
+ var coverageText = (result) => result.engines?.semgrep === void 0 ? "" : ` CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s).`;
179
+ var printCleanReport = (result, coverage) => {
180
+ console.log(chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`));
181
+ };
182
+ var findingsTable = (result) => {
183
+ const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
184
184
  for (const finding of result.findings) {
185
185
  const colorize = SEVERITY_COLOR[finding.severity];
186
186
  table.push([
@@ -191,7 +191,10 @@ var printConsoleReport = (result) => {
191
191
  finding.message
192
192
  ]);
193
193
  }
194
- console.log(table.toString());
194
+ return table;
195
+ };
196
+ var printFindingsReport = (result, coverage) => {
197
+ console.log(findingsTable(result).toString());
195
198
  console.log(
196
199
  chalk.bold(
197
200
  `
@@ -199,6 +202,14 @@ ${result.findings.length} problema(s) encontrado(s) em ${result.scannedFiles} ar
199
202
  )
200
203
  );
201
204
  };
205
+ var printConsoleReport = (result) => {
206
+ const coverage = coverageText(result);
207
+ if (result.findings.length === 0) {
208
+ printCleanReport(result, coverage);
209
+ return;
210
+ }
211
+ printFindingsReport(result, coverage);
212
+ };
202
213
 
203
214
  // src/reporters/json.reporter.ts
204
215
  var toJsonReport = (result) => {
@@ -207,12 +218,13 @@ var toJsonReport = (result) => {
207
218
 
208
219
  // src/reporters/markdown.reporter.ts
209
220
  var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
210
- var SEVERITY_LABEL = {
211
- critical: "Critical",
212
- high: "High",
213
- medium: "Medium",
214
- low: "Low"
215
- };
221
+ var SEVERITY_LABEL = /* @__PURE__ */ new Map([
222
+ ["critical", "Critical"],
223
+ ["high", "High"],
224
+ ["medium", "Medium"],
225
+ ["low", "Low"]
226
+ ]);
227
+ var severityLabel = (severity) => SEVERITY_LABEL.get(severity) ?? severity;
216
228
  var escapeCell = (text2) => text2.replaceAll("|", "\\|");
217
229
  var groupBy = (items, keyOf) => {
218
230
  const map = /* @__PURE__ */ new Map();
@@ -222,7 +234,7 @@ var groupBy = (items, keyOf) => {
222
234
  }
223
235
  return map;
224
236
  };
225
- var findingsTable = (findings) => {
237
+ var findingsTable2 = (findings) => {
226
238
  const sorted = [...findings].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
227
239
  const lines = ["| Arquivo | Linha | Mensagem |", "| --- | --- | --- |"];
228
240
  for (const f of sorted) {
@@ -231,11 +243,11 @@ var findingsTable = (findings) => {
231
243
  return lines;
232
244
  };
233
245
  var severitySection = (severity, findings) => {
234
- const lines = [`## ${SEVERITY_LABEL[severity]} (${findings.length})`, ""];
246
+ const lines = [`## ${severityLabel(severity)} (${findings.length})`, ""];
235
247
  const byRule = groupBy(findings, (f) => f.ruleId);
236
248
  for (const ruleId of [...byRule.keys()].sort()) {
237
249
  const ruleFindings = byRule.get(ruleId) ?? [];
238
- lines.push(`### ${ruleId} (${ruleFindings.length})`, "", ...findingsTable(ruleFindings), "");
250
+ lines.push(`### ${ruleId} (${ruleFindings.length})`, "", ...findingsTable2(ruleFindings), "");
239
251
  }
240
252
  return lines;
241
253
  };
@@ -254,7 +266,7 @@ var reportHeader = (result, generatedAt) => [
254
266
  var summaryTable = (bySeverity) => {
255
267
  const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
256
268
  for (const severity of SEVERITY_ORDER) {
257
- lines.push(`| ${SEVERITY_LABEL[severity]} | ${(bySeverity.get(severity) ?? []).length} |`);
269
+ lines.push(`| ${severityLabel(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
258
270
  }
259
271
  lines.push("");
260
272
  return lines;
@@ -295,20 +307,21 @@ import { availableParallelism } from "os";
295
307
  import { readdir } from "fs/promises";
296
308
  import { join } from "path";
297
309
  var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
298
- var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next"]);
310
+ var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "tests"]);
299
311
  var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
312
+ var filesFromEntry = async (currentDir, entry) => {
313
+ if (entry.isDirectory()) {
314
+ return IGNORED_DIRS.has(entry.name) ? [] : walk(join(currentDir, entry.name));
315
+ }
316
+ return entry.isFile() && isScannable(entry.name) ? [join(currentDir, entry.name)] : [];
317
+ };
300
318
  var walk = async (currentDir) => {
301
- const entries = await readdir(currentDir, { withFileTypes: true });
302
- const files = [];
303
- for (const entry of entries) {
304
- if (entry.isDirectory()) {
305
- if (IGNORED_DIRS.has(entry.name)) continue;
306
- files.push(...await walk(join(currentDir, entry.name)));
307
- } else if (entry.isFile() && isScannable(entry.name)) {
308
- files.push(join(currentDir, entry.name));
309
- }
319
+ try {
320
+ const entries = await readdir(currentDir, { withFileTypes: true });
321
+ return (await Promise.all(entries.map((entry) => filesFromEntry(currentDir, entry)))).flat();
322
+ } catch (error) {
323
+ throw new Error(`N\xE3o foi poss\xEDvel listar os arquivos em "${currentDir}".`, { cause: error });
310
324
  }
311
- return files;
312
325
  };
313
326
  var findFiles = async (targetDir) => {
314
327
  try {
@@ -342,21 +355,30 @@ var readFileContent = async (filePath) => {
342
355
  throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
343
356
  }
344
357
  };
358
+ var checkRule = (rule, filePath, content) => {
359
+ try {
360
+ return { findings: rule.check(filePath, content) };
361
+ } catch (error) {
362
+ return { findings: [], error };
363
+ }
364
+ };
345
365
  var scanFile = async (filePath, rules) => {
346
- const content = await readFileContent(filePath);
347
- const findings = [];
348
- let hasParseError = false;
349
- for (const rule of rules) {
350
- try {
351
- findings.push(...rule.check(filePath, content));
352
- } catch (error) {
353
- if (!hasParseError) {
354
- findings.push(parseErrorFinding(filePath, error));
366
+ try {
367
+ const content = await readFileContent(filePath);
368
+ const findings = [];
369
+ let hasParseError = false;
370
+ for (const rule of rules) {
371
+ const result = checkRule(rule, filePath, content);
372
+ findings.push(...result.findings);
373
+ if (result.error && !hasParseError) {
374
+ findings.push(parseErrorFinding(filePath, result.error));
355
375
  hasParseError = true;
356
376
  }
357
377
  }
378
+ return findings;
379
+ } catch (error) {
380
+ throw new Error(`N\xE3o foi poss\xEDvel analisar o arquivo "${filePath}".`, { cause: error });
358
381
  }
359
- return findings;
360
382
  };
361
383
  var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY) => {
362
384
  try {
@@ -460,51 +482,65 @@ var mapSemgrepReportToFindings = (report) => report.results.map((finding) => ({
460
482
  severity: SEMGREP_SEVERITIES[finding.extra.severity] ?? "medium"
461
483
  }));
462
484
  var outputFromError = (error) => typeof error.stdout === "string" ? error.stdout : void 0;
463
- var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync) => {
464
- const startedAt = Date.now();
465
- const args = [
466
- "scan",
467
- "--config",
468
- ruleset,
469
- "--metrics=off",
470
- "--json",
471
- "--quiet",
472
- "--exclude",
473
- "node_modules",
474
- "--exclude",
475
- ".git",
476
- "--exclude",
477
- "dist",
478
- "--exclude",
479
- ".next",
480
- // Não repetir targetDir aqui: o processo já roda com cwd = targetDir
481
- // (abaixo), então o alvo relativo a esse cwd é o diretório atual.
482
- "."
483
- ];
484
- let stdout;
485
+ var semgrepArgs = (ruleset) => [
486
+ "scan",
487
+ "--config",
488
+ ruleset,
489
+ "--metrics=off",
490
+ "--json",
491
+ "--quiet",
492
+ "--exclude",
493
+ "node_modules",
494
+ "--exclude",
495
+ ".git",
496
+ "--exclude",
497
+ "dist",
498
+ "--exclude",
499
+ ".next",
500
+ "--exclude",
501
+ "tests",
502
+ // Não repetir targetDir aqui: o processo já roda com cwd = targetDir,
503
+ // então o alvo relativo a esse cwd é o diretório atual.
504
+ "."
505
+ ];
506
+ var semgrepEnvironment = (runtime) => {
485
507
  const semgrepDir = dirname2(runtime.semgrep);
486
- const SYSTEM_PATH_FALLBACK = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
487
- const env = {
508
+ const systemPathFallback = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
509
+ return {
488
510
  ...process.env,
489
- PATH: `${semgrepDir}${delimiter}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${SYSTEM_PATH_FALLBACK}`
511
+ PATH: `${semgrepDir}${delimiter}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
490
512
  };
513
+ };
514
+ var executeSemgrep = async (targetDir, runtime, ruleset, execute) => {
491
515
  try {
492
- ({ stdout } = await execute(runtime.semgrep, args, { cwd: targetDir, maxBuffer: 20 * 1024 * 1024, env }));
516
+ const { stdout } = await execute(runtime.semgrep, semgrepArgs(ruleset), {
517
+ cwd: targetDir,
518
+ maxBuffer: 20 * 1024 * 1024,
519
+ env: semgrepEnvironment(runtime)
520
+ });
521
+ return stdout;
493
522
  } catch (error) {
494
523
  const output = outputFromError(error);
495
- if (!output) {
496
- throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
524
+ if (output) {
525
+ return output;
497
526
  }
498
- stdout = output;
527
+ throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
528
+ }
529
+ };
530
+ var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync) => {
531
+ const startedAt = Date.now();
532
+ try {
533
+ const report = parseSemgrepReport(await executeSemgrep(targetDir, runtime, ruleset, execute));
534
+ const scannedFiles = report.paths?.scanned.length ?? 0;
535
+ return {
536
+ scannedFiles,
537
+ findings: mapSemgrepReportToFindings(report),
538
+ durationMs: Date.now() - startedAt,
539
+ engines: { semgrep: scannedFiles }
540
+ };
541
+ } catch (error) {
542
+ throw new Error("N\xE3o foi poss\xEDvel processar o resultado do Semgrep.", { cause: error });
499
543
  }
500
- const report = parseSemgrepReport(stdout);
501
- const scannedFiles = report.paths?.scanned.length ?? 0;
502
- return {
503
- scannedFiles,
504
- findings: mapSemgrepReportToFindings(report),
505
- durationMs: Date.now() - startedAt,
506
- engines: { semgrep: scannedFiles }
507
- };
508
544
  };
509
545
 
510
546
  // src/commands/scan/report-filename.ts
@@ -540,22 +576,28 @@ Relat\xF3rio detalhado gerado em: ${filePath}`));
540
576
  console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage2(error)}`));
541
577
  }
542
578
  };
579
+ var runScanEngines = async (path, rules, options) => {
580
+ try {
581
+ const nativeResult = await runScan(path, rules, options.concurrency);
582
+ if (!options.semgrep) {
583
+ return nativeResult;
584
+ }
585
+ const semgrepResult = await runBundledSemgrep(path, void 0, options.config);
586
+ return mergeScanResults(nativeResult, semgrepResult);
587
+ } catch (error) {
588
+ throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, { cause: error });
589
+ }
590
+ };
543
591
  var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
544
592
  {
545
593
  title: taskTitle,
546
- task: () => runScan(path, rules, options.concurrency).then((nativeResult) => {
547
- if (!options.semgrep) {
548
- onResult(nativeResult);
549
- return void 0;
594
+ task: async () => {
595
+ try {
596
+ onResult(await runScanEngines(path, rules, options));
597
+ } catch (error) {
598
+ throw error;
550
599
  }
551
- return runBundledSemgrep(path, void 0, options.config).then((semgrepResult) => {
552
- onResult(mergeScanResults(nativeResult, semgrepResult));
553
- });
554
- }).catch((error) => {
555
- throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, {
556
- cause: error
557
- });
558
- })
600
+ }
559
601
  }
560
602
  ]);
561
603
  var scanAndReport = async (path, rules, taskTitle, options) => {
@@ -884,14 +926,14 @@ var nodeName = (node) => {
884
926
  }
885
927
  return nameValue(node) ?? literalValue(node) ?? nodeName(node.id);
886
928
  };
887
- var PARENT_NAME_KEYS = {
888
- VariableDeclarator: "id",
889
- ObjectProperty: "key",
890
- AssignmentExpression: "left"
891
- };
929
+ var PARENT_NAME_READERS = /* @__PURE__ */ new Map([
930
+ ["VariableDeclarator", (parent) => parent.id],
931
+ ["ObjectProperty", (parent) => parent.key],
932
+ ["AssignmentExpression", (parent) => parent.left]
933
+ ]);
892
934
  var parentFunctionName = (parent) => {
893
- const nameKey = parent ? PARENT_NAME_KEYS[parent.type] : void 0;
894
- return nameKey ? nodeName(parent?.[nameKey]) : void 0;
935
+ const readName = parent ? PARENT_NAME_READERS.get(parent.type) : void 0;
936
+ return readName && parent ? nodeName(readName(parent)) : void 0;
895
937
  };
896
938
  var isConstructor = (node) => {
897
939
  return node.type === "ClassMethod" && node.kind === "constructor";
@@ -1073,20 +1115,11 @@ var calleeName = (callee) => {
1073
1115
  return void 0;
1074
1116
  };
1075
1117
  var isDateNowOrGetTimeCall = (node) => {
1076
- if (node.type !== "CallExpression") {
1077
- return false;
1078
- }
1079
1118
  const callee = node.callee;
1080
- if (callee?.type !== "MemberExpression") {
1081
- return false;
1082
- }
1083
- const property = callee.property;
1119
+ const property = callee?.property;
1084
1120
  const propertyName = property?.type === "Identifier" ? property.name : void 0;
1085
- if (propertyName === "getTime") {
1086
- return true;
1087
- }
1088
- const object = callee.object;
1089
- return object?.type === "Identifier" && object.name === "Date" && propertyName === "now";
1121
+ const object = callee?.object;
1122
+ return node.type === "CallExpression" && callee?.type === "MemberExpression" && (propertyName === "getTime" || object?.type === "Identifier" && object.name === "Date" && propertyName === "now");
1090
1123
  };
1091
1124
  var TIMESTAMP_NAME_PATTERN = /timestamp|^now$/i;
1092
1125
  var containsPredictableTimestamp = (node) => {
@@ -1208,13 +1241,12 @@ var analyzeFile = (sourceFile) => {
1208
1241
  hasSignatureVerification: false
1209
1242
  };
1210
1243
  visitSourceNodes(sourceFile, (node) => {
1211
- if (isBase64DecodeCall(node) && node.loc) {
1212
- analysis.base64DecodeLines.push(node.loc.start.line);
1213
- } else if (isJsonParseCall(node)) {
1214
- analysis.hasJsonParse = true;
1215
- } else if (isSignatureVerificationCall(node)) {
1216
- analysis.hasSignatureVerification = true;
1244
+ const base64DecodeLine = isBase64DecodeCall(node) ? node.loc?.start.line : void 0;
1245
+ if (base64DecodeLine !== void 0) {
1246
+ analysis.base64DecodeLines.push(base64DecodeLine);
1217
1247
  }
1248
+ analysis.hasJsonParse ||= isJsonParseCall(node);
1249
+ analysis.hasSignatureVerification ||= isSignatureVerificationCall(node);
1218
1250
  });
1219
1251
  return analysis;
1220
1252
  };
@@ -1242,14 +1274,14 @@ var jwtDecodeWithoutVerifyRule = {
1242
1274
 
1243
1275
  // src/commands/jwt-decode-without-verify/jwt-decode-without-verify.command.ts
1244
1276
  var registerJwtDecodeWithoutVerifyCommand = (program) => {
1245
- program.command("jwt-decode-without-verify").description("Detecta decodifica\xE7\xE3o manual de um token (base64 + JSON.parse) sem verificar a assinatura").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
1246
- await scanAndReport(
1277
+ program.command("jwt-decode-without-verify").description("Detecta decodifica\xE7\xE3o manual de um token (base64 + JSON.parse) sem verificar a assinatura").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
1278
+ (path, options) => scanAndReport(
1247
1279
  path,
1248
1280
  [jwtDecodeWithoutVerifyRule],
1249
1281
  "Checking JWT decode without verification...",
1250
1282
  options
1251
- );
1252
- });
1283
+ )
1284
+ );
1253
1285
  };
1254
1286
 
1255
1287
  // src/rules/jwt-no-expiration.rule.ts
@@ -1610,9 +1642,9 @@ var publicEnvVarSecretRule = {
1610
1642
 
1611
1643
  // src/commands/public-env-var-secret/public-env-var-secret.command.ts
1612
1644
  var registerPublicEnvVarSecretCommand = (program) => {
1613
- program.command("public-env-var-secret").description("Detecta uma vari\xE1vel de ambiente p\xFAblica (NEXT_PUBLIC_/VITE_/REACT_APP_) com nome de segredo").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
1614
- await scanAndReport(path, [publicEnvVarSecretRule], "Checking public env var secrets...", options);
1615
- });
1645
+ program.command("public-env-var-secret").description("Detecta uma vari\xE1vel de ambiente p\xFAblica (NEXT_PUBLIC_/VITE_/REACT_APP_) com nome de segredo").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
1646
+ (path, options) => scanAndReport(path, [publicEnvVarSecretRule], "Checking public env var secrets...", options)
1647
+ );
1616
1648
  };
1617
1649
 
1618
1650
  // src/commands/rules/rules.command.ts
@@ -1760,15 +1792,15 @@ var chainReachesCatch = (thenCall, ancestors) => {
1760
1792
  let currentCall = thenCall;
1761
1793
  let i = 0;
1762
1794
  while (i < ancestors.length) {
1763
- const member = ancestors[i];
1764
- const nextCall = ancestors[i + 1];
1765
- const isChainMember = member?.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
1795
+ const member = ancestors.at(i);
1796
+ const nextCall = ancestors.at(i + 1);
1797
+ const isChainMember = member !== void 0 && member.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
1766
1798
  const methodName = isChainMember ? memberPropertyName(member) : void 0;
1767
1799
  const continuesChain = methodName === "then" || methodName === "finally";
1768
1800
  if (methodName === "catch") {
1769
1801
  return true;
1770
1802
  }
1771
- if (!continuesChain) {
1803
+ if (!nextCall || !continuesChain) {
1772
1804
  return false;
1773
1805
  }
1774
1806
  currentCall = nextCall;
@@ -1864,11 +1896,14 @@ var RULES = {
1864
1896
  "security/detect-new-buffer": "error"
1865
1897
  };
1866
1898
  var PLUGINS = { security: securityPlugin };
1899
+ var suppressionMarker = (ruleId) => `codesentry-disable-next-line ${ruleId}`;
1900
+ var isSuppressed = (contentLines, finding) => contentLines.at(finding.line - 2)?.includes(suppressionMarker(finding.ruleId)) ?? false;
1867
1901
  var securityLintRule = {
1868
1902
  id: "security-lint",
1869
1903
  description: "Detecta padr\xF5es de seguran\xE7a gen\xE9ricos (object injection, regex n\xE3o literal, fs n\xE3o literal, etc.) via eslint-plugin-security",
1870
1904
  check(filePath, content) {
1871
- return runEslintRules(filePath, content, RULES, PLUGINS).map((finding) => ({
1905
+ const contentLines = content.split("\n");
1906
+ return runEslintRules(filePath, content, RULES, PLUGINS).filter((finding) => !isSuppressed(contentLines, finding)).map((finding) => ({
1872
1907
  ruleId: finding.ruleId,
1873
1908
  message: `Padr\xE3o inseguro detectado (${finding.ruleId}): ${finding.message}`,
1874
1909
  file: filePath,
@@ -1906,14 +1941,9 @@ var propertyKeyName = (property) => {
1906
1941
  var argumentContainsSensitiveData = (argument) => {
1907
1942
  let found = false;
1908
1943
  visitSourceNodes(argument, (node) => {
1909
- if (node.type === "Identifier" && SENSITIVE_NAME_PATTERN2.test(node.name)) {
1910
- found = true;
1911
- } else if (node.type === "ObjectProperty") {
1912
- const keyName = propertyKeyName(node);
1913
- if (keyName && SENSITIVE_NAME_PATTERN2.test(keyName)) {
1914
- found = true;
1915
- }
1916
- }
1944
+ const isSensitiveIdentifier = node.type === "Identifier" && SENSITIVE_NAME_PATTERN2.test(node.name);
1945
+ const keyName = node.type === "ObjectProperty" ? propertyKeyName(node) : void 0;
1946
+ found ||= isSensitiveIdentifier || keyName !== void 0 && SENSITIVE_NAME_PATTERN2.test(keyName);
1917
1947
  });
1918
1948
  return found;
1919
1949
  };
@@ -2198,29 +2228,18 @@ var RULES2 = {
2198
2228
  "no-unsanitized/method": "error"
2199
2229
  };
2200
2230
  var PLUGINS2 = { "no-unsanitized": noUnsanitizedPlugin };
2231
+ var isHtmlProperty = (property) => {
2232
+ const key = property.key;
2233
+ return key?.type === "Identifier" && key.name === "__html";
2234
+ };
2201
2235
  var isDangerouslySetInnerHtmlWithDynamicValue = (node) => {
2202
- if (node.type !== "JSXAttribute") {
2203
- return false;
2204
- }
2205
2236
  const name = node.name;
2206
- if (name?.type !== "JSXIdentifier" || name.name !== "dangerouslySetInnerHTML") {
2207
- return false;
2208
- }
2209
2237
  const value = node.value;
2210
- if (value?.type !== "JSXExpressionContainer") {
2211
- return false;
2212
- }
2213
- const expression = value.expression;
2214
- if (expression?.type !== "ObjectExpression") {
2215
- return false;
2216
- }
2217
- const properties = expression.properties;
2218
- const htmlProperty = properties?.find((property) => {
2219
- const key = property.key;
2220
- return key?.type === "Identifier" && key.name === "__html";
2221
- });
2238
+ const expression = value?.expression;
2239
+ const properties = expression?.properties;
2240
+ const htmlProperty = properties?.find(isHtmlProperty);
2222
2241
  const htmlValue = htmlProperty?.value;
2223
- return htmlValue !== void 0 && htmlValue.type !== "StringLiteral";
2242
+ return node.type === "JSXAttribute" && name?.type === "JSXIdentifier" && name.name === "dangerouslySetInnerHTML" && value?.type === "JSXExpressionContainer" && expression?.type === "ObjectExpression" && htmlValue !== void 0 && htmlValue.type !== "StringLiteral";
2224
2243
  };
2225
2244
  var findDangerouslySetInnerHtmlFindings = (filePath, content) => {
2226
2245
  const findings = [];
@@ -2273,19 +2292,10 @@ var hasRiskyOptionEnabled = (options) => {
2273
2292
  }) ?? false;
2274
2293
  };
2275
2294
  var isUnsafeXmlParseCall = (node) => {
2276
- if (node.type !== "CallExpression") {
2277
- return false;
2278
- }
2279
2295
  const callee = node.callee;
2280
- if (callee?.type !== "MemberExpression") {
2281
- return false;
2282
- }
2283
- const property = callee.property;
2284
- if (property?.type !== "Identifier" || !XML_PARSE_METHOD_NAMES.has(property.name)) {
2285
- return false;
2286
- }
2296
+ const property = callee?.property;
2287
2297
  const args = node.arguments;
2288
- return hasRiskyOptionEnabled(args?.[1]);
2298
+ return node.type === "CallExpression" && callee?.type === "MemberExpression" && property?.type === "Identifier" && XML_PARSE_METHOD_NAMES.has(property.name) && hasRiskyOptionEnabled(args?.[1]);
2289
2299
  };
2290
2300
  var findUnsafeXmlParsingLines = (filePath, content) => {
2291
2301
  const lines = /* @__PURE__ */ new Set();
@@ -2386,9 +2396,9 @@ var registerSecurityLintCommand = (program) => {
2386
2396
 
2387
2397
  // src/commands/sensitive-data-in-logs/sensitive-data-in-logs.command.ts
2388
2398
  var registerSensitiveDataInLogsCommand = (program) => {
2389
- program.command("sensitive-data-in-logs").description("Detecta senhas/segredos/tokens sendo passados para chamadas de log").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
2390
- await scanAndReport(path, [sensitiveDataInLogsRule], "Checking sensitive data in logs...", options);
2391
- });
2399
+ program.command("sensitive-data-in-logs").description("Detecta senhas/segredos/tokens sendo passados para chamadas de log").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
2400
+ (path, options) => scanAndReport(path, [sensitiveDataInLogsRule], "Checking sensitive data in logs...", options)
2401
+ );
2392
2402
  };
2393
2403
 
2394
2404
  // src/commands/tls-validation-disabled/tls-validation-disabled.command.ts
@@ -2463,9 +2473,9 @@ var registerWeakHashAlgorithmCommand = (program) => {
2463
2473
  var registerWeakSecretFallbackCommand = (program) => {
2464
2474
  program.command("weak-secret-fallback").description(
2465
2475
  "Detecta uma vari\xE1vel de ambiente de segredo/chave com um valor hardcoded como fallback (|| ou ??)"
2466
- ).argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
2467
- await scanAndReport(path, [weakSecretFallbackRule], "Checking weak secret fallbacks...", options);
2468
- });
2476
+ ).argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
2477
+ (path, options) => scanAndReport(path, [weakSecretFallbackRule], "Checking weak secret fallbacks...", options)
2478
+ );
2469
2479
  };
2470
2480
 
2471
2481
  // src/commands/xss/xss.command.ts
@@ -2477,9 +2487,9 @@ var registerXssCommand = (program) => {
2477
2487
 
2478
2488
  // src/commands/xxe-unsafe-xml-parsing/xxe-unsafe-xml-parsing.command.ts
2479
2489
  var registerXxeUnsafeXmlParsingCommand = (program) => {
2480
- program.command("xxe-unsafe-xml-parsing").description("Detecta parsing de XML com noent/dtdload habilitados (risco de XXE)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
2481
- await scanAndReport(path, [xxeUnsafeXmlParsingRule], "Checking unsafe XML parsing...", options);
2482
- });
2490
+ program.command("xxe-unsafe-xml-parsing").description("Detecta parsing de XML com noent/dtdload habilitados (risco de XXE)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
2491
+ (path, options) => scanAndReport(path, [xxeUnsafeXmlParsingRule], "Checking unsafe XML parsing...", options)
2492
+ );
2483
2493
  };
2484
2494
 
2485
2495
  // src/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesentry",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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.9",
51
+ "codesentry-semgrep-rules": "0.1.10",
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.9",
63
- "codesentry-semgrep-win32-x64": "0.1.9"
62
+ "codesentry-semgrep-linux-x64": "0.1.10",
63
+ "codesentry-semgrep-win32-x64": "0.1.10"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "^26.4.1",