codesentry 0.1.8 → 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 +211 -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,50 +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
- const SYSTEM_PATH_FALLBACK = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
486
- const env = {
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) => {
507
+ const semgrepDir = dirname2(runtime.semgrep);
508
+ const systemPathFallback = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
509
+ return {
487
510
  ...process.env,
488
- PATH: `${dirname2(runtime.semgrep)}${delimiter}${process.env.PATH ?? ""}${delimiter}${SYSTEM_PATH_FALLBACK}`
511
+ PATH: `${semgrepDir}${delimiter}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
489
512
  };
513
+ };
514
+ var executeSemgrep = async (targetDir, runtime, ruleset, execute) => {
490
515
  try {
491
- ({ 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;
492
522
  } catch (error) {
493
523
  const output = outputFromError(error);
494
- if (!output) {
495
- throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
524
+ if (output) {
525
+ return output;
496
526
  }
497
- 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 });
498
543
  }
499
- const report = parseSemgrepReport(stdout);
500
- const scannedFiles = report.paths?.scanned.length ?? 0;
501
- return {
502
- scannedFiles,
503
- findings: mapSemgrepReportToFindings(report),
504
- durationMs: Date.now() - startedAt,
505
- engines: { semgrep: scannedFiles }
506
- };
507
544
  };
508
545
 
509
546
  // src/commands/scan/report-filename.ts
@@ -539,22 +576,28 @@ Relat\xF3rio detalhado gerado em: ${filePath}`));
539
576
  console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage2(error)}`));
540
577
  }
541
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
+ };
542
591
  var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
543
592
  {
544
593
  title: taskTitle,
545
- task: () => runScan(path, rules, options.concurrency).then((nativeResult) => {
546
- if (!options.semgrep) {
547
- onResult(nativeResult);
548
- return void 0;
594
+ task: async () => {
595
+ try {
596
+ onResult(await runScanEngines(path, rules, options));
597
+ } catch (error) {
598
+ throw error;
549
599
  }
550
- return runBundledSemgrep(path, void 0, options.config).then((semgrepResult) => {
551
- onResult(mergeScanResults(nativeResult, semgrepResult));
552
- });
553
- }).catch((error) => {
554
- throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, {
555
- cause: error
556
- });
557
- })
600
+ }
558
601
  }
559
602
  ]);
560
603
  var scanAndReport = async (path, rules, taskTitle, options) => {
@@ -883,14 +926,14 @@ var nodeName = (node) => {
883
926
  }
884
927
  return nameValue(node) ?? literalValue(node) ?? nodeName(node.id);
885
928
  };
886
- var PARENT_NAME_KEYS = {
887
- VariableDeclarator: "id",
888
- ObjectProperty: "key",
889
- AssignmentExpression: "left"
890
- };
929
+ var PARENT_NAME_READERS = /* @__PURE__ */ new Map([
930
+ ["VariableDeclarator", (parent) => parent.id],
931
+ ["ObjectProperty", (parent) => parent.key],
932
+ ["AssignmentExpression", (parent) => parent.left]
933
+ ]);
891
934
  var parentFunctionName = (parent) => {
892
- const nameKey = parent ? PARENT_NAME_KEYS[parent.type] : void 0;
893
- 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;
894
937
  };
895
938
  var isConstructor = (node) => {
896
939
  return node.type === "ClassMethod" && node.kind === "constructor";
@@ -1072,20 +1115,11 @@ var calleeName = (callee) => {
1072
1115
  return void 0;
1073
1116
  };
1074
1117
  var isDateNowOrGetTimeCall = (node) => {
1075
- if (node.type !== "CallExpression") {
1076
- return false;
1077
- }
1078
1118
  const callee = node.callee;
1079
- if (callee?.type !== "MemberExpression") {
1080
- return false;
1081
- }
1082
- const property = callee.property;
1119
+ const property = callee?.property;
1083
1120
  const propertyName = property?.type === "Identifier" ? property.name : void 0;
1084
- if (propertyName === "getTime") {
1085
- return true;
1086
- }
1087
- const object = callee.object;
1088
- 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");
1089
1123
  };
1090
1124
  var TIMESTAMP_NAME_PATTERN = /timestamp|^now$/i;
1091
1125
  var containsPredictableTimestamp = (node) => {
@@ -1207,13 +1241,12 @@ var analyzeFile = (sourceFile) => {
1207
1241
  hasSignatureVerification: false
1208
1242
  };
1209
1243
  visitSourceNodes(sourceFile, (node) => {
1210
- if (isBase64DecodeCall(node) && node.loc) {
1211
- analysis.base64DecodeLines.push(node.loc.start.line);
1212
- } else if (isJsonParseCall(node)) {
1213
- analysis.hasJsonParse = true;
1214
- } else if (isSignatureVerificationCall(node)) {
1215
- analysis.hasSignatureVerification = true;
1244
+ const base64DecodeLine = isBase64DecodeCall(node) ? node.loc?.start.line : void 0;
1245
+ if (base64DecodeLine !== void 0) {
1246
+ analysis.base64DecodeLines.push(base64DecodeLine);
1216
1247
  }
1248
+ analysis.hasJsonParse ||= isJsonParseCall(node);
1249
+ analysis.hasSignatureVerification ||= isSignatureVerificationCall(node);
1217
1250
  });
1218
1251
  return analysis;
1219
1252
  };
@@ -1241,14 +1274,14 @@ var jwtDecodeWithoutVerifyRule = {
1241
1274
 
1242
1275
  // src/commands/jwt-decode-without-verify/jwt-decode-without-verify.command.ts
1243
1276
  var registerJwtDecodeWithoutVerifyCommand = (program) => {
1244
- 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) => {
1245
- 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(
1246
1279
  path,
1247
1280
  [jwtDecodeWithoutVerifyRule],
1248
1281
  "Checking JWT decode without verification...",
1249
1282
  options
1250
- );
1251
- });
1283
+ )
1284
+ );
1252
1285
  };
1253
1286
 
1254
1287
  // src/rules/jwt-no-expiration.rule.ts
@@ -1609,9 +1642,9 @@ var publicEnvVarSecretRule = {
1609
1642
 
1610
1643
  // src/commands/public-env-var-secret/public-env-var-secret.command.ts
1611
1644
  var registerPublicEnvVarSecretCommand = (program) => {
1612
- 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) => {
1613
- await scanAndReport(path, [publicEnvVarSecretRule], "Checking public env var secrets...", options);
1614
- });
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
+ );
1615
1648
  };
1616
1649
 
1617
1650
  // src/commands/rules/rules.command.ts
@@ -1759,15 +1792,15 @@ var chainReachesCatch = (thenCall, ancestors) => {
1759
1792
  let currentCall = thenCall;
1760
1793
  let i = 0;
1761
1794
  while (i < ancestors.length) {
1762
- const member = ancestors[i];
1763
- const nextCall = ancestors[i + 1];
1764
- 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);
1765
1798
  const methodName = isChainMember ? memberPropertyName(member) : void 0;
1766
1799
  const continuesChain = methodName === "then" || methodName === "finally";
1767
1800
  if (methodName === "catch") {
1768
1801
  return true;
1769
1802
  }
1770
- if (!continuesChain) {
1803
+ if (!nextCall || !continuesChain) {
1771
1804
  return false;
1772
1805
  }
1773
1806
  currentCall = nextCall;
@@ -1863,11 +1896,14 @@ var RULES = {
1863
1896
  "security/detect-new-buffer": "error"
1864
1897
  };
1865
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;
1866
1901
  var securityLintRule = {
1867
1902
  id: "security-lint",
1868
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",
1869
1904
  check(filePath, content) {
1870
- 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) => ({
1871
1907
  ruleId: finding.ruleId,
1872
1908
  message: `Padr\xE3o inseguro detectado (${finding.ruleId}): ${finding.message}`,
1873
1909
  file: filePath,
@@ -1905,14 +1941,9 @@ var propertyKeyName = (property) => {
1905
1941
  var argumentContainsSensitiveData = (argument) => {
1906
1942
  let found = false;
1907
1943
  visitSourceNodes(argument, (node) => {
1908
- if (node.type === "Identifier" && SENSITIVE_NAME_PATTERN2.test(node.name)) {
1909
- found = true;
1910
- } else if (node.type === "ObjectProperty") {
1911
- const keyName = propertyKeyName(node);
1912
- if (keyName && SENSITIVE_NAME_PATTERN2.test(keyName)) {
1913
- found = true;
1914
- }
1915
- }
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);
1916
1947
  });
1917
1948
  return found;
1918
1949
  };
@@ -2197,29 +2228,18 @@ var RULES2 = {
2197
2228
  "no-unsanitized/method": "error"
2198
2229
  };
2199
2230
  var PLUGINS2 = { "no-unsanitized": noUnsanitizedPlugin };
2231
+ var isHtmlProperty = (property) => {
2232
+ const key = property.key;
2233
+ return key?.type === "Identifier" && key.name === "__html";
2234
+ };
2200
2235
  var isDangerouslySetInnerHtmlWithDynamicValue = (node) => {
2201
- if (node.type !== "JSXAttribute") {
2202
- return false;
2203
- }
2204
2236
  const name = node.name;
2205
- if (name?.type !== "JSXIdentifier" || name.name !== "dangerouslySetInnerHTML") {
2206
- return false;
2207
- }
2208
2237
  const value = node.value;
2209
- if (value?.type !== "JSXExpressionContainer") {
2210
- return false;
2211
- }
2212
- const expression = value.expression;
2213
- if (expression?.type !== "ObjectExpression") {
2214
- return false;
2215
- }
2216
- const properties = expression.properties;
2217
- const htmlProperty = properties?.find((property) => {
2218
- const key = property.key;
2219
- return key?.type === "Identifier" && key.name === "__html";
2220
- });
2238
+ const expression = value?.expression;
2239
+ const properties = expression?.properties;
2240
+ const htmlProperty = properties?.find(isHtmlProperty);
2221
2241
  const htmlValue = htmlProperty?.value;
2222
- 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";
2223
2243
  };
2224
2244
  var findDangerouslySetInnerHtmlFindings = (filePath, content) => {
2225
2245
  const findings = [];
@@ -2272,19 +2292,10 @@ var hasRiskyOptionEnabled = (options) => {
2272
2292
  }) ?? false;
2273
2293
  };
2274
2294
  var isUnsafeXmlParseCall = (node) => {
2275
- if (node.type !== "CallExpression") {
2276
- return false;
2277
- }
2278
2295
  const callee = node.callee;
2279
- if (callee?.type !== "MemberExpression") {
2280
- return false;
2281
- }
2282
- const property = callee.property;
2283
- if (property?.type !== "Identifier" || !XML_PARSE_METHOD_NAMES.has(property.name)) {
2284
- return false;
2285
- }
2296
+ const property = callee?.property;
2286
2297
  const args = node.arguments;
2287
- 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]);
2288
2299
  };
2289
2300
  var findUnsafeXmlParsingLines = (filePath, content) => {
2290
2301
  const lines = /* @__PURE__ */ new Set();
@@ -2385,9 +2396,9 @@ var registerSecurityLintCommand = (program) => {
2385
2396
 
2386
2397
  // src/commands/sensitive-data-in-logs/sensitive-data-in-logs.command.ts
2387
2398
  var registerSensitiveDataInLogsCommand = (program) => {
2388
- 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) => {
2389
- await scanAndReport(path, [sensitiveDataInLogsRule], "Checking sensitive data in logs...", options);
2390
- });
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
+ );
2391
2402
  };
2392
2403
 
2393
2404
  // src/commands/tls-validation-disabled/tls-validation-disabled.command.ts
@@ -2462,9 +2473,9 @@ var registerWeakHashAlgorithmCommand = (program) => {
2462
2473
  var registerWeakSecretFallbackCommand = (program) => {
2463
2474
  program.command("weak-secret-fallback").description(
2464
2475
  "Detecta uma vari\xE1vel de ambiente de segredo/chave com um valor hardcoded como fallback (|| ou ??)"
2465
- ).argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
2466
- await scanAndReport(path, [weakSecretFallbackRule], "Checking weak secret fallbacks...", options);
2467
- });
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
+ );
2468
2479
  };
2469
2480
 
2470
2481
  // src/commands/xss/xss.command.ts
@@ -2476,9 +2487,9 @@ var registerXssCommand = (program) => {
2476
2487
 
2477
2488
  // src/commands/xxe-unsafe-xml-parsing/xxe-unsafe-xml-parsing.command.ts
2478
2489
  var registerXxeUnsafeXmlParsingCommand = (program) => {
2479
- 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) => {
2480
- await scanAndReport(path, [xxeUnsafeXmlParsingRule], "Checking unsafe XML parsing...", options);
2481
- });
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
+ );
2482
2493
  };
2483
2494
 
2484
2495
  // src/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codesentry",
3
- "version": "0.1.8",
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.8",
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.8",
63
- "codesentry-semgrep-win32-x64": "0.1.8"
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",