codesentry 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/index.js +2502 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import figlet from "figlet";
|
|
6
|
+
import gradient from "gradient-string";
|
|
7
|
+
|
|
8
|
+
// src/parser/source-file.ts
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
10
|
+
var isJsxFile = (filePath) => /\.(tsx|jsx)$/.test(filePath);
|
|
11
|
+
var parseSourceFile = (filePath, content) => {
|
|
12
|
+
return parse(content, {
|
|
13
|
+
sourceFilename: filePath,
|
|
14
|
+
sourceType: "unambiguous",
|
|
15
|
+
plugins: isJsxFile(filePath) ? ["jsx", "typescript", "decorators-legacy"] : ["typescript", "decorators-legacy"],
|
|
16
|
+
errorRecovery: true
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
var isSourceNode = (value) => {
|
|
20
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
21
|
+
};
|
|
22
|
+
var visitSourceNodes = (node, visitor, parent) => {
|
|
23
|
+
visitor(node, parent);
|
|
24
|
+
for (const value of Object.values(node)) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
value.filter(isSourceNode).forEach((child) => visitSourceNodes(child, visitor, node));
|
|
27
|
+
} else if (isSourceNode(value)) {
|
|
28
|
+
visitSourceNodes(value, visitor, node);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/rules/command-injection.rule.ts
|
|
34
|
+
var EXEC_METHOD_NAMES = /* @__PURE__ */ new Set(["exec", "execSync"]);
|
|
35
|
+
var CHILD_PROCESS_MODULE_NAMES = /* @__PURE__ */ new Set(["child_process", "node:child_process"]);
|
|
36
|
+
var isChildProcessModuleSpecifier = (node) => node?.type === "StringLiteral" && CHILD_PROCESS_MODULE_NAMES.has(node.value);
|
|
37
|
+
var collectFromImportDeclaration = (node, bindings) => {
|
|
38
|
+
if (!isChildProcessModuleSpecifier(node.source)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const specifier of node.specifiers ?? []) {
|
|
42
|
+
const local = specifier.local;
|
|
43
|
+
if (local?.type !== "Identifier") {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (specifier.type === "ImportSpecifier") {
|
|
47
|
+
const imported = specifier.imported;
|
|
48
|
+
if (imported?.type === "Identifier" && EXEC_METHOD_NAMES.has(imported.name)) {
|
|
49
|
+
bindings.directCalls.add(local.name);
|
|
50
|
+
}
|
|
51
|
+
} else if (specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier") {
|
|
52
|
+
bindings.namespaces.add(local.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var isRequireCall = (node) => node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "require" && isChildProcessModuleSpecifier(node.arguments?.[0]);
|
|
57
|
+
var collectFromVariableDeclarator = (node, bindings) => {
|
|
58
|
+
if (!isRequireCall(node.init)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const id = node.id;
|
|
62
|
+
if (id?.type === "Identifier") {
|
|
63
|
+
bindings.namespaces.add(id.name);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (id?.type !== "ObjectPattern") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const property of id.properties ?? []) {
|
|
70
|
+
const key = property.key;
|
|
71
|
+
const value = property.value;
|
|
72
|
+
if (key?.type === "Identifier" && EXEC_METHOD_NAMES.has(key.name) && value?.type === "Identifier") {
|
|
73
|
+
bindings.directCalls.add(value.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var collectChildProcessBindings = (sourceFile) => {
|
|
78
|
+
const bindings = { directCalls: /* @__PURE__ */ new Set(), namespaces: /* @__PURE__ */ new Set() };
|
|
79
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
80
|
+
if (node.type === "ImportDeclaration") {
|
|
81
|
+
collectFromImportDeclaration(node, bindings);
|
|
82
|
+
} else if (node.type === "VariableDeclarator") {
|
|
83
|
+
collectFromVariableDeclarator(node, bindings);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return bindings;
|
|
87
|
+
};
|
|
88
|
+
var isKnownChildProcessCallee = (callee, bindings) => {
|
|
89
|
+
if (callee?.type === "Identifier") {
|
|
90
|
+
return bindings.directCalls.has(callee.name);
|
|
91
|
+
}
|
|
92
|
+
if (callee?.type !== "MemberExpression") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const object = callee.object;
|
|
96
|
+
const property = callee.property;
|
|
97
|
+
const methodName = property?.type === "Identifier" ? property.name : void 0;
|
|
98
|
+
return !!methodName && EXEC_METHOD_NAMES.has(methodName) && object?.type === "Identifier" && bindings.namespaces.has(object.name);
|
|
99
|
+
};
|
|
100
|
+
var isDynamicCommandArgument = (argument) => argument?.type === "StringLiteral" ? false : argument?.type === "TemplateLiteral" ? (argument.expressions?.length ?? 0) > 0 : argument !== void 0;
|
|
101
|
+
var isCommandInjectionCall = (node, bindings) => {
|
|
102
|
+
if (node.type !== "CallExpression") {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const callee = node.callee;
|
|
106
|
+
if (!isKnownChildProcessCallee(callee, bindings)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const args = node.arguments;
|
|
110
|
+
return isDynamicCommandArgument(args?.[0]);
|
|
111
|
+
};
|
|
112
|
+
var findCommandInjectionLines = (filePath, content) => {
|
|
113
|
+
const lines = /* @__PURE__ */ new Set();
|
|
114
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
115
|
+
const bindings = collectChildProcessBindings(sourceFile);
|
|
116
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
117
|
+
if (isCommandInjectionCall(node, bindings) && node.loc) {
|
|
118
|
+
lines.add(node.loc.start.line);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return [...lines].sort((a, b) => a - b);
|
|
122
|
+
};
|
|
123
|
+
var commandInjectionRule = {
|
|
124
|
+
id: "command-injection",
|
|
125
|
+
description: "Detecta child_process.exec()/execSync() (confirmado via import/require do m\xF3dulo) recebendo um comando constru\xEDdo dinamicamente",
|
|
126
|
+
check(filePath, content) {
|
|
127
|
+
return findCommandInjectionLines(filePath, content).map((line) => ({
|
|
128
|
+
ruleId: "command-injection",
|
|
129
|
+
message: "Comando de shell constru\xEDdo dinamicamente \u2014 risco de command injection",
|
|
130
|
+
file: filePath,
|
|
131
|
+
line,
|
|
132
|
+
severity: "critical"
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/commands/scan/scan-runner.ts
|
|
138
|
+
import { writeFile } from "fs/promises";
|
|
139
|
+
import { join as join2 } from "path";
|
|
140
|
+
import chalk2 from "chalk";
|
|
141
|
+
import { Listr } from "listr2";
|
|
142
|
+
|
|
143
|
+
// src/reporters/console.reporter.ts
|
|
144
|
+
import chalk from "chalk";
|
|
145
|
+
import Table from "cli-table3";
|
|
146
|
+
var SEVERITY_COLOR = {
|
|
147
|
+
low: (text2) => chalk.gray(text2),
|
|
148
|
+
medium: (text2) => chalk.yellow(text2),
|
|
149
|
+
high: (text2) => chalk.red(text2),
|
|
150
|
+
critical: (text2) => chalk.bgRed.white(text2)
|
|
151
|
+
};
|
|
152
|
+
var printConsoleReport = (result) => {
|
|
153
|
+
const coverage = result.engines?.semgrep === void 0 ? "" : ` CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s).`;
|
|
154
|
+
if (result.findings.length === 0) {
|
|
155
|
+
console.log(
|
|
156
|
+
chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`)
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const table = new Table({
|
|
161
|
+
head: ["Severity", "Rule", "File", "Line", "Message"]
|
|
162
|
+
});
|
|
163
|
+
for (const finding of result.findings) {
|
|
164
|
+
const colorize = SEVERITY_COLOR[finding.severity];
|
|
165
|
+
table.push([
|
|
166
|
+
colorize(finding.severity),
|
|
167
|
+
finding.ruleId,
|
|
168
|
+
finding.file,
|
|
169
|
+
String(finding.line),
|
|
170
|
+
finding.message
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
console.log(table.toString());
|
|
174
|
+
console.log(
|
|
175
|
+
chalk.bold(
|
|
176
|
+
`
|
|
177
|
+
${result.findings.length} problema(s) encontrado(s) em ${result.scannedFiles} arquivo(s) (${result.durationMs}ms).${coverage}`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// src/reporters/json.reporter.ts
|
|
183
|
+
var toJsonReport = (result) => {
|
|
184
|
+
return JSON.stringify(result, null, 2);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/reporters/markdown.reporter.ts
|
|
188
|
+
var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
|
|
189
|
+
var SEVERITY_LABEL = {
|
|
190
|
+
critical: "Critical",
|
|
191
|
+
high: "High",
|
|
192
|
+
medium: "Medium",
|
|
193
|
+
low: "Low"
|
|
194
|
+
};
|
|
195
|
+
var escapeCell = (text2) => text2.replaceAll("|", "\\|");
|
|
196
|
+
var groupBy = (items, keyOf) => {
|
|
197
|
+
const map = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const item of items) {
|
|
199
|
+
const key = keyOf(item);
|
|
200
|
+
map.set(key, [...map.get(key) ?? [], item]);
|
|
201
|
+
}
|
|
202
|
+
return map;
|
|
203
|
+
};
|
|
204
|
+
var findingsTable = (findings) => {
|
|
205
|
+
const sorted = [...findings].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
206
|
+
const lines = ["| Arquivo | Linha | Mensagem |", "| --- | --- | --- |"];
|
|
207
|
+
for (const f of sorted) {
|
|
208
|
+
lines.push(`| ${escapeCell(f.file)} | ${f.line} | ${escapeCell(f.message)} |`);
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
};
|
|
212
|
+
var severitySection = (severity, findings) => {
|
|
213
|
+
const lines = [`## ${SEVERITY_LABEL[severity]} (${findings.length})`, ""];
|
|
214
|
+
const byRule = groupBy(findings, (f) => f.ruleId);
|
|
215
|
+
for (const ruleId of [...byRule.keys()].sort()) {
|
|
216
|
+
const ruleFindings = byRule.get(ruleId) ?? [];
|
|
217
|
+
lines.push(`### ${ruleId} (${ruleFindings.length})`, "", ...findingsTable(ruleFindings), "");
|
|
218
|
+
}
|
|
219
|
+
return lines;
|
|
220
|
+
};
|
|
221
|
+
var reportHeader = (result, generatedAt) => [
|
|
222
|
+
"# Relat\xF3rio CodeSentry",
|
|
223
|
+
"",
|
|
224
|
+
`- **Gerado em:** ${generatedAt.toISOString()}`,
|
|
225
|
+
`- **Arquivos analisados:** ${result.scannedFiles}`,
|
|
226
|
+
...result.engines?.semgrep === void 0 ? [] : [
|
|
227
|
+
`- **Cobertura por motor:** CodeSentry ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep ${result.engines.semgrep} arquivos`
|
|
228
|
+
],
|
|
229
|
+
`- **Dura\xE7\xE3o:** ${result.durationMs}ms`,
|
|
230
|
+
`- **Total de problemas:** ${result.findings.length}`,
|
|
231
|
+
""
|
|
232
|
+
];
|
|
233
|
+
var summaryTable = (bySeverity) => {
|
|
234
|
+
const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
|
|
235
|
+
for (const severity of SEVERITY_ORDER) {
|
|
236
|
+
lines.push(`| ${SEVERITY_LABEL[severity]} | ${(bySeverity.get(severity) ?? []).length} |`);
|
|
237
|
+
}
|
|
238
|
+
lines.push("");
|
|
239
|
+
return lines;
|
|
240
|
+
};
|
|
241
|
+
var severitySections = (bySeverity) => {
|
|
242
|
+
const lines = [];
|
|
243
|
+
for (const severity of SEVERITY_ORDER) {
|
|
244
|
+
const findings = bySeverity.get(severity) ?? [];
|
|
245
|
+
if (findings.length > 0) {
|
|
246
|
+
lines.push(...severitySection(severity, findings));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return lines;
|
|
250
|
+
};
|
|
251
|
+
var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
|
|
252
|
+
const bySeverity = groupBy(result.findings, (f) => f.severity);
|
|
253
|
+
return [...reportHeader(result, generatedAt), ...summaryTable(bySeverity), ...severitySections(bySeverity)].join(
|
|
254
|
+
"\n"
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/scanner/scan-result.ts
|
|
259
|
+
var mergeScanResults = (nativeResult, semgrepResult) => ({
|
|
260
|
+
scannedFiles: nativeResult.scannedFiles,
|
|
261
|
+
findings: [...nativeResult.findings, ...semgrepResult.findings],
|
|
262
|
+
durationMs: nativeResult.durationMs + semgrepResult.durationMs,
|
|
263
|
+
engines: {
|
|
264
|
+
codesentry: nativeResult.engines?.codesentry ?? nativeResult.scannedFiles,
|
|
265
|
+
semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// src/scanner/scanner.ts
|
|
270
|
+
import { readFile } from "fs/promises";
|
|
271
|
+
import { availableParallelism } from "os";
|
|
272
|
+
|
|
273
|
+
// src/scanner/file-finder.ts
|
|
274
|
+
import { readdir } from "fs/promises";
|
|
275
|
+
import { join, sep } from "path";
|
|
276
|
+
var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
|
|
277
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next"]);
|
|
278
|
+
var isInsideIgnoredDir = (relativePath) => {
|
|
279
|
+
return relativePath.split(sep).some((segment) => IGNORED_DIRS.has(segment));
|
|
280
|
+
};
|
|
281
|
+
var findFiles = async (targetDir) => {
|
|
282
|
+
try {
|
|
283
|
+
const entries = await readdir(targetDir, { recursive: true, withFileTypes: true });
|
|
284
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name)).filter((filePath) => !isInsideIgnoredDir(filePath)).filter((filePath) => SCANNABLE_EXTENSIONS.some((ext) => filePath.endsWith(ext)));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw new Error(`N\xE3o foi poss\xEDvel listar os arquivos em "${targetDir}".`, { cause: error });
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/scanner/run-with-concurrency-limit.ts
|
|
291
|
+
import pLimit from "p-limit";
|
|
292
|
+
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
293
|
+
const limit = pLimit(concurrency);
|
|
294
|
+
return Promise.all(items.map((item) => limit(() => task(item))));
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/scanner/scanner.ts
|
|
298
|
+
var DEFAULT_SCAN_CONCURRENCY = Math.max(1, Math.min(8, availableParallelism()));
|
|
299
|
+
var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
300
|
+
var parseErrorFinding = (filePath, error) => ({
|
|
301
|
+
ruleId: "parse-error",
|
|
302
|
+
message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${errorMessage(error)}`,
|
|
303
|
+
file: filePath,
|
|
304
|
+
line: 1,
|
|
305
|
+
severity: "low"
|
|
306
|
+
});
|
|
307
|
+
var readFileContent = async (filePath) => {
|
|
308
|
+
try {
|
|
309
|
+
return await readFile(filePath, "utf-8");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var scanFile = async (filePath, rules) => {
|
|
315
|
+
const content = await readFileContent(filePath);
|
|
316
|
+
const findings = [];
|
|
317
|
+
let hasParseError = false;
|
|
318
|
+
for (const rule of rules) {
|
|
319
|
+
try {
|
|
320
|
+
findings.push(...rule.check(filePath, content));
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (!hasParseError) {
|
|
323
|
+
findings.push(parseErrorFinding(filePath, error));
|
|
324
|
+
hasParseError = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return findings;
|
|
329
|
+
};
|
|
330
|
+
var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY) => {
|
|
331
|
+
try {
|
|
332
|
+
const startedAt = Date.now();
|
|
333
|
+
const files = await findFiles(targetDir);
|
|
334
|
+
const findings = (await runWithConcurrencyLimit(files, concurrency, (filePath) => scanFile(filePath, rules))).flat();
|
|
335
|
+
return {
|
|
336
|
+
scannedFiles: files.length,
|
|
337
|
+
findings,
|
|
338
|
+
durationMs: Date.now() - startedAt,
|
|
339
|
+
engines: { codesentry: files.length }
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw new Error(`N\xE3o foi poss\xEDvel concluir a an\xE1lise de "${targetDir}".`, { cause: error });
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/scanner/semgrep.ts
|
|
347
|
+
import { execFile } from "child_process";
|
|
348
|
+
import { promisify } from "util";
|
|
349
|
+
|
|
350
|
+
// src/scanner/semgrep-runtime.ts
|
|
351
|
+
import { existsSync, readFileSync } from "fs";
|
|
352
|
+
import { createRequire } from "module";
|
|
353
|
+
import { dirname, resolve } from "path";
|
|
354
|
+
var require2 = createRequire(import.meta.url);
|
|
355
|
+
var packageForPlatform = (platform, architecture) => {
|
|
356
|
+
if (platform === "linux" && architecture === "x64") {
|
|
357
|
+
return "codesentry-semgrep-linux-x64";
|
|
358
|
+
}
|
|
359
|
+
if (platform === "win32" && architecture === "x64") {
|
|
360
|
+
return "codesentry-semgrep-win32-x64";
|
|
361
|
+
}
|
|
362
|
+
return void 0;
|
|
363
|
+
};
|
|
364
|
+
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.`;
|
|
365
|
+
var resolveBundledSemgrepRuntime = (platform = process.platform, architecture = process.arch, resolveManifestPath = (packageName) => require2.resolve(`${packageName}/runtime.json`)) => {
|
|
366
|
+
const packageName = packageForPlatform(platform, architecture);
|
|
367
|
+
if (!packageName) {
|
|
368
|
+
throw new Error(unsupportedRuntimeMessage(platform, architecture));
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const manifestPath = resolveManifestPath(packageName);
|
|
372
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
373
|
+
const runtime = {
|
|
374
|
+
semgrep: resolve(dirname(manifestPath), manifest.semgrep)
|
|
375
|
+
};
|
|
376
|
+
if (!existsSync(runtime.semgrep)) {
|
|
377
|
+
throw new Error("artefatos do runtime ausentes");
|
|
378
|
+
}
|
|
379
|
+
return runtime;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const reason = error instanceof Error ? error.message : "erro desconhecido";
|
|
382
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${reason}`, { cause: error });
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/scanner/semgrep-rules.ts
|
|
387
|
+
import { existsSync as existsSync2 } from "fs";
|
|
388
|
+
import { createRequire as createRequire2 } from "module";
|
|
389
|
+
var require3 = createRequire2(import.meta.url);
|
|
390
|
+
var resolveBundledSemgrepRuleset = () => {
|
|
391
|
+
try {
|
|
392
|
+
const ruleset = require3.resolve("codesentry-semgrep-rules/rules/owasp.yml");
|
|
393
|
+
if (!existsSync2(ruleset)) {
|
|
394
|
+
throw new Error("ruleset ausente");
|
|
395
|
+
}
|
|
396
|
+
return ruleset;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
const reason = error instanceof Error ? error.message : "erro desconhecido";
|
|
399
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar o ruleset OWASP embutido: ${reason}`, { cause: error });
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/scanner/semgrep.ts
|
|
404
|
+
var execFileAsync = promisify(execFile);
|
|
405
|
+
var SEMGREP_SEVERITIES = {
|
|
406
|
+
INFO: "low",
|
|
407
|
+
WARNING: "medium",
|
|
408
|
+
ERROR: "high",
|
|
409
|
+
CRITICAL: "critical"
|
|
410
|
+
};
|
|
411
|
+
var parseSemgrepReport = (stdout) => {
|
|
412
|
+
try {
|
|
413
|
+
const report = JSON.parse(stdout);
|
|
414
|
+
if (!Array.isArray(report.results)) {
|
|
415
|
+
throw new Error('campo "results" ausente');
|
|
416
|
+
}
|
|
417
|
+
return { results: report.results, paths: report.paths };
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const reason = error instanceof Error ? error.message : "erro desconhecido";
|
|
420
|
+
throw new Error(`Semgrep retornou JSON inv\xE1lido: ${reason}`, { cause: error });
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var mapSemgrepReportToFindings = (report) => report.results.map((finding) => ({
|
|
424
|
+
ruleId: `semgrep/${finding.check_id}`,
|
|
425
|
+
message: finding.extra.message,
|
|
426
|
+
file: finding.path,
|
|
427
|
+
line: finding.start.line,
|
|
428
|
+
severity: SEMGREP_SEVERITIES[finding.extra.severity] ?? "medium"
|
|
429
|
+
}));
|
|
430
|
+
var outputFromError = (error) => typeof error.stdout === "string" ? error.stdout : void 0;
|
|
431
|
+
var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync) => {
|
|
432
|
+
const startedAt = Date.now();
|
|
433
|
+
const args = [
|
|
434
|
+
"scan",
|
|
435
|
+
"--config",
|
|
436
|
+
ruleset,
|
|
437
|
+
"--metrics=off",
|
|
438
|
+
"--json",
|
|
439
|
+
"--quiet",
|
|
440
|
+
"--exclude",
|
|
441
|
+
"node_modules",
|
|
442
|
+
"--exclude",
|
|
443
|
+
".git",
|
|
444
|
+
"--exclude",
|
|
445
|
+
"dist",
|
|
446
|
+
"--exclude",
|
|
447
|
+
".next",
|
|
448
|
+
targetDir
|
|
449
|
+
];
|
|
450
|
+
let stdout;
|
|
451
|
+
try {
|
|
452
|
+
({ stdout } = await execute(runtime.semgrep, args, { cwd: targetDir, maxBuffer: 20 * 1024 * 1024 }));
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const output = outputFromError(error);
|
|
455
|
+
if (!output) {
|
|
456
|
+
throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
|
|
457
|
+
}
|
|
458
|
+
stdout = output;
|
|
459
|
+
}
|
|
460
|
+
const report = parseSemgrepReport(stdout);
|
|
461
|
+
const scannedFiles = report.paths?.scanned.length ?? 0;
|
|
462
|
+
return {
|
|
463
|
+
scannedFiles,
|
|
464
|
+
findings: mapSemgrepReportToFindings(report),
|
|
465
|
+
durationMs: Date.now() - startedAt,
|
|
466
|
+
engines: { semgrep: scannedFiles }
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/commands/scan/report-filename.ts
|
|
471
|
+
var MARKDOWN_REPORT_FINDINGS_THRESHOLD = 20;
|
|
472
|
+
var generateMarkdownReportFilename = (date = /* @__PURE__ */ new Date()) => {
|
|
473
|
+
const timestamp = date.toISOString().replace(/[:.]/g, "-");
|
|
474
|
+
return `codesentry-report-${timestamp}.md`;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/commands/scan/scan-runner.ts
|
|
478
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
479
|
+
var reportScanFailure = (error) => {
|
|
480
|
+
process.exitCode = 1;
|
|
481
|
+
console.error(`Falha ao executar o scan: ${errorMessage2(error)}`);
|
|
482
|
+
};
|
|
483
|
+
var printResult = (result, options) => {
|
|
484
|
+
if (options.json) {
|
|
485
|
+
console.log(toJsonReport(result));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
printConsoleReport(result);
|
|
489
|
+
};
|
|
490
|
+
var writeMarkdownReportIfNeeded = async (result, targetDir) => {
|
|
491
|
+
if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const filePath = join2(targetDir, generateMarkdownReportFilename());
|
|
495
|
+
try {
|
|
496
|
+
await writeFile(filePath, toMarkdownReport(result), "utf-8");
|
|
497
|
+
console.log(chalk2.cyan(`
|
|
498
|
+
Relat\xF3rio detalhado gerado em: ${filePath}`));
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage2(error)}`));
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
|
|
504
|
+
{
|
|
505
|
+
title: taskTitle,
|
|
506
|
+
task: () => runScan(path, rules, options.concurrency).then((nativeResult) => {
|
|
507
|
+
if (!options.semgrep) {
|
|
508
|
+
onResult(nativeResult);
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
return runBundledSemgrep(path, void 0, options.config).then((semgrepResult) => {
|
|
512
|
+
onResult(mergeScanResults(nativeResult, semgrepResult));
|
|
513
|
+
});
|
|
514
|
+
}).catch((error) => {
|
|
515
|
+
throw new Error(`Falha durante a an\xE1lise: ${errorMessage2(error)}`, {
|
|
516
|
+
cause: error
|
|
517
|
+
});
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
]);
|
|
521
|
+
var scanAndReport = async (path, rules, taskTitle, options) => {
|
|
522
|
+
let result;
|
|
523
|
+
const tasks = createScanTasks(path, rules, taskTitle, options, (scanResult) => {
|
|
524
|
+
result = scanResult;
|
|
525
|
+
});
|
|
526
|
+
try {
|
|
527
|
+
await tasks.run();
|
|
528
|
+
if (result) {
|
|
529
|
+
printResult(result, options);
|
|
530
|
+
await writeMarkdownReportIfNeeded(result, path);
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
reportScanFailure(error);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/commands/command-injection/command-injection.command.ts
|
|
538
|
+
var registerCommandInjectionCommand = (program) => {
|
|
539
|
+
program.command("command-injection").description("Detecta child_process.exec()/execSync() recebendo entrada externa").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
540
|
+
(path, options) => scanAndReport(path, [commandInjectionRule], "Checking command injection...", options)
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/rules/deep-nesting.rule.ts
|
|
545
|
+
var MAX_NESTING_DEPTH = 3;
|
|
546
|
+
var NESTING_TYPES = /* @__PURE__ */ new Set([
|
|
547
|
+
"ForStatement",
|
|
548
|
+
"ForInStatement",
|
|
549
|
+
"ForOfStatement",
|
|
550
|
+
"WhileStatement",
|
|
551
|
+
"DoWhileStatement",
|
|
552
|
+
"SwitchStatement",
|
|
553
|
+
"TryStatement"
|
|
554
|
+
]);
|
|
555
|
+
var FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
556
|
+
"FunctionDeclaration",
|
|
557
|
+
"FunctionExpression",
|
|
558
|
+
"ArrowFunctionExpression",
|
|
559
|
+
"ObjectMethod",
|
|
560
|
+
"ClassMethod",
|
|
561
|
+
"ClassPrivateMethod"
|
|
562
|
+
]);
|
|
563
|
+
var isSourceNode2 = (value) => {
|
|
564
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
565
|
+
};
|
|
566
|
+
var walkChildren = (node, depth, results) => {
|
|
567
|
+
for (const value of Object.values(node)) {
|
|
568
|
+
if (Array.isArray(value)) {
|
|
569
|
+
value.filter(isSourceNode2).forEach((child) => walk(child, depth, results));
|
|
570
|
+
} else if (isSourceNode2(value)) {
|
|
571
|
+
walk(value, depth, results);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
var checkDepth = (newDepth, line, results) => {
|
|
576
|
+
if (newDepth > MAX_NESTING_DEPTH && line !== void 0) {
|
|
577
|
+
results.push({ line, depth: newDepth });
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var walkIfChild = (value, depth, results) => {
|
|
581
|
+
if (isSourceNode2(value)) {
|
|
582
|
+
walk(value, depth, results);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
var walkIfAlternate = (alternate, depth, results) => {
|
|
586
|
+
if (!isSourceNode2(alternate)) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (alternate.type === "IfStatement") {
|
|
590
|
+
walkIfChain(alternate, depth, results);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
walk(alternate, depth + 1, results);
|
|
594
|
+
};
|
|
595
|
+
var walkIfChain = (node, depth, results) => {
|
|
596
|
+
const chainDepth = depth + 1;
|
|
597
|
+
checkDepth(chainDepth, node.loc?.start.line, results);
|
|
598
|
+
walkIfChild(node.test, chainDepth, results);
|
|
599
|
+
walkIfChild(node.consequent, chainDepth, results);
|
|
600
|
+
walkIfAlternate(node.alternate, depth, results);
|
|
601
|
+
};
|
|
602
|
+
var walkFunction = (node, _depth, results) => {
|
|
603
|
+
walkChildren(node, 0, results);
|
|
604
|
+
};
|
|
605
|
+
var walkNestingBlock = (node, depth, results) => {
|
|
606
|
+
const newDepth = depth + 1;
|
|
607
|
+
checkDepth(newDepth, node.loc?.start.line, results);
|
|
608
|
+
walkChildren(node, newDepth, results);
|
|
609
|
+
};
|
|
610
|
+
var selectWalker = (node) => {
|
|
611
|
+
return FUNCTION_TYPES.has(node.type) ? walkFunction : node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
|
|
612
|
+
};
|
|
613
|
+
var walk = (node, depth, results) => {
|
|
614
|
+
selectWalker(node)(node, depth, results);
|
|
615
|
+
};
|
|
616
|
+
var findDeepNesting = (filePath, content) => {
|
|
617
|
+
const results = [];
|
|
618
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
619
|
+
walk(sourceFile, 0, results);
|
|
620
|
+
return results;
|
|
621
|
+
};
|
|
622
|
+
var deepNestingRule = {
|
|
623
|
+
id: "deep-nesting",
|
|
624
|
+
description: "Detecta blocos aninhados al\xE9m do limite recomendado",
|
|
625
|
+
check(filePath, content) {
|
|
626
|
+
return findDeepNesting(filePath, content).map(({ line, depth }) => ({
|
|
627
|
+
ruleId: "deep-nesting",
|
|
628
|
+
message: `Bloco aninhado em profundidade ${depth} (limite recomendado: ${MAX_NESTING_DEPTH}) \u2014 considere extrair para uma fun\xE7\xE3o ou simplificar a condi\xE7\xE3o.`,
|
|
629
|
+
file: filePath,
|
|
630
|
+
line,
|
|
631
|
+
severity: "low"
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/commands/deep-nesting/deep-nesting.command.ts
|
|
637
|
+
var registerDeepNestingCommand = (program) => {
|
|
638
|
+
program.command("deep-nesting").description("Detecta blocos aninhados al\xE9m do limite recomendado").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
639
|
+
(path, options) => scanAndReport(path, [deepNestingRule], "Checking nesting depth...", options)
|
|
640
|
+
);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// src/scanner/dependency-audit.ts
|
|
644
|
+
import { execFile as execFile2 } from "child_process";
|
|
645
|
+
import { promisify as promisify2 } from "util";
|
|
646
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
647
|
+
var SEVERITY_MAP = {
|
|
648
|
+
info: "low",
|
|
649
|
+
low: "low",
|
|
650
|
+
moderate: "medium",
|
|
651
|
+
high: "high",
|
|
652
|
+
critical: "critical"
|
|
653
|
+
};
|
|
654
|
+
var vulnerabilityTitle = (vulnerability) => {
|
|
655
|
+
const firstVia = vulnerability.via[0];
|
|
656
|
+
if (typeof firstVia === "object" && firstVia?.title) {
|
|
657
|
+
return firstVia.title;
|
|
658
|
+
}
|
|
659
|
+
return 'ver "npm audit" para detalhes';
|
|
660
|
+
};
|
|
661
|
+
var mapAuditReportToFindings = (report) => {
|
|
662
|
+
return Object.values(report.vulnerabilities).map((vulnerability) => ({
|
|
663
|
+
ruleId: "dependency-audit",
|
|
664
|
+
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)}`,
|
|
665
|
+
file: "package.json",
|
|
666
|
+
line: 1,
|
|
667
|
+
severity: SEVERITY_MAP[vulnerability.severity]
|
|
668
|
+
}));
|
|
669
|
+
};
|
|
670
|
+
var runDependencyAudit = async (targetDir) => {
|
|
671
|
+
const startedAt = Date.now();
|
|
672
|
+
let stdout;
|
|
673
|
+
try {
|
|
674
|
+
({ stdout } = await execFileAsync2("npm", ["audit", "--json"], {
|
|
675
|
+
cwd: targetDir,
|
|
676
|
+
maxBuffer: 10 * 1024 * 1024
|
|
677
|
+
}));
|
|
678
|
+
} catch (error) {
|
|
679
|
+
const stdoutFromError = error.stdout;
|
|
680
|
+
if (!stdoutFromError) {
|
|
681
|
+
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
682
|
+
}
|
|
683
|
+
stdout = stdoutFromError;
|
|
684
|
+
}
|
|
685
|
+
const report = JSON.parse(stdout);
|
|
686
|
+
return {
|
|
687
|
+
scannedFiles: 1,
|
|
688
|
+
findings: mapAuditReportToFindings(report),
|
|
689
|
+
durationMs: Date.now() - startedAt
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/commands/dependency-audit/dependency-audit.command.ts
|
|
694
|
+
var printAuditResult = (result, json) => {
|
|
695
|
+
if (json) {
|
|
696
|
+
console.log(toJsonReport(result));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
printConsoleReport(result);
|
|
700
|
+
};
|
|
701
|
+
var auditAndReport = async (path, options) => {
|
|
702
|
+
try {
|
|
703
|
+
printAuditResult(await runDependencyAudit(path), options.json ?? false);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const message = error instanceof Error ? error.message : "erro desconhecido";
|
|
706
|
+
process.exitCode = 1;
|
|
707
|
+
console.error(`Falha ao auditar depend\xEAncias: ${message}`);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
var registerDependencyAuditCommand = (program) => {
|
|
711
|
+
program.command("dependency-audit").description(
|
|
712
|
+
'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit"; requer npm no PATH e acesso \xE0 rede)'
|
|
713
|
+
).argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").action((path, options) => auditAndReport(path, options));
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/rules/empty-catch.rule.ts
|
|
717
|
+
var isEmptyCatchClause = (node) => {
|
|
718
|
+
if (node.type !== "CatchClause") {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
const body = node.body;
|
|
722
|
+
return typeof body === "object" && body !== null && "type" in body && body.type === "BlockStatement" && Array.isArray(body.body) && body.body.length === 0;
|
|
723
|
+
};
|
|
724
|
+
var findEmptyCatchLines = (filePath, content) => {
|
|
725
|
+
const lines = /* @__PURE__ */ new Set();
|
|
726
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
727
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
728
|
+
if (isEmptyCatchClause(node) && node.loc) {
|
|
729
|
+
lines.add(node.loc.start.line);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
return [...lines];
|
|
733
|
+
};
|
|
734
|
+
var emptyCatchRule = {
|
|
735
|
+
id: "empty-catch",
|
|
736
|
+
description: "Detecta blocos catch vazios, que escondem erros silenciosamente",
|
|
737
|
+
check(filePath, content) {
|
|
738
|
+
return findEmptyCatchLines(filePath, content).map((line) => ({
|
|
739
|
+
ruleId: "empty-catch",
|
|
740
|
+
message: "Bloco catch vazio \u2014 o erro est\xE1 sendo engolido silenciosamente",
|
|
741
|
+
file: filePath,
|
|
742
|
+
line,
|
|
743
|
+
severity: "medium"
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/commands/empty-catch/empty-catch.command.ts
|
|
749
|
+
var registerEmptyCatchCommand = (program) => {
|
|
750
|
+
program.command("empty-catch").description("Detecta blocos catch vazios").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
751
|
+
(path, options) => scanAndReport(path, [emptyCatchRule], "Checking empty catch blocks...", options)
|
|
752
|
+
);
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/rules/express-missing-body-limit.rule.ts
|
|
756
|
+
var BODY_PARSER_OBJECTS = /* @__PURE__ */ new Set(["express", "bodyParser"]);
|
|
757
|
+
var BODY_PARSER_METHODS = /* @__PURE__ */ new Set(["json", "urlencoded"]);
|
|
758
|
+
var hasLimitOption = (options) => {
|
|
759
|
+
if (options?.type !== "ObjectExpression") {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
const properties = options.properties;
|
|
763
|
+
return properties?.some((property) => {
|
|
764
|
+
const key = property.key;
|
|
765
|
+
return key?.type === "Identifier" && key.name === "limit";
|
|
766
|
+
}) ?? false;
|
|
767
|
+
};
|
|
768
|
+
var isBodyParserWithoutLimit = (node) => {
|
|
769
|
+
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
770
|
+
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
771
|
+
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
772
|
+
const isBodyParserCall = object?.type === "Identifier" && BODY_PARSER_OBJECTS.has(object.name) && property?.type === "Identifier" && BODY_PARSER_METHODS.has(property.name);
|
|
773
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
774
|
+
return isBodyParserCall && !hasLimitOption(args?.[0]);
|
|
775
|
+
};
|
|
776
|
+
var findMissingBodyLimitLines = (filePath, content) => {
|
|
777
|
+
const lines = /* @__PURE__ */ new Set();
|
|
778
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
779
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
780
|
+
if (isBodyParserWithoutLimit(node) && node.loc) {
|
|
781
|
+
lines.add(node.loc.start.line);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
return [...lines].sort((a, b) => a - b);
|
|
785
|
+
};
|
|
786
|
+
var expressMissingBodyLimitRule = {
|
|
787
|
+
id: "express-missing-body-limit",
|
|
788
|
+
description: "Detecta middlewares de body parsing do Express sem limite de tamanho de requisi\xE7\xE3o",
|
|
789
|
+
check(filePath, content) {
|
|
790
|
+
return findMissingBodyLimitLines(filePath, content).map((line) => ({
|
|
791
|
+
ruleId: "express-missing-body-limit",
|
|
792
|
+
message: 'Body parser sem "limit" configurado \u2014 risco de nega\xE7\xE3o de servi\xE7o por payload grande',
|
|
793
|
+
file: filePath,
|
|
794
|
+
line,
|
|
795
|
+
severity: "low"
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/commands/express-missing-body-limit/express-missing-body-limit.command.ts
|
|
801
|
+
var registerExpressMissingBodyLimitCommand = (program) => {
|
|
802
|
+
program.command("express-missing-body-limit").description("Detecta middlewares de body parsing do Express sem limite de tamanho de requisi\xE7\xE3o").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
803
|
+
(path, options) => scanAndReport(path, [expressMissingBodyLimitRule], "Checking Express body limits...", options)
|
|
804
|
+
);
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// src/commands/help/help.command.ts
|
|
808
|
+
import Table2 from "cli-table3";
|
|
809
|
+
var buildHelpTable = (commands) => {
|
|
810
|
+
const table = new Table2({ head: ["Comando", "Descri\xE7\xE3o"] });
|
|
811
|
+
for (const command of commands) {
|
|
812
|
+
table.push([command.name, command.description]);
|
|
813
|
+
}
|
|
814
|
+
return table.toString();
|
|
815
|
+
};
|
|
816
|
+
var registerHelpCommand = (program) => {
|
|
817
|
+
program.command("help").description("Lista os comandos dispon\xEDveis").action(() => {
|
|
818
|
+
const commands = program.commands.filter((command) => command.name() !== "help").map((command) => ({ name: command.name(), description: command.description() }));
|
|
819
|
+
console.log(buildHelpTable(commands));
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/rules/lib/function-info.ts
|
|
824
|
+
var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
825
|
+
"FunctionDeclaration",
|
|
826
|
+
"FunctionExpression",
|
|
827
|
+
"ArrowFunctionExpression",
|
|
828
|
+
"ObjectMethod",
|
|
829
|
+
"ClassMethod",
|
|
830
|
+
"ClassPrivateMethod"
|
|
831
|
+
]);
|
|
832
|
+
var isNodeRecord = (value) => {
|
|
833
|
+
return typeof value === "object" && value !== null;
|
|
834
|
+
};
|
|
835
|
+
var nameValue = (node) => {
|
|
836
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
837
|
+
};
|
|
838
|
+
var literalValue = (node) => {
|
|
839
|
+
return typeof node.value === "string" || typeof node.value === "number" ? String(node.value) : void 0;
|
|
840
|
+
};
|
|
841
|
+
var nodeName = (node) => {
|
|
842
|
+
if (!isNodeRecord(node)) {
|
|
843
|
+
return void 0;
|
|
844
|
+
}
|
|
845
|
+
return nameValue(node) ?? literalValue(node) ?? nodeName(node.id);
|
|
846
|
+
};
|
|
847
|
+
var PARENT_NAME_KEYS = {
|
|
848
|
+
VariableDeclarator: "id",
|
|
849
|
+
ObjectProperty: "key",
|
|
850
|
+
AssignmentExpression: "left"
|
|
851
|
+
};
|
|
852
|
+
var parentFunctionName = (parent) => {
|
|
853
|
+
const nameKey = parent ? PARENT_NAME_KEYS[parent.type] : void 0;
|
|
854
|
+
return nameKey ? nodeName(parent?.[nameKey]) : void 0;
|
|
855
|
+
};
|
|
856
|
+
var isConstructor = (node) => {
|
|
857
|
+
return node.type === "ClassMethod" && node.kind === "constructor";
|
|
858
|
+
};
|
|
859
|
+
var getFunctionName = (node, parent) => {
|
|
860
|
+
const ownName = nodeName(node.id) ?? nodeName(node.key);
|
|
861
|
+
return isConstructor(node) ? "constructor" : ownName ?? parentFunctionName(parent);
|
|
862
|
+
};
|
|
863
|
+
var getFunctionStartLine = (node, parent) => {
|
|
864
|
+
if (node.type === "ArrowFunctionExpression" && parent?.type === "VariableDeclarator") {
|
|
865
|
+
return parent.loc?.start.line;
|
|
866
|
+
}
|
|
867
|
+
return node.loc?.start.line;
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
// src/rules/lib/statement-count-rule.ts
|
|
871
|
+
var isSourceNode3 = (value) => {
|
|
872
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
873
|
+
};
|
|
874
|
+
var createContext = (node, parent) => {
|
|
875
|
+
return {
|
|
876
|
+
startLine: getFunctionStartLine(node, parent) ?? node.loc?.start.line ?? 0,
|
|
877
|
+
count: 0,
|
|
878
|
+
name: getFunctionName(node, parent)
|
|
879
|
+
};
|
|
880
|
+
};
|
|
881
|
+
var appendOverLimitContext = (context, state) => {
|
|
882
|
+
if (context.count > state.config.maxCount) {
|
|
883
|
+
state.results.push(context);
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
var walkChild = (value, parent, context, state) => {
|
|
887
|
+
if (Array.isArray(value)) {
|
|
888
|
+
value.filter(isSourceNode3).forEach((child) => walk2(child, parent, context, state));
|
|
889
|
+
} else if (isSourceNode3(value)) {
|
|
890
|
+
walk2(value, parent, context, state);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var walkChildren2 = (node, context, state) => {
|
|
894
|
+
for (const value of Object.values(node)) {
|
|
895
|
+
walkChild(value, node, context, state);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
var walkFunction2 = (node, parent, state) => {
|
|
899
|
+
const context = createContext(node, parent);
|
|
900
|
+
walkChildren2(node, context, state);
|
|
901
|
+
appendOverLimitContext(context, state);
|
|
902
|
+
};
|
|
903
|
+
var incrementStatementCount = (node, context, state) => {
|
|
904
|
+
if (context && state.config.statementTypes.has(node.type)) {
|
|
905
|
+
context.count += 1;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var walk2 = (node, parent, context, state) => {
|
|
909
|
+
if (FUNCTION_TYPES2.has(node.type)) {
|
|
910
|
+
walkFunction2(node, parent, state);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
incrementStatementCount(node, context, state);
|
|
914
|
+
walkChildren2(node, context, state);
|
|
915
|
+
};
|
|
916
|
+
var findFunctionStatementCounts = (filePath, content, config) => {
|
|
917
|
+
const state = { config, results: [] };
|
|
918
|
+
walkChildren2(parseSourceFile(filePath, content), void 0, state);
|
|
919
|
+
return state.results;
|
|
920
|
+
};
|
|
921
|
+
var toFinding = (filePath, config, result) => {
|
|
922
|
+
const subject = result.name ? `Fun\xE7\xE3o "${result.name}"` : "Fun\xE7\xE3o an\xF4nima";
|
|
923
|
+
return {
|
|
924
|
+
ruleId: config.id,
|
|
925
|
+
message: `${subject} tem ${result.count} ${config.unitLabel} \u2014 complexidade alta, considere refatorar (limite recomendado: ${config.maxCount})`,
|
|
926
|
+
file: filePath,
|
|
927
|
+
line: result.startLine,
|
|
928
|
+
severity: "low"
|
|
929
|
+
};
|
|
930
|
+
};
|
|
931
|
+
var createFunctionStatementCountRule = (config) => ({
|
|
932
|
+
id: config.id,
|
|
933
|
+
description: config.description,
|
|
934
|
+
check(filePath, content) {
|
|
935
|
+
return findFunctionStatementCounts(filePath, content, config).map(
|
|
936
|
+
(result) => toFinding(filePath, config, result)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// src/rules/high-complexity.rule.ts
|
|
942
|
+
var highComplexityRule = createFunctionStatementCountRule({
|
|
943
|
+
id: "high-complexity",
|
|
944
|
+
description: "Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)",
|
|
945
|
+
statementTypes: /* @__PURE__ */ new Set([
|
|
946
|
+
"IfStatement",
|
|
947
|
+
"ForStatement",
|
|
948
|
+
"ForInStatement",
|
|
949
|
+
"ForOfStatement",
|
|
950
|
+
"WhileStatement",
|
|
951
|
+
"DoWhileStatement",
|
|
952
|
+
"SwitchCase",
|
|
953
|
+
"CatchClause"
|
|
954
|
+
]),
|
|
955
|
+
maxCount: 5,
|
|
956
|
+
unitLabel: "condicionais/loops"
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// src/commands/high-complexity/high-complexity.command.ts
|
|
960
|
+
var registerHighComplexityCommand = (program) => {
|
|
961
|
+
program.command("high-complexity").description("Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
962
|
+
(path, options) => scanAndReport(path, [highComplexityRule], "Checking function complexity...", options)
|
|
963
|
+
);
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
// src/commands/init/init.command.ts
|
|
967
|
+
import { confirm, intro, isCancel, outro, text } from "@clack/prompts";
|
|
968
|
+
import chalk3 from "chalk";
|
|
969
|
+
var collectAnswers = async () => {
|
|
970
|
+
try {
|
|
971
|
+
const include = await text({
|
|
972
|
+
message: "Quais extens\xF5es de arquivo devem ser analisadas?",
|
|
973
|
+
placeholder: ".ts,.js",
|
|
974
|
+
defaultValue: ".ts,.js"
|
|
975
|
+
});
|
|
976
|
+
if (isCancel(include)) {
|
|
977
|
+
return void 0;
|
|
978
|
+
}
|
|
979
|
+
const shouldSave = await confirm({ message: "Salvar essa configura\xE7\xE3o?" });
|
|
980
|
+
if (isCancel(shouldSave)) {
|
|
981
|
+
return void 0;
|
|
982
|
+
}
|
|
983
|
+
return { include, shouldSave };
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw new Error("N\xE3o foi poss\xEDvel coletar as op\xE7\xF5es de configura\xE7\xE3o.", { cause: error });
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
var printOutcome = ({ include, shouldSave }) => {
|
|
989
|
+
const message = shouldSave ? chalk3.green(`Configura\xE7\xE3o recebida (${include}). Persist\xEAncia em arquivo ainda n\xE3o implementada.`) : "Configura\xE7\xE3o descartada.";
|
|
990
|
+
outro(message);
|
|
991
|
+
};
|
|
992
|
+
var runInit = () => {
|
|
993
|
+
intro(chalk3.cyan("CodeSentry \u2014 configura\xE7\xE3o inicial"));
|
|
994
|
+
return collectAnswers().then((answers) => {
|
|
995
|
+
if (!answers) {
|
|
996
|
+
outro("Cancelado.");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
printOutcome(answers);
|
|
1000
|
+
}).catch((error) => {
|
|
1001
|
+
const message = error instanceof Error ? error.message : "erro desconhecido";
|
|
1002
|
+
outro(chalk3.red(`Falha ao configurar o CodeSentry: ${message}`));
|
|
1003
|
+
process.exitCode = 1;
|
|
1004
|
+
});
|
|
1005
|
+
};
|
|
1006
|
+
var registerInitCommand = (program) => {
|
|
1007
|
+
program.command("init").description("Configura o CodeSentry no projeto atual (interativo)").action(() => runInit());
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// src/rules/insecure-random-token.rule.ts
|
|
1011
|
+
var TOKEN_NAME_PATTERN = /token|secret|password|senha|session|csrf|nonce|otp/i;
|
|
1012
|
+
var isMathRandomCall = (node) => {
|
|
1013
|
+
if (node.type !== "CallExpression") {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
const callee = node.callee;
|
|
1017
|
+
if (callee?.type !== "MemberExpression") {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const object = callee.object;
|
|
1021
|
+
const property = callee.property;
|
|
1022
|
+
return object?.type === "Identifier" && object.name === "Math" && property?.type === "Identifier" && property.name === "random";
|
|
1023
|
+
};
|
|
1024
|
+
var HASH_LIKE_NAME_PATTERN = /hash|md5|sha1/i;
|
|
1025
|
+
var calleeName = (callee) => {
|
|
1026
|
+
if (callee?.type === "Identifier") {
|
|
1027
|
+
return callee.name;
|
|
1028
|
+
}
|
|
1029
|
+
if (callee?.type === "MemberExpression") {
|
|
1030
|
+
const property = callee.property;
|
|
1031
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
1032
|
+
}
|
|
1033
|
+
return void 0;
|
|
1034
|
+
};
|
|
1035
|
+
var isDateNowOrGetTimeCall = (node) => {
|
|
1036
|
+
if (node.type !== "CallExpression") {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
const callee = node.callee;
|
|
1040
|
+
if (callee?.type !== "MemberExpression") {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
const property = callee.property;
|
|
1044
|
+
const propertyName = property?.type === "Identifier" ? property.name : void 0;
|
|
1045
|
+
if (propertyName === "getTime") {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
const object = callee.object;
|
|
1049
|
+
return object?.type === "Identifier" && object.name === "Date" && propertyName === "now";
|
|
1050
|
+
};
|
|
1051
|
+
var TIMESTAMP_NAME_PATTERN = /timestamp|^now$/i;
|
|
1052
|
+
var containsPredictableTimestamp = (node) => {
|
|
1053
|
+
if (!node) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
let found = false;
|
|
1057
|
+
visitSourceNodes(node, (child) => {
|
|
1058
|
+
if (isDateNowOrGetTimeCall(child)) {
|
|
1059
|
+
found = true;
|
|
1060
|
+
} else if (child.type === "Identifier" && TIMESTAMP_NAME_PATTERN.test(child.name)) {
|
|
1061
|
+
found = true;
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
return found;
|
|
1065
|
+
};
|
|
1066
|
+
var isPredictableHashCall = (node) => {
|
|
1067
|
+
if (node.type !== "CallExpression") {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
const name = calleeName(node.callee);
|
|
1071
|
+
if (!name || !HASH_LIKE_NAME_PATTERN.test(name)) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
const args = node.arguments;
|
|
1075
|
+
return containsPredictableTimestamp(args?.[0]);
|
|
1076
|
+
};
|
|
1077
|
+
var containsInsecureValueGeneration = (node) => {
|
|
1078
|
+
if (!node) {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
let found = false;
|
|
1082
|
+
visitSourceNodes(node, (child) => {
|
|
1083
|
+
if (isMathRandomCall(child) || isPredictableHashCall(child)) {
|
|
1084
|
+
found = true;
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
return found;
|
|
1088
|
+
};
|
|
1089
|
+
var findInsecureRandomTokenLines = (filePath, content) => {
|
|
1090
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1091
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1092
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1093
|
+
if (node.type !== "VariableDeclarator") {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const id = node.id;
|
|
1097
|
+
const init = node.init;
|
|
1098
|
+
if (id?.type === "Identifier" && TOKEN_NAME_PATTERN.test(id.name) && containsInsecureValueGeneration(init) && node.loc) {
|
|
1099
|
+
lines.add(node.loc.start.line);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
return [...lines].sort((a, b) => a - b);
|
|
1103
|
+
};
|
|
1104
|
+
var insecureRandomTokenRule = {
|
|
1105
|
+
id: "insecure-random-token",
|
|
1106
|
+
description: "Detecta tokens/segredos gerados de forma previs\xEDvel (Math.random(), ou hash de um valor previs\xEDvel como um timestamp)",
|
|
1107
|
+
check(filePath, content) {
|
|
1108
|
+
return findInsecureRandomTokenLines(filePath, content).map((line) => ({
|
|
1109
|
+
ruleId: "insecure-random-token",
|
|
1110
|
+
message: "Token gerado de forma previs\xEDvel \u2014 use crypto.randomBytes()/randomUUID() em vez de Math.random() ou hash de um timestamp",
|
|
1111
|
+
file: filePath,
|
|
1112
|
+
line,
|
|
1113
|
+
severity: "high"
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/commands/insecure-random-token/insecure-random-token.command.ts
|
|
1119
|
+
var registerInsecureRandomTokenCommand = (program) => {
|
|
1120
|
+
program.command("insecure-random-token").description("Detecta o uso de Math.random() para gerar tokens/segredos previs\xEDveis").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1121
|
+
(path, options) => scanAndReport(path, [insecureRandomTokenRule], "Checking insecure random tokens...", options)
|
|
1122
|
+
);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// src/rules/jwt-decode-without-verify.rule.ts
|
|
1126
|
+
var isBufferFromBase64Call = (node) => {
|
|
1127
|
+
const callee = node.callee;
|
|
1128
|
+
if (callee?.type !== "MemberExpression") {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
const object = callee.object;
|
|
1132
|
+
const property = callee.property;
|
|
1133
|
+
const isBufferFrom = object?.type === "Identifier" && object.name === "Buffer" && property?.type === "Identifier" && property.name === "from";
|
|
1134
|
+
if (!isBufferFrom) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
const args = node.arguments;
|
|
1138
|
+
return args?.[1]?.type === "StringLiteral" && args[1].value === "base64";
|
|
1139
|
+
};
|
|
1140
|
+
var isBase64DecodeCall = (node) => {
|
|
1141
|
+
if (node.type !== "CallExpression") {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
const callee = node.callee;
|
|
1145
|
+
if (callee?.type === "Identifier" && callee.name === "atob") {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
return isBufferFromBase64Call(node);
|
|
1149
|
+
};
|
|
1150
|
+
var isJsonParseCall = (node) => {
|
|
1151
|
+
if (node.type !== "CallExpression") {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
const callee = node.callee;
|
|
1155
|
+
return callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "JSON" && callee.property?.type === "Identifier" && callee.property.name === "parse";
|
|
1156
|
+
};
|
|
1157
|
+
var isSignatureVerificationCall = (node) => {
|
|
1158
|
+
if (node.type !== "CallExpression") {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
const callee = node.callee;
|
|
1162
|
+
return callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "verify";
|
|
1163
|
+
};
|
|
1164
|
+
var analyzeFile = (sourceFile) => {
|
|
1165
|
+
const analysis = {
|
|
1166
|
+
base64DecodeLines: [],
|
|
1167
|
+
hasJsonParse: false,
|
|
1168
|
+
hasSignatureVerification: false
|
|
1169
|
+
};
|
|
1170
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1171
|
+
if (isBase64DecodeCall(node) && node.loc) {
|
|
1172
|
+
analysis.base64DecodeLines.push(node.loc.start.line);
|
|
1173
|
+
} else if (isJsonParseCall(node)) {
|
|
1174
|
+
analysis.hasJsonParse = true;
|
|
1175
|
+
} else if (isSignatureVerificationCall(node)) {
|
|
1176
|
+
analysis.hasSignatureVerification = true;
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
return analysis;
|
|
1180
|
+
};
|
|
1181
|
+
var findJwtDecodeWithoutVerifyLines = (filePath, content) => {
|
|
1182
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1183
|
+
const analysis = analyzeFile(sourceFile);
|
|
1184
|
+
if (!analysis.hasJsonParse || analysis.hasSignatureVerification) {
|
|
1185
|
+
return [];
|
|
1186
|
+
}
|
|
1187
|
+
return [...new Set(analysis.base64DecodeLines)].sort((a, b) => a - b);
|
|
1188
|
+
};
|
|
1189
|
+
var jwtDecodeWithoutVerifyRule = {
|
|
1190
|
+
id: "jwt-decode-without-verify",
|
|
1191
|
+
description: "Detecta decodifica\xE7\xE3o manual de um token (base64 + JSON.parse) sem nenhuma verifica\xE7\xE3o de assinatura no arquivo",
|
|
1192
|
+
check(filePath, content) {
|
|
1193
|
+
return findJwtDecodeWithoutVerifyLines(filePath, content).map((line) => ({
|
|
1194
|
+
ruleId: "jwt-decode-without-verify",
|
|
1195
|
+
message: "Token decodificado (base64 + JSON.parse) sem verificar a assinatura \u2014 claims n\xE3o confi\xE1veis podem ser forjadas",
|
|
1196
|
+
file: filePath,
|
|
1197
|
+
line,
|
|
1198
|
+
severity: "critical"
|
|
1199
|
+
}));
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// src/commands/jwt-decode-without-verify/jwt-decode-without-verify.command.ts
|
|
1204
|
+
var registerJwtDecodeWithoutVerifyCommand = (program) => {
|
|
1205
|
+
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) => {
|
|
1206
|
+
await scanAndReport(
|
|
1207
|
+
path,
|
|
1208
|
+
[jwtDecodeWithoutVerifyRule],
|
|
1209
|
+
"Checking JWT decode without verification...",
|
|
1210
|
+
options
|
|
1211
|
+
);
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// src/rules/jwt-no-expiration.rule.ts
|
|
1216
|
+
var objectHasProperty = (object, propertyName) => {
|
|
1217
|
+
if (object?.type !== "ObjectExpression") {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
const properties = object.properties;
|
|
1221
|
+
return properties?.some((property) => {
|
|
1222
|
+
const key = property.key;
|
|
1223
|
+
return key?.type === "Identifier" && key.name === propertyName;
|
|
1224
|
+
}) ?? false;
|
|
1225
|
+
};
|
|
1226
|
+
var isJwtSignWithoutExpiration = (node) => {
|
|
1227
|
+
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
1228
|
+
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
1229
|
+
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
1230
|
+
const isJwtSignCall = object?.type === "Identifier" && object.name === "jwt" && property?.type === "Identifier" && property.name === "sign";
|
|
1231
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
1232
|
+
const payload = args?.[0];
|
|
1233
|
+
const options = args?.[2];
|
|
1234
|
+
const hasExpInPayload = objectHasProperty(payload, "exp");
|
|
1235
|
+
const hasExpiresInOption = objectHasProperty(options, "expiresIn");
|
|
1236
|
+
return isJwtSignCall && !hasExpInPayload && !hasExpiresInOption;
|
|
1237
|
+
};
|
|
1238
|
+
var findJwtNoExpirationLines = (filePath, content) => {
|
|
1239
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1240
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1241
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1242
|
+
if (isJwtSignWithoutExpiration(node) && node.loc) {
|
|
1243
|
+
lines.add(node.loc.start.line);
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
return [...lines].sort((a, b) => a - b);
|
|
1247
|
+
};
|
|
1248
|
+
var jwtNoExpirationRule = {
|
|
1249
|
+
id: "jwt-no-expiration",
|
|
1250
|
+
description: "Detecta jwt.sign() sem expira\xE7\xE3o configurada",
|
|
1251
|
+
check(filePath, content) {
|
|
1252
|
+
return findJwtNoExpirationLines(filePath, content).map((line) => ({
|
|
1253
|
+
ruleId: "jwt-no-expiration",
|
|
1254
|
+
message: "jwt.sign() sem expira\xE7\xE3o \u2014 o token nunca expira",
|
|
1255
|
+
file: filePath,
|
|
1256
|
+
line,
|
|
1257
|
+
severity: "medium"
|
|
1258
|
+
}));
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
// src/commands/jwt-no-expiration/jwt-no-expiration.command.ts
|
|
1263
|
+
var registerJwtNoExpirationCommand = (program) => {
|
|
1264
|
+
program.command("jwt-no-expiration").description("Detecta jwt.sign() sem expira\xE7\xE3o configurada").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1265
|
+
(path, options) => scanAndReport(path, [jwtNoExpirationRule], "Checking JWT expiration...", options)
|
|
1266
|
+
);
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// src/rules/long-function.rule.ts
|
|
1270
|
+
var MAX_LINES = 30;
|
|
1271
|
+
var toLongFunction = (node, parent) => {
|
|
1272
|
+
const startLine = getFunctionStartLine(node, parent);
|
|
1273
|
+
const endLine = node.loc?.end.line;
|
|
1274
|
+
if (startLine === void 0 || endLine === void 0) {
|
|
1275
|
+
return void 0;
|
|
1276
|
+
}
|
|
1277
|
+
const totalLines = endLine - startLine + 1;
|
|
1278
|
+
if (totalLines <= MAX_LINES) {
|
|
1279
|
+
return void 0;
|
|
1280
|
+
}
|
|
1281
|
+
return { startLine, totalLines, name: getFunctionName(node, parent) };
|
|
1282
|
+
};
|
|
1283
|
+
var findLongFunctions = (filePath, content) => {
|
|
1284
|
+
const results = [];
|
|
1285
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1286
|
+
visitSourceNodes(sourceFile, (node, parent) => {
|
|
1287
|
+
if (FUNCTION_TYPES2.has(node.type)) {
|
|
1288
|
+
const longFunction = toLongFunction(node, parent);
|
|
1289
|
+
if (longFunction) {
|
|
1290
|
+
results.push(longFunction);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
return results;
|
|
1295
|
+
};
|
|
1296
|
+
var toFinding2 = (filePath, longFunction) => {
|
|
1297
|
+
const { startLine, totalLines, name } = longFunction;
|
|
1298
|
+
const subject = name ? `Fun\xE7\xE3o "${name}"` : "Fun\xE7\xE3o an\xF4nima";
|
|
1299
|
+
return {
|
|
1300
|
+
ruleId: "long-function",
|
|
1301
|
+
message: `${subject} com ${totalLines} linhas \u2014 considere dividir em fun\xE7\xF5es menores (limite recomendado: ${MAX_LINES})`,
|
|
1302
|
+
file: filePath,
|
|
1303
|
+
line: startLine,
|
|
1304
|
+
severity: "low"
|
|
1305
|
+
};
|
|
1306
|
+
};
|
|
1307
|
+
var longFunctionRule = {
|
|
1308
|
+
id: "long-function",
|
|
1309
|
+
description: "Detecta fun\xE7\xF5es com mais de 30 linhas",
|
|
1310
|
+
check(filePath, content) {
|
|
1311
|
+
return findLongFunctions(filePath, content).map((longFunction) => toFinding2(filePath, longFunction));
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
// src/commands/long-functions/long-functions.command.ts
|
|
1316
|
+
var registerLongFunctionsCommand = (program) => {
|
|
1317
|
+
program.command("long-functions").description("Detecta fun\xE7\xF5es com mais de 30 linhas").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1318
|
+
(path, options) => scanAndReport(path, [longFunctionRule], "Checking function length...", options)
|
|
1319
|
+
);
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/rules/no-any.rule.ts
|
|
1323
|
+
var findAnyUsageLines = (filePath, content) => {
|
|
1324
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1325
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1326
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1327
|
+
if (node.type === "TSAnyKeyword" && node.loc) {
|
|
1328
|
+
lines.add(node.loc.start.line);
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
return [...lines].sort((a, b) => a - b);
|
|
1332
|
+
};
|
|
1333
|
+
var noAnyRule = {
|
|
1334
|
+
id: "no-any",
|
|
1335
|
+
description: 'Detecta o uso do tipo "any", que enfraquece a seguran\xE7a de tipos do TypeScript',
|
|
1336
|
+
check(filePath, content) {
|
|
1337
|
+
return findAnyUsageLines(filePath, content).map((line) => ({
|
|
1338
|
+
ruleId: "no-any",
|
|
1339
|
+
message: 'Uso do tipo "any" encontrado \u2014 considere um tipo mais espec\xEDfico',
|
|
1340
|
+
file: filePath,
|
|
1341
|
+
line,
|
|
1342
|
+
severity: "low"
|
|
1343
|
+
}));
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
// src/commands/no-any/no-any.command.ts
|
|
1348
|
+
var registerNoAnyCommand = (program) => {
|
|
1349
|
+
program.command("no-any").description('Detecta o uso do tipo "any" no TypeScript').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1350
|
+
(path, options) => scanAndReport(path, [noAnyRule], "Checking any usage...", options)
|
|
1351
|
+
);
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// src/rules/no-eval.rule.ts
|
|
1355
|
+
var isNamed = (node, name) => {
|
|
1356
|
+
return typeof node === "object" && node !== null && "type" in node && "name" in node && node.type === "Identifier" && node.name === name;
|
|
1357
|
+
};
|
|
1358
|
+
var isEvalCall = (node) => {
|
|
1359
|
+
if (node.type !== "CallExpression" && node.type !== "OptionalCallExpression") {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
if (isNamed(node.callee, "eval")) {
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
const callee = node.callee;
|
|
1366
|
+
return typeof callee === "object" && callee !== null && "type" in callee && (callee.type === "MemberExpression" || callee.type === "OptionalMemberExpression") && "property" in callee && isNamed(callee.property, "eval");
|
|
1367
|
+
};
|
|
1368
|
+
var isNewFunctionCall = (node) => {
|
|
1369
|
+
return node.type === "NewExpression" && isNamed(node.callee, "Function");
|
|
1370
|
+
};
|
|
1371
|
+
var findEvalCallLines = (filePath, content) => {
|
|
1372
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1373
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1374
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1375
|
+
if ((isEvalCall(node) || isNewFunctionCall(node)) && node.loc) {
|
|
1376
|
+
lines.add(node.loc.start.line);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
return [...lines];
|
|
1380
|
+
};
|
|
1381
|
+
var noEvalRule = {
|
|
1382
|
+
id: "no-eval",
|
|
1383
|
+
description: "Detecta o uso de eval() ou new Function(), que podem executar c\xF3digo arbitr\xE1rio",
|
|
1384
|
+
check(filePath, content) {
|
|
1385
|
+
return findEvalCallLines(filePath, content).map((line) => ({
|
|
1386
|
+
ruleId: "no-eval",
|
|
1387
|
+
message: "Uso de eval()/new Function() encontrado \u2014 evite executar c\xF3digo arbitr\xE1rio",
|
|
1388
|
+
file: filePath,
|
|
1389
|
+
line,
|
|
1390
|
+
severity: "high"
|
|
1391
|
+
}));
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// src/commands/no-eval/no-eval.command.ts
|
|
1396
|
+
var registerNoEvalCommand = (program) => {
|
|
1397
|
+
program.command("no-eval").description("Detecta o uso de eval() ou new Function()").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1398
|
+
(path, options) => scanAndReport(path, [noEvalRule], "Checking eval/new Function usage...", options)
|
|
1399
|
+
);
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/rules/no-hardcoded-secret.rule.ts
|
|
1403
|
+
var SECRET_NAME_PATTERN = /password|senha|secret|token|apikey|api_key|private_key|access_key/i;
|
|
1404
|
+
var PUBLIC_METADATA_NAME_PATTERN = /scope|issuer|ttl|expir/i;
|
|
1405
|
+
var isNonEmptyStringLiteral = (node) => {
|
|
1406
|
+
return node?.type === "StringLiteral" && node.value.trim().length > 0;
|
|
1407
|
+
};
|
|
1408
|
+
var nameFromKeyLike = (node) => {
|
|
1409
|
+
if (node?.type === "Identifier") {
|
|
1410
|
+
return node.name;
|
|
1411
|
+
}
|
|
1412
|
+
if (node?.type === "StringLiteral") {
|
|
1413
|
+
return node.value;
|
|
1414
|
+
}
|
|
1415
|
+
return void 0;
|
|
1416
|
+
};
|
|
1417
|
+
var assignmentNodes = (node) => {
|
|
1418
|
+
switch (node.type) {
|
|
1419
|
+
case "VariableDeclarator":
|
|
1420
|
+
return { nameNode: node.id, valueNode: node.init };
|
|
1421
|
+
case "ObjectProperty":
|
|
1422
|
+
return { nameNode: node.key, valueNode: node.value };
|
|
1423
|
+
case "AssignmentExpression": {
|
|
1424
|
+
const left = node.left;
|
|
1425
|
+
return {
|
|
1426
|
+
nameNode: left.type === "MemberExpression" ? left.property : left,
|
|
1427
|
+
valueNode: node.right
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
default:
|
|
1431
|
+
return {};
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
var isHardcodedSecretAssignment = (node) => {
|
|
1435
|
+
const { nameNode, valueNode } = assignmentNodes(node);
|
|
1436
|
+
const name = nameFromKeyLike(nameNode);
|
|
1437
|
+
return !!name && SECRET_NAME_PATTERN.test(name) && !PUBLIC_METADATA_NAME_PATTERN.test(name) && isNonEmptyStringLiteral(valueNode);
|
|
1438
|
+
};
|
|
1439
|
+
var findHardcodedSecretLines = (filePath, content) => {
|
|
1440
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1441
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1442
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1443
|
+
if (isHardcodedSecretAssignment(node) && node.loc) {
|
|
1444
|
+
lines.add(node.loc.start.line);
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
return [...lines].sort((a, b) => a - b);
|
|
1448
|
+
};
|
|
1449
|
+
var noHardcodedSecretRule = {
|
|
1450
|
+
id: "no-hardcoded-secret",
|
|
1451
|
+
description: "Detecta segredos/credenciais hardcoded no c\xF3digo-fonte",
|
|
1452
|
+
check(filePath, content) {
|
|
1453
|
+
return findHardcodedSecretLines(filePath, content).map((line) => ({
|
|
1454
|
+
ruleId: "no-hardcoded-secret",
|
|
1455
|
+
message: "Poss\xEDvel segredo/credencial hardcoded no c\xF3digo-fonte",
|
|
1456
|
+
file: filePath,
|
|
1457
|
+
line,
|
|
1458
|
+
severity: "critical"
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
// src/commands/no-hardcoded-secret/no-hardcoded-secret.command.ts
|
|
1464
|
+
var registerNoHardcodedSecretCommand = (program) => {
|
|
1465
|
+
program.command("no-hardcoded-secret").description("Detecta segredos/credenciais hardcoded no c\xF3digo-fonte").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1466
|
+
(path, options) => scanAndReport(path, [noHardcodedSecretRule], "Checking hardcoded secrets...", options)
|
|
1467
|
+
);
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
// src/rules/permissive-cors.rule.ts
|
|
1471
|
+
var HEADER_SETTER_METHODS = /* @__PURE__ */ new Set(["setHeader", "header"]);
|
|
1472
|
+
var isWildcardOriginOption = (options) => {
|
|
1473
|
+
if (options?.type !== "ObjectExpression") {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
const properties = options.properties;
|
|
1477
|
+
return properties?.some((property) => {
|
|
1478
|
+
const key = property.key;
|
|
1479
|
+
const value = property.value;
|
|
1480
|
+
return key?.type === "Identifier" && key.name === "origin" && value?.type === "StringLiteral" && value.value === "*";
|
|
1481
|
+
}) ?? false;
|
|
1482
|
+
};
|
|
1483
|
+
var isPermissiveCorsCall = (node) => {
|
|
1484
|
+
if (node.type !== "CallExpression") {
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
const callee = node.callee;
|
|
1488
|
+
if (callee?.type !== "Identifier" || callee.name !== "cors") {
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
const args = node.arguments;
|
|
1492
|
+
return (args?.length ?? 0) === 0 || isWildcardOriginOption(args?.[0]);
|
|
1493
|
+
};
|
|
1494
|
+
var isWildcardOriginHeader = (node) => {
|
|
1495
|
+
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
1496
|
+
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
1497
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
1498
|
+
const headerName = args?.[0];
|
|
1499
|
+
const headerValue = args?.[1];
|
|
1500
|
+
return property?.type === "Identifier" && HEADER_SETTER_METHODS.has(property.name) && headerName?.type === "StringLiteral" && headerName.value === "Access-Control-Allow-Origin" && headerValue?.type === "StringLiteral" && headerValue.value === "*";
|
|
1501
|
+
};
|
|
1502
|
+
var findPermissiveCorsLines = (filePath, content) => {
|
|
1503
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1504
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1505
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1506
|
+
if ((isPermissiveCorsCall(node) || isWildcardOriginHeader(node)) && node.loc) {
|
|
1507
|
+
lines.add(node.loc.start.line);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
return [...lines].sort((a, b) => a - b);
|
|
1511
|
+
};
|
|
1512
|
+
var permissiveCorsRule = {
|
|
1513
|
+
id: "permissive-cors",
|
|
1514
|
+
description: "Detecta CORS configurado para permitir qualquer origem",
|
|
1515
|
+
check(filePath, content) {
|
|
1516
|
+
return findPermissiveCorsLines(filePath, content).map((line) => ({
|
|
1517
|
+
ruleId: "permissive-cors",
|
|
1518
|
+
message: 'CORS permitindo qualquer origem ("*") \u2014 restrinja para as origens confi\xE1veis',
|
|
1519
|
+
file: filePath,
|
|
1520
|
+
line,
|
|
1521
|
+
severity: "medium"
|
|
1522
|
+
}));
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
// src/commands/permissive-cors/permissive-cors.command.ts
|
|
1527
|
+
var registerPermissiveCorsCommand = (program) => {
|
|
1528
|
+
program.command("permissive-cors").description("Detecta CORS configurado para permitir qualquer origem").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
1529
|
+
(path, options) => scanAndReport(path, [permissiveCorsRule], "Checking permissive CORS...", options)
|
|
1530
|
+
);
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// src/rules/public-env-var-secret.rule.ts
|
|
1534
|
+
var PUBLIC_ENV_PREFIX_PATTERN = /^(NEXT_PUBLIC_|VITE_|REACT_APP_)/;
|
|
1535
|
+
var SENSITIVE_NAME_PATTERN = /secret|key|token|password|senha/i;
|
|
1536
|
+
var isPublicSecretEnvAccess = (node) => {
|
|
1537
|
+
if (node.type !== "MemberExpression") {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
const property = node.property;
|
|
1541
|
+
if (property?.type !== "Identifier") {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
const name = property.name;
|
|
1545
|
+
return PUBLIC_ENV_PREFIX_PATTERN.test(name) && SENSITIVE_NAME_PATTERN.test(name);
|
|
1546
|
+
};
|
|
1547
|
+
var findPublicEnvVarSecretLines = (filePath, content) => {
|
|
1548
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1549
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1550
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1551
|
+
if (isPublicSecretEnvAccess(node) && node.loc) {
|
|
1552
|
+
lines.add(node.loc.start.line);
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
return [...lines].sort((a, b) => a - b);
|
|
1556
|
+
};
|
|
1557
|
+
var publicEnvVarSecretRule = {
|
|
1558
|
+
id: "public-env-var-secret",
|
|
1559
|
+
description: "Detecta uma vari\xE1vel de ambiente p\xFAblica (NEXT_PUBLIC_/VITE_/REACT_APP_) com nome de segredo",
|
|
1560
|
+
check(filePath, content) {
|
|
1561
|
+
return findPublicEnvVarSecretLines(filePath, content).map((line) => ({
|
|
1562
|
+
ruleId: "public-env-var-secret",
|
|
1563
|
+
message: "Vari\xE1vel de ambiente p\xFAblica com nome de segredo \u2014 o prefixo faz o valor ser inclu\xEDdo no bundle enviado ao navegador, nunca pode ser secreto",
|
|
1564
|
+
file: filePath,
|
|
1565
|
+
line,
|
|
1566
|
+
severity: "high"
|
|
1567
|
+
}));
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
// src/commands/public-env-var-secret/public-env-var-secret.command.ts
|
|
1572
|
+
var registerPublicEnvVarSecretCommand = (program) => {
|
|
1573
|
+
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) => {
|
|
1574
|
+
await scanAndReport(path, [publicEnvVarSecretRule], "Checking public env var secrets...", options);
|
|
1575
|
+
});
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
// src/commands/rules/rules.command.ts
|
|
1579
|
+
import Table3 from "cli-table3";
|
|
1580
|
+
|
|
1581
|
+
// src/rules/lib/ast-ancestors.ts
|
|
1582
|
+
var isSourceNode4 = (value) => {
|
|
1583
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
1584
|
+
};
|
|
1585
|
+
var walkWithAncestors = (node, visitor, ancestors = []) => {
|
|
1586
|
+
visitor(node, ancestors);
|
|
1587
|
+
const nextAncestors = [node, ...ancestors];
|
|
1588
|
+
for (const value of Object.values(node)) {
|
|
1589
|
+
if (Array.isArray(value)) {
|
|
1590
|
+
value.filter(isSourceNode4).forEach((child) => walkWithAncestors(child, visitor, nextAncestors));
|
|
1591
|
+
} else if (isSourceNode4(value)) {
|
|
1592
|
+
walkWithAncestors(value, visitor, nextAncestors);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
// src/rules/await-no-try-catch.rule.ts
|
|
1598
|
+
var CLASS_TYPES = /* @__PURE__ */ new Set(["ClassDeclaration", "ClassExpression"]);
|
|
1599
|
+
var TEST_FILE_PATH = /(^|[\\/])(?:__tests__|tests?)[\\/]|\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
1600
|
+
var isNestedInsideClass = (ancestors) => {
|
|
1601
|
+
return ancestors.some((ancestor) => CLASS_TYPES.has(ancestor.type));
|
|
1602
|
+
};
|
|
1603
|
+
var isInsideTryBlockBeforeFunctionBoundary = (awaitNode, ancestors) => {
|
|
1604
|
+
let child = awaitNode;
|
|
1605
|
+
for (const ancestor of ancestors) {
|
|
1606
|
+
if (FUNCTION_TYPES2.has(ancestor.type)) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
if (ancestor.type === "TryStatement" && ancestor.block === child) {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
child = ancestor;
|
|
1613
|
+
}
|
|
1614
|
+
return false;
|
|
1615
|
+
};
|
|
1616
|
+
var findUnprotectedAwaitLines = (filePath, content) => {
|
|
1617
|
+
if (TEST_FILE_PATH.test(filePath)) {
|
|
1618
|
+
return [];
|
|
1619
|
+
}
|
|
1620
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1621
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1622
|
+
walkWithAncestors(sourceFile, (node, ancestors) => {
|
|
1623
|
+
const location = node.loc;
|
|
1624
|
+
const isUnprotectedAwait = node.type === "AwaitExpression" && location && !isNestedInsideClass(ancestors) && !isInsideTryBlockBeforeFunctionBoundary(node, ancestors);
|
|
1625
|
+
if (isUnprotectedAwait) {
|
|
1626
|
+
lines.add(location.start.line);
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
return [...lines].sort((a, b) => a - b);
|
|
1630
|
+
};
|
|
1631
|
+
var awaitNoTryCatchRule = {
|
|
1632
|
+
id: "await-no-try-catch",
|
|
1633
|
+
description: "Detecta await fora de try/catch em c\xF3digo de produ\xE7\xE3o (arquivos de teste e m\xE9todos de classe s\xE3o ignorados)",
|
|
1634
|
+
check(filePath, content) {
|
|
1635
|
+
return findUnprotectedAwaitLines(filePath, content).map((line) => ({
|
|
1636
|
+
ruleId: "await-no-try-catch",
|
|
1637
|
+
message: "await fora de um bloco try/catch \u2014 uma rejei\xE7\xE3o da promise n\xE3o seria tratada",
|
|
1638
|
+
file: filePath,
|
|
1639
|
+
line,
|
|
1640
|
+
severity: "medium"
|
|
1641
|
+
}));
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
// src/rules/floating-promise.rule.ts
|
|
1646
|
+
var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set(["all", "race", "allSettled", "any"]);
|
|
1647
|
+
var asyncFunctionName = (node) => {
|
|
1648
|
+
const declarationId = node.type === "FunctionDeclaration" ? node.id : void 0;
|
|
1649
|
+
const declarationName = declarationId?.type === "Identifier" ? declarationId.name : void 0;
|
|
1650
|
+
const id = node.type === "VariableDeclarator" ? node.id : void 0;
|
|
1651
|
+
const init = node.type === "VariableDeclarator" ? node.init : void 0;
|
|
1652
|
+
const hasAsyncFunctionValue = (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && init.async;
|
|
1653
|
+
return declarationName ?? (id?.type === "Identifier" && hasAsyncFunctionValue ? id.name : void 0);
|
|
1654
|
+
};
|
|
1655
|
+
var collectAsyncFunctionNames = (sourceFile) => {
|
|
1656
|
+
const names = /* @__PURE__ */ new Set();
|
|
1657
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1658
|
+
const name = asyncFunctionName(node);
|
|
1659
|
+
if (name && (node.type !== "FunctionDeclaration" || node.async)) {
|
|
1660
|
+
names.add(name);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
return names;
|
|
1664
|
+
};
|
|
1665
|
+
var isKnownPromiseReturningCall = (call, asyncFunctionNames) => {
|
|
1666
|
+
const callee = call.callee;
|
|
1667
|
+
if (!callee) {
|
|
1668
|
+
return false;
|
|
1669
|
+
}
|
|
1670
|
+
const isKnownFunction = callee.type === "Identifier" && (callee.name === "fetch" || asyncFunctionNames.has(callee.name));
|
|
1671
|
+
const object = callee.type === "MemberExpression" ? callee.object : void 0;
|
|
1672
|
+
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
1673
|
+
const isPromiseStaticMethod = object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
|
|
1674
|
+
return isKnownFunction || isPromiseStaticMethod;
|
|
1675
|
+
};
|
|
1676
|
+
var findFloatingPromiseLines = (filePath, content) => {
|
|
1677
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1678
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1679
|
+
const asyncFunctionNames = collectAsyncFunctionNames(sourceFile);
|
|
1680
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1681
|
+
const expression = node.type === "ExpressionStatement" ? node.expression : void 0;
|
|
1682
|
+
const isFloatingPromise = expression?.type === "CallExpression" && isKnownPromiseReturningCall(expression, asyncFunctionNames);
|
|
1683
|
+
if (isFloatingPromise && expression.loc) {
|
|
1684
|
+
lines.add(expression.loc.start.line);
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
return [...lines].sort((a, b) => a - b);
|
|
1688
|
+
};
|
|
1689
|
+
var floatingPromiseRule = {
|
|
1690
|
+
id: "floating-promise",
|
|
1691
|
+
description: 'Detecta uma promise "solta", chamada sem await, .then/.catch, atribui\xE7\xE3o ou return',
|
|
1692
|
+
check(filePath, content) {
|
|
1693
|
+
return findFloatingPromiseLines(filePath, content).map((line) => ({
|
|
1694
|
+
ruleId: "floating-promise",
|
|
1695
|
+
message: "Promise n\xE3o tratada \u2014 nem await, .then/.catch, atribui\xE7\xE3o ou return foram usados",
|
|
1696
|
+
file: filePath,
|
|
1697
|
+
line,
|
|
1698
|
+
severity: "medium"
|
|
1699
|
+
}));
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// src/rules/promise-no-catch.rule.ts
|
|
1704
|
+
var memberPropertyName = (member) => {
|
|
1705
|
+
const property = member.property;
|
|
1706
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
1707
|
+
};
|
|
1708
|
+
var isCallOf = (call, callee) => {
|
|
1709
|
+
return call !== void 0 && call.type === "CallExpression" && call.callee === callee;
|
|
1710
|
+
};
|
|
1711
|
+
var isThenCall = (node) => {
|
|
1712
|
+
if (node.type !== "CallExpression") {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
const callee = node.callee;
|
|
1716
|
+
const args = node.arguments;
|
|
1717
|
+
return callee?.type === "MemberExpression" && memberPropertyName(callee) === "then" && (args?.length ?? 0) < 2;
|
|
1718
|
+
};
|
|
1719
|
+
var chainReachesCatch = (thenCall, ancestors) => {
|
|
1720
|
+
let currentCall = thenCall;
|
|
1721
|
+
let i = 0;
|
|
1722
|
+
while (i < ancestors.length) {
|
|
1723
|
+
const member = ancestors[i];
|
|
1724
|
+
const nextCall = ancestors[i + 1];
|
|
1725
|
+
const isChainMember = member?.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
|
|
1726
|
+
const methodName = isChainMember ? memberPropertyName(member) : void 0;
|
|
1727
|
+
const continuesChain = methodName === "then" || methodName === "finally";
|
|
1728
|
+
if (methodName === "catch") {
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
if (!continuesChain) {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
currentCall = nextCall;
|
|
1735
|
+
i += 2;
|
|
1736
|
+
}
|
|
1737
|
+
return false;
|
|
1738
|
+
};
|
|
1739
|
+
var findUnhandledThenLines = (filePath, content) => {
|
|
1740
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1741
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1742
|
+
walkWithAncestors(sourceFile, (node, ancestors) => {
|
|
1743
|
+
if (isThenCall(node) && node.loc && !chainReachesCatch(node, ancestors)) {
|
|
1744
|
+
lines.add(node.loc.start.line);
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
return [...lines].sort((a, b) => a - b);
|
|
1748
|
+
};
|
|
1749
|
+
var promiseNoCatchRule = {
|
|
1750
|
+
id: "promise-no-catch",
|
|
1751
|
+
description: "Detecta uma cadeia .then() que nunca chega a um .catch()",
|
|
1752
|
+
check(filePath, content) {
|
|
1753
|
+
return findUnhandledThenLines(filePath, content).map((line) => ({
|
|
1754
|
+
ruleId: "promise-no-catch",
|
|
1755
|
+
message: "Promise com .then() sem .catch() \u2014 erros da promise n\xE3o est\xE3o sendo tratados",
|
|
1756
|
+
file: filePath,
|
|
1757
|
+
line,
|
|
1758
|
+
severity: "medium"
|
|
1759
|
+
}));
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// src/rules/security-lint.rule.ts
|
|
1764
|
+
import securityPlugin from "eslint-plugin-security";
|
|
1765
|
+
|
|
1766
|
+
// src/rules/lib/eslint-linter.ts
|
|
1767
|
+
import { createRequire as createRequire3 } from "module";
|
|
1768
|
+
import { basename } from "path";
|
|
1769
|
+
import { Linter } from "eslint";
|
|
1770
|
+
import babelParser from "@babel/eslint-parser";
|
|
1771
|
+
var require4 = createRequire3(import.meta.url);
|
|
1772
|
+
var SYNTAX_TYPESCRIPT_PLUGIN_PATH = require4.resolve("@babel/plugin-syntax-typescript");
|
|
1773
|
+
var SYNTAX_JSX_PLUGIN_PATH = require4.resolve("@babel/plugin-syntax-jsx");
|
|
1774
|
+
var isJsxFile2 = (filePath) => /\.(tsx|jsx)$/.test(filePath);
|
|
1775
|
+
var SCANNABLE_FILE_GLOBS = ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts", "**/*.jsx", "**/*.tsx"];
|
|
1776
|
+
var createConfig = (filePath, rules, plugins) => [
|
|
1777
|
+
{
|
|
1778
|
+
files: SCANNABLE_FILE_GLOBS,
|
|
1779
|
+
languageOptions: {
|
|
1780
|
+
parser: babelParser,
|
|
1781
|
+
ecmaVersion: "latest",
|
|
1782
|
+
sourceType: "module",
|
|
1783
|
+
parserOptions: {
|
|
1784
|
+
requireConfigFile: false,
|
|
1785
|
+
babelOptions: {
|
|
1786
|
+
plugins: isJsxFile2(filePath) ? [SYNTAX_JSX_PLUGIN_PATH, SYNTAX_TYPESCRIPT_PLUGIN_PATH] : [SYNTAX_TYPESCRIPT_PLUGIN_PATH]
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
plugins,
|
|
1791
|
+
rules
|
|
1792
|
+
}
|
|
1793
|
+
];
|
|
1794
|
+
var verify = (content, config, filename) => {
|
|
1795
|
+
try {
|
|
1796
|
+
return new Linter().verify(content, config, filename);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return [];
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
var toFindings = (messages) => messages.filter(
|
|
1802
|
+
(message) => message.ruleId !== null && typeof message.line === "number"
|
|
1803
|
+
).map((message) => ({
|
|
1804
|
+
ruleId: message.ruleId,
|
|
1805
|
+
message: message.message,
|
|
1806
|
+
line: message.line
|
|
1807
|
+
}));
|
|
1808
|
+
var runEslintRules = (filePath, content, rules, plugins) => {
|
|
1809
|
+
const syntheticFilename = basename(filePath) || "source.js";
|
|
1810
|
+
return toFindings(verify(content, createConfig(syntheticFilename, rules, plugins), syntheticFilename));
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
// src/rules/security-lint.rule.ts
|
|
1814
|
+
var RULES = {
|
|
1815
|
+
"security/detect-object-injection": "error",
|
|
1816
|
+
"security/detect-non-literal-regexp": "error",
|
|
1817
|
+
"security/detect-non-literal-fs-filename": "error",
|
|
1818
|
+
"security/detect-unsafe-regex": "error",
|
|
1819
|
+
"security/detect-buffer-noassert": "error",
|
|
1820
|
+
"security/detect-disable-mustache-escape": "error",
|
|
1821
|
+
"security/detect-no-csrf-before-method-override": "error",
|
|
1822
|
+
"security/detect-pseudoRandomBytes": "error",
|
|
1823
|
+
"security/detect-possible-timing-attacks": "error",
|
|
1824
|
+
"security/detect-new-buffer": "error"
|
|
1825
|
+
};
|
|
1826
|
+
var PLUGINS = { security: securityPlugin };
|
|
1827
|
+
var securityLintRule = {
|
|
1828
|
+
id: "security-lint",
|
|
1829
|
+
description: "Detecta padr\xF5es de seguran\xE7a gen\xE9ricos (object injection, regex n\xE3o literal, fs n\xE3o literal, etc.) via eslint-plugin-security",
|
|
1830
|
+
check(filePath, content) {
|
|
1831
|
+
return runEslintRules(filePath, content, RULES, PLUGINS).map((finding) => ({
|
|
1832
|
+
ruleId: finding.ruleId,
|
|
1833
|
+
message: `Padr\xE3o inseguro detectado (${finding.ruleId}): ${finding.message}`,
|
|
1834
|
+
file: filePath,
|
|
1835
|
+
line: finding.line,
|
|
1836
|
+
severity: "medium"
|
|
1837
|
+
}));
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1841
|
+
// src/rules/sensitive-data-in-logs.rule.ts
|
|
1842
|
+
var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["log", "warn", "error", "info", "debug"]);
|
|
1843
|
+
var SENSITIVE_NAME_PATTERN2 = /password|senha|secret|token|apikey|api_key|private_key|access_key/i;
|
|
1844
|
+
var looksLikeLoggerObject = (object) => object?.type === "Identifier" && (object.name === "console" || /log/i.test(object.name));
|
|
1845
|
+
var isLogCall = (node) => {
|
|
1846
|
+
if (node.type !== "CallExpression") {
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
const callee = node.callee;
|
|
1850
|
+
if (callee?.type !== "MemberExpression") {
|
|
1851
|
+
return false;
|
|
1852
|
+
}
|
|
1853
|
+
const property = callee.property;
|
|
1854
|
+
return looksLikeLoggerObject(callee.object) && property?.type === "Identifier" && LOG_METHOD_NAMES.has(property.name);
|
|
1855
|
+
};
|
|
1856
|
+
var propertyKeyName = (property) => {
|
|
1857
|
+
const key = property.key;
|
|
1858
|
+
if (key?.type === "Identifier") {
|
|
1859
|
+
return key.name;
|
|
1860
|
+
}
|
|
1861
|
+
if (key?.type === "StringLiteral") {
|
|
1862
|
+
return key.value;
|
|
1863
|
+
}
|
|
1864
|
+
return void 0;
|
|
1865
|
+
};
|
|
1866
|
+
var argumentContainsSensitiveData = (argument) => {
|
|
1867
|
+
let found = false;
|
|
1868
|
+
visitSourceNodes(argument, (node) => {
|
|
1869
|
+
if (node.type === "Identifier" && SENSITIVE_NAME_PATTERN2.test(node.name)) {
|
|
1870
|
+
found = true;
|
|
1871
|
+
} else if (node.type === "ObjectProperty") {
|
|
1872
|
+
const keyName = propertyKeyName(node);
|
|
1873
|
+
if (keyName && SENSITIVE_NAME_PATTERN2.test(keyName)) {
|
|
1874
|
+
found = true;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
return found;
|
|
1879
|
+
};
|
|
1880
|
+
var isSensitiveLogCall = (node) => {
|
|
1881
|
+
if (!isLogCall(node)) {
|
|
1882
|
+
return false;
|
|
1883
|
+
}
|
|
1884
|
+
const args = node.arguments ?? [];
|
|
1885
|
+
return args.some((argument) => argumentContainsSensitiveData(argument));
|
|
1886
|
+
};
|
|
1887
|
+
var findSensitiveLogLines = (filePath, content) => {
|
|
1888
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1889
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1890
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1891
|
+
if (isSensitiveLogCall(node) && node.loc) {
|
|
1892
|
+
lines.add(node.loc.start.line);
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
return [...lines].sort((a, b) => a - b);
|
|
1896
|
+
};
|
|
1897
|
+
var sensitiveDataInLogsRule = {
|
|
1898
|
+
id: "sensitive-data-in-logs",
|
|
1899
|
+
description: "Detecta senhas/segredos/tokens sendo passados para chamadas de log",
|
|
1900
|
+
check(filePath, content) {
|
|
1901
|
+
return findSensitiveLogLines(filePath, content).map((line) => ({
|
|
1902
|
+
ruleId: "sensitive-data-in-logs",
|
|
1903
|
+
message: "Dado sens\xEDvel (senha/segredo/token) sendo registrado em log",
|
|
1904
|
+
file: filePath,
|
|
1905
|
+
line,
|
|
1906
|
+
severity: "high"
|
|
1907
|
+
}));
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
// src/rules/tls-validation-disabled.rule.ts
|
|
1912
|
+
var isRejectUnauthorizedFalse = (node) => {
|
|
1913
|
+
if (node.type !== "ObjectProperty") {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
const key = node.key;
|
|
1917
|
+
const value = node.value;
|
|
1918
|
+
return key?.type === "Identifier" && key.name === "rejectUnauthorized" && value?.type === "BooleanLiteral" && value.value === false;
|
|
1919
|
+
};
|
|
1920
|
+
var isNodeTlsRejectUnauthorizedDisabled = (node) => {
|
|
1921
|
+
if (node.type !== "AssignmentExpression") {
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
const left = node.left;
|
|
1925
|
+
const right = node.right;
|
|
1926
|
+
if (left?.type !== "MemberExpression") {
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
const property = left.property;
|
|
1930
|
+
const object = left.object;
|
|
1931
|
+
const isEnvVar = property?.type === "Identifier" && property.name === "NODE_TLS_REJECT_UNAUTHORIZED" && object?.type === "MemberExpression" && object.property?.type === "Identifier" && object.property.name === "env";
|
|
1932
|
+
return isEnvVar && right?.type === "StringLiteral" && right.value === "0";
|
|
1933
|
+
};
|
|
1934
|
+
var findDisabledTlsValidationLines = (filePath, content) => {
|
|
1935
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1936
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1937
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1938
|
+
if ((isRejectUnauthorizedFalse(node) || isNodeTlsRejectUnauthorizedDisabled(node)) && node.loc) {
|
|
1939
|
+
lines.add(node.loc.start.line);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
return [...lines].sort((a, b) => a - b);
|
|
1943
|
+
};
|
|
1944
|
+
var tlsValidationDisabledRule = {
|
|
1945
|
+
id: "tls-validation-disabled",
|
|
1946
|
+
description: "Detecta a desativa\xE7\xE3o da valida\xE7\xE3o de certificados TLS",
|
|
1947
|
+
check(filePath, content) {
|
|
1948
|
+
return findDisabledTlsValidationLines(filePath, content).map((line) => ({
|
|
1949
|
+
ruleId: "tls-validation-disabled",
|
|
1950
|
+
message: "Valida\xE7\xE3o de certificado TLS desativada \u2014 risco de ataque man-in-the-middle",
|
|
1951
|
+
file: filePath,
|
|
1952
|
+
line,
|
|
1953
|
+
severity: "critical"
|
|
1954
|
+
}));
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
// src/rules/too-many-for-loops.rule.ts
|
|
1959
|
+
var tooManyForLoopsRule = createFunctionStatementCountRule({
|
|
1960
|
+
id: "too-many-for-loops",
|
|
1961
|
+
description: 'Detecta fun\xE7\xF5es com muitos loops "for"/"for-in"/"for-of"',
|
|
1962
|
+
statementTypes: /* @__PURE__ */ new Set(["ForStatement", "ForInStatement", "ForOfStatement"]),
|
|
1963
|
+
maxCount: 1,
|
|
1964
|
+
unitLabel: "loops for"
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
// src/rules/too-many-ifs.rule.ts
|
|
1968
|
+
var tooManyIfsRule = createFunctionStatementCountRule({
|
|
1969
|
+
id: "too-many-ifs",
|
|
1970
|
+
description: 'Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")',
|
|
1971
|
+
statementTypes: /* @__PURE__ */ new Set(["IfStatement"]),
|
|
1972
|
+
maxCount: 2,
|
|
1973
|
+
unitLabel: "declara\xE7\xF5es if (incluindo else if)"
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
// src/rules/too-many-switch-cases.rule.ts
|
|
1977
|
+
var MAX_CASES = 4;
|
|
1978
|
+
var findOversizedSwitches = (filePath, content) => {
|
|
1979
|
+
const results = [];
|
|
1980
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1981
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1982
|
+
const cases = node.type === "SwitchStatement" && Array.isArray(node.cases) ? node.cases : [];
|
|
1983
|
+
if (cases.length > MAX_CASES && node.loc) {
|
|
1984
|
+
results.push({ line: node.loc.start.line, caseCount: cases.length });
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
return results;
|
|
1988
|
+
};
|
|
1989
|
+
var tooManySwitchCasesRule = {
|
|
1990
|
+
id: "too-many-switch-cases",
|
|
1991
|
+
description: 'Detecta "switch" com muitos "case" (considere um mapa/lookup)',
|
|
1992
|
+
check(filePath, content) {
|
|
1993
|
+
return findOversizedSwitches(filePath, content).map(({ line, caseCount }) => ({
|
|
1994
|
+
ruleId: "too-many-switch-cases",
|
|
1995
|
+
message: `Switch com ${caseCount} cases \u2014 complexidade alta, considere um mapa/lookup em vez de switch (limite recomendado: ${MAX_CASES})`,
|
|
1996
|
+
file: filePath,
|
|
1997
|
+
line,
|
|
1998
|
+
severity: "low"
|
|
1999
|
+
}));
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
// src/rules/too-many-try-catch.rule.ts
|
|
2004
|
+
var tooManyTryCatchRule = createFunctionStatementCountRule({
|
|
2005
|
+
id: "too-many-try-catch",
|
|
2006
|
+
description: 'Detecta fun\xE7\xF5es com muitos blocos "try/catch"',
|
|
2007
|
+
statementTypes: /* @__PURE__ */ new Set(["TryStatement"]),
|
|
2008
|
+
maxCount: 1,
|
|
2009
|
+
unitLabel: "blocos try/catch"
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
// src/rules/too-many-while-loops.rule.ts
|
|
2013
|
+
var tooManyWhileLoopsRule = createFunctionStatementCountRule({
|
|
2014
|
+
id: "too-many-while-loops",
|
|
2015
|
+
description: 'Detecta fun\xE7\xF5es com muitos loops "while"/"do-while"',
|
|
2016
|
+
statementTypes: /* @__PURE__ */ new Set(["WhileStatement", "DoWhileStatement"]),
|
|
2017
|
+
maxCount: 1,
|
|
2018
|
+
unitLabel: "loops while/do-while"
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// src/rules/unsafe-sql.rule.ts
|
|
2022
|
+
var SQL_KEYWORD_PREFIX = /^(select|insert|update|delete)\b/i;
|
|
2023
|
+
var leftmostStringLiteral = (node) => {
|
|
2024
|
+
if (node?.type === "StringLiteral") {
|
|
2025
|
+
return node;
|
|
2026
|
+
}
|
|
2027
|
+
if (node?.type === "BinaryExpression" && node.operator === "+") {
|
|
2028
|
+
return leftmostStringLiteral(node.left);
|
|
2029
|
+
}
|
|
2030
|
+
return void 0;
|
|
2031
|
+
};
|
|
2032
|
+
var isUnsafeConcatenatedSql = (node) => {
|
|
2033
|
+
if (node.type !== "BinaryExpression" || node.operator !== "+") {
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2036
|
+
const literal = leftmostStringLiteral(node);
|
|
2037
|
+
return !!literal && SQL_KEYWORD_PREFIX.test(literal.value.trim());
|
|
2038
|
+
};
|
|
2039
|
+
var isUnsafeSqlTemplateLiteral = (node) => {
|
|
2040
|
+
if (node.type !== "TemplateLiteral") {
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
const expressions = node.expressions;
|
|
2044
|
+
if ((expressions?.length ?? 0) === 0) {
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
const quasis = node.quasis;
|
|
2048
|
+
const firstQuasi = quasis?.[0];
|
|
2049
|
+
const rawText = firstQuasi?.value?.raw ?? "";
|
|
2050
|
+
return SQL_KEYWORD_PREFIX.test(rawText.trim());
|
|
2051
|
+
};
|
|
2052
|
+
var findUnsafeSqlLines = (filePath, content) => {
|
|
2053
|
+
const lines = /* @__PURE__ */ new Set();
|
|
2054
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
2055
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2056
|
+
if ((isUnsafeConcatenatedSql(node) || isUnsafeSqlTemplateLiteral(node)) && node.loc) {
|
|
2057
|
+
lines.add(node.loc.start.line);
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
return [...lines].sort((a, b) => a - b);
|
|
2061
|
+
};
|
|
2062
|
+
var unsafeSqlRule = {
|
|
2063
|
+
id: "unsafe-sql",
|
|
2064
|
+
description: "Detecta concatena\xE7\xE3o insegura de SQL (risco de SQL injection)",
|
|
2065
|
+
check(filePath, content) {
|
|
2066
|
+
return findUnsafeSqlLines(filePath, content).map((line) => ({
|
|
2067
|
+
ruleId: "unsafe-sql",
|
|
2068
|
+
message: "Consulta SQL montada por concatena\xE7\xE3o \u2014 use queries parametrizadas",
|
|
2069
|
+
file: filePath,
|
|
2070
|
+
line,
|
|
2071
|
+
severity: "high"
|
|
2072
|
+
}));
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
// src/rules/weak-hash-algorithm.rule.ts
|
|
2077
|
+
var WEAK_ALGORITHMS = /^(md5|sha1)$/i;
|
|
2078
|
+
var isWeakCreateHashCall = (node) => {
|
|
2079
|
+
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
2080
|
+
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
2081
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
2082
|
+
const algorithm = args?.[0];
|
|
2083
|
+
return property?.type === "Identifier" && property.name === "createHash" && algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
|
|
2084
|
+
};
|
|
2085
|
+
var findWeakHashLines = (filePath, content) => {
|
|
2086
|
+
const lines = /* @__PURE__ */ new Set();
|
|
2087
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
2088
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2089
|
+
if (isWeakCreateHashCall(node) && node.loc) {
|
|
2090
|
+
lines.add(node.loc.start.line);
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
return [...lines].sort((a, b) => a - b);
|
|
2094
|
+
};
|
|
2095
|
+
var weakHashAlgorithmRule = {
|
|
2096
|
+
id: "weak-hash-algorithm",
|
|
2097
|
+
description: "Detecta o uso de algoritmos de hash fracos (MD5, SHA-1)",
|
|
2098
|
+
check(filePath, content) {
|
|
2099
|
+
return findWeakHashLines(filePath, content).map((line) => ({
|
|
2100
|
+
ruleId: "weak-hash-algorithm",
|
|
2101
|
+
message: "Algoritmo de hash fraco (MD5/SHA-1) \u2014 considere SHA-256 ou superior",
|
|
2102
|
+
file: filePath,
|
|
2103
|
+
line,
|
|
2104
|
+
severity: "medium"
|
|
2105
|
+
}));
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
// src/rules/weak-secret-fallback.rule.ts
|
|
2110
|
+
var SECRET_ENV_NAME_PATTERN = /password|senha|secret|token|apikey|api_key|private_key|access_key|encryption_key|signing_key|jwt/i;
|
|
2111
|
+
var isProcessEnvAccess = (node) => {
|
|
2112
|
+
if (node?.type !== "MemberExpression") {
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
const object = node.object;
|
|
2116
|
+
return object?.type === "MemberExpression" && object.object?.type === "Identifier" && object.object.name === "process" && object.property?.type === "Identifier" && object.property.name === "env";
|
|
2117
|
+
};
|
|
2118
|
+
var isNonEmptyStringLiteral2 = (node) => node?.type === "StringLiteral" && node.value.trim().length > 0;
|
|
2119
|
+
var isWeakSecretFallback = (node) => {
|
|
2120
|
+
if (node.type !== "LogicalExpression" || node.operator !== "||" && node.operator !== "??") {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
const left = node.left;
|
|
2124
|
+
if (!isProcessEnvAccess(left)) {
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
const envVarName = left.property.name;
|
|
2128
|
+
return SECRET_ENV_NAME_PATTERN.test(envVarName) && isNonEmptyStringLiteral2(node.right);
|
|
2129
|
+
};
|
|
2130
|
+
var findWeakSecretFallbackLines = (filePath, content) => {
|
|
2131
|
+
const lines = /* @__PURE__ */ new Set();
|
|
2132
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
2133
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2134
|
+
if (isWeakSecretFallback(node) && node.loc) {
|
|
2135
|
+
lines.add(node.loc.start.line);
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
return [...lines].sort((a, b) => a - b);
|
|
2139
|
+
};
|
|
2140
|
+
var weakSecretFallbackRule = {
|
|
2141
|
+
id: "weak-secret-fallback",
|
|
2142
|
+
description: "Detecta uma vari\xE1vel de ambiente de segredo/chave com um valor hardcoded como fallback (|| ou ??)",
|
|
2143
|
+
check(filePath, content) {
|
|
2144
|
+
return findWeakSecretFallbackLines(filePath, content).map((line) => ({
|
|
2145
|
+
ruleId: "weak-secret-fallback",
|
|
2146
|
+
message: "Segredo/chave com fallback hardcoded \u2014 se a vari\xE1vel de ambiente n\xE3o for definida, um valor previs\xEDvel \xE9 usado",
|
|
2147
|
+
file: filePath,
|
|
2148
|
+
line,
|
|
2149
|
+
severity: "critical"
|
|
2150
|
+
}));
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
// src/rules/xss.rule.ts
|
|
2155
|
+
import noUnsanitizedPlugin from "eslint-plugin-no-unsanitized";
|
|
2156
|
+
var RULES2 = {
|
|
2157
|
+
"no-unsanitized/property": "error",
|
|
2158
|
+
"no-unsanitized/method": "error"
|
|
2159
|
+
};
|
|
2160
|
+
var PLUGINS2 = { "no-unsanitized": noUnsanitizedPlugin };
|
|
2161
|
+
var isDangerouslySetInnerHtmlWithDynamicValue = (node) => {
|
|
2162
|
+
if (node.type !== "JSXAttribute") {
|
|
2163
|
+
return false;
|
|
2164
|
+
}
|
|
2165
|
+
const name = node.name;
|
|
2166
|
+
if (name?.type !== "JSXIdentifier" || name.name !== "dangerouslySetInnerHTML") {
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
const value = node.value;
|
|
2170
|
+
if (value?.type !== "JSXExpressionContainer") {
|
|
2171
|
+
return false;
|
|
2172
|
+
}
|
|
2173
|
+
const expression = value.expression;
|
|
2174
|
+
if (expression?.type !== "ObjectExpression") {
|
|
2175
|
+
return false;
|
|
2176
|
+
}
|
|
2177
|
+
const properties = expression.properties;
|
|
2178
|
+
const htmlProperty = properties?.find((property) => {
|
|
2179
|
+
const key = property.key;
|
|
2180
|
+
return key?.type === "Identifier" && key.name === "__html";
|
|
2181
|
+
});
|
|
2182
|
+
const htmlValue = htmlProperty?.value;
|
|
2183
|
+
return htmlValue !== void 0 && htmlValue.type !== "StringLiteral";
|
|
2184
|
+
};
|
|
2185
|
+
var findDangerouslySetInnerHtmlFindings = (filePath, content) => {
|
|
2186
|
+
const findings = [];
|
|
2187
|
+
let sourceFile;
|
|
2188
|
+
try {
|
|
2189
|
+
sourceFile = parseSourceFile(filePath, content);
|
|
2190
|
+
} catch {
|
|
2191
|
+
return findings;
|
|
2192
|
+
}
|
|
2193
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2194
|
+
if (isDangerouslySetInnerHtmlWithDynamicValue(node) && node.loc) {
|
|
2195
|
+
findings.push({
|
|
2196
|
+
ruleId: "dangerously-set-inner-html",
|
|
2197
|
+
message: "Poss\xEDvel XSS: dangerouslySetInnerHTML com valor n\xE3o literal (dado n\xE3o confi\xE1vel)",
|
|
2198
|
+
file: filePath,
|
|
2199
|
+
line: node.loc.start.line,
|
|
2200
|
+
severity: "high"
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
return findings;
|
|
2205
|
+
};
|
|
2206
|
+
var xssRule = {
|
|
2207
|
+
id: "xss",
|
|
2208
|
+
description: "Detecta sinks perigosos de XSS (innerHTML, document.write, dangerouslySetInnerHTML, etc.)",
|
|
2209
|
+
check(filePath, content) {
|
|
2210
|
+
const eslintFindings = runEslintRules(filePath, content, RULES2, PLUGINS2).map((finding) => ({
|
|
2211
|
+
ruleId: finding.ruleId,
|
|
2212
|
+
message: `Poss\xEDvel XSS: ${finding.message}`,
|
|
2213
|
+
file: filePath,
|
|
2214
|
+
line: finding.line,
|
|
2215
|
+
severity: "high"
|
|
2216
|
+
}));
|
|
2217
|
+
return [...eslintFindings, ...findDangerouslySetInnerHtmlFindings(filePath, content)];
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
// src/rules/xxe-unsafe-xml-parsing.rule.ts
|
|
2222
|
+
var XML_PARSE_METHOD_NAMES = /* @__PURE__ */ new Set(["parseXmlString", "parseXml"]);
|
|
2223
|
+
var RISKY_OPTION_NAMES = /* @__PURE__ */ new Set(["noent", "dtdload"]);
|
|
2224
|
+
var hasRiskyOptionEnabled = (options) => {
|
|
2225
|
+
if (options?.type !== "ObjectExpression") {
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
const properties = options.properties;
|
|
2229
|
+
return properties?.some((property) => {
|
|
2230
|
+
const key = property.key;
|
|
2231
|
+
const value = property.value;
|
|
2232
|
+
return key?.type === "Identifier" && RISKY_OPTION_NAMES.has(key.name) && value?.type === "BooleanLiteral" && value.value === true;
|
|
2233
|
+
}) ?? false;
|
|
2234
|
+
};
|
|
2235
|
+
var isUnsafeXmlParseCall = (node) => {
|
|
2236
|
+
if (node.type !== "CallExpression") {
|
|
2237
|
+
return false;
|
|
2238
|
+
}
|
|
2239
|
+
const callee = node.callee;
|
|
2240
|
+
if (callee?.type !== "MemberExpression") {
|
|
2241
|
+
return false;
|
|
2242
|
+
}
|
|
2243
|
+
const property = callee.property;
|
|
2244
|
+
if (property?.type !== "Identifier" || !XML_PARSE_METHOD_NAMES.has(property.name)) {
|
|
2245
|
+
return false;
|
|
2246
|
+
}
|
|
2247
|
+
const args = node.arguments;
|
|
2248
|
+
return hasRiskyOptionEnabled(args?.[1]);
|
|
2249
|
+
};
|
|
2250
|
+
var findUnsafeXmlParsingLines = (filePath, content) => {
|
|
2251
|
+
const lines = /* @__PURE__ */ new Set();
|
|
2252
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
2253
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2254
|
+
if (isUnsafeXmlParseCall(node) && node.loc) {
|
|
2255
|
+
lines.add(node.loc.start.line);
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
return [...lines].sort((a, b) => a - b);
|
|
2259
|
+
};
|
|
2260
|
+
var xxeUnsafeXmlParsingRule = {
|
|
2261
|
+
id: "xxe-unsafe-xml-parsing",
|
|
2262
|
+
description: "Detecta parsing de XML com substitui\xE7\xE3o de entidades e/ou carregamento de DTD externo habilitados (risco de XXE)",
|
|
2263
|
+
check(filePath, content) {
|
|
2264
|
+
return findUnsafeXmlParsingLines(filePath, content).map((line) => ({
|
|
2265
|
+
ruleId: "xxe-unsafe-xml-parsing",
|
|
2266
|
+
message: "Parser XML com noent/dtdload habilitado \u2014 risco de XML External Entity (XXE)",
|
|
2267
|
+
file: filePath,
|
|
2268
|
+
line,
|
|
2269
|
+
severity: "critical"
|
|
2270
|
+
}));
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
// src/rules/index.ts
|
|
2275
|
+
var allRules = [
|
|
2276
|
+
noEvalRule,
|
|
2277
|
+
noHardcodedSecretRule,
|
|
2278
|
+
unsafeSqlRule,
|
|
2279
|
+
longFunctionRule,
|
|
2280
|
+
deepNestingRule,
|
|
2281
|
+
highComplexityRule,
|
|
2282
|
+
tooManyIfsRule,
|
|
2283
|
+
tooManyForLoopsRule,
|
|
2284
|
+
tooManyWhileLoopsRule,
|
|
2285
|
+
tooManyTryCatchRule,
|
|
2286
|
+
tooManySwitchCasesRule,
|
|
2287
|
+
promiseNoCatchRule,
|
|
2288
|
+
awaitNoTryCatchRule,
|
|
2289
|
+
floatingPromiseRule,
|
|
2290
|
+
noAnyRule,
|
|
2291
|
+
emptyCatchRule,
|
|
2292
|
+
commandInjectionRule,
|
|
2293
|
+
jwtNoExpirationRule,
|
|
2294
|
+
permissiveCorsRule,
|
|
2295
|
+
insecureRandomTokenRule,
|
|
2296
|
+
weakHashAlgorithmRule,
|
|
2297
|
+
tlsValidationDisabledRule,
|
|
2298
|
+
expressMissingBodyLimitRule,
|
|
2299
|
+
xssRule,
|
|
2300
|
+
securityLintRule,
|
|
2301
|
+
weakSecretFallbackRule,
|
|
2302
|
+
jwtDecodeWithoutVerifyRule,
|
|
2303
|
+
xxeUnsafeXmlParsingRule,
|
|
2304
|
+
sensitiveDataInLogsRule,
|
|
2305
|
+
publicEnvVarSecretRule
|
|
2306
|
+
];
|
|
2307
|
+
|
|
2308
|
+
// src/commands/rules/rules.command.ts
|
|
2309
|
+
var registerRulesCommand = (program) => {
|
|
2310
|
+
program.command("rules").description("Lista as regras de an\xE1lise dispon\xEDveis").action(() => {
|
|
2311
|
+
const table = new Table3({ head: ["ID", "Descri\xE7\xE3o"] });
|
|
2312
|
+
for (const rule of allRules) {
|
|
2313
|
+
table.push([rule.id, rule.description]);
|
|
2314
|
+
}
|
|
2315
|
+
console.log(table.toString());
|
|
2316
|
+
});
|
|
2317
|
+
};
|
|
2318
|
+
|
|
2319
|
+
// src/commands/scan/scan.command.ts
|
|
2320
|
+
import { resolve as resolve2 } from "path";
|
|
2321
|
+
var parseConcurrency = (value) => {
|
|
2322
|
+
const concurrency = Number(value);
|
|
2323
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
2324
|
+
throw new Error("A concorr\xEAncia deve ser um inteiro positivo.");
|
|
2325
|
+
}
|
|
2326
|
+
return concurrency;
|
|
2327
|
+
};
|
|
2328
|
+
var parseLocalSemgrepConfig = (value) => {
|
|
2329
|
+
if (/^[a-z][a-z\d+.-]*:\/\//i.test(value)) {
|
|
2330
|
+
throw new Error("A configura\xE7\xE3o do Semgrep deve ser um arquivo local.");
|
|
2331
|
+
}
|
|
2332
|
+
return resolve2(value);
|
|
2333
|
+
};
|
|
2334
|
+
var registerScanCommand = (program) => {
|
|
2335
|
+
program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig).action(
|
|
2336
|
+
(path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
|
|
2337
|
+
);
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
// src/commands/security-lint/security-lint.command.ts
|
|
2341
|
+
var registerSecurityLintCommand = (program) => {
|
|
2342
|
+
program.command("security-lint").description("Detecta padr\xF5es de seguran\xE7a gen\xE9ricos via eslint-plugin-security").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2343
|
+
(path, options) => scanAndReport(path, [securityLintRule], "Checking generic security patterns...", options)
|
|
2344
|
+
);
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
// src/commands/sensitive-data-in-logs/sensitive-data-in-logs.command.ts
|
|
2348
|
+
var registerSensitiveDataInLogsCommand = (program) => {
|
|
2349
|
+
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) => {
|
|
2350
|
+
await scanAndReport(path, [sensitiveDataInLogsRule], "Checking sensitive data in logs...", options);
|
|
2351
|
+
});
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
// src/commands/tls-validation-disabled/tls-validation-disabled.command.ts
|
|
2355
|
+
var registerTlsValidationDisabledCommand = (program) => {
|
|
2356
|
+
program.command("tls-validation-disabled").description("Detecta a desativa\xE7\xE3o da valida\xE7\xE3o de certificados TLS").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2357
|
+
(path, options) => scanAndReport(path, [tlsValidationDisabledRule], "Checking TLS validation...", options)
|
|
2358
|
+
);
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// src/commands/too-many-for-loops/too-many-for-loops.command.ts
|
|
2362
|
+
var registerTooManyForLoopsCommand = (program) => {
|
|
2363
|
+
program.command("too-many-for-loops").description('Detecta fun\xE7\xF5es com muitos loops "for"/"for-in"/"for-of"').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2364
|
+
(path, options) => scanAndReport(path, [tooManyForLoopsRule], "Checking for-loop count...", options)
|
|
2365
|
+
);
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
// src/commands/too-many-ifs/too-many-ifs.command.ts
|
|
2369
|
+
var registerTooManyIfsCommand = (program) => {
|
|
2370
|
+
program.command("too-many-ifs").description('Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2371
|
+
(path, options) => scanAndReport(path, [tooManyIfsRule], "Checking if count...", options)
|
|
2372
|
+
);
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// src/commands/too-many-switch-cases/too-many-switch-cases.command.ts
|
|
2376
|
+
var registerTooManySwitchCasesCommand = (program) => {
|
|
2377
|
+
program.command("too-many-switch-cases").description('Detecta "switch" com muitos "case" (considere um mapa/lookup)').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2378
|
+
(path, options) => scanAndReport(path, [tooManySwitchCasesRule], "Checking switch cases...", options)
|
|
2379
|
+
);
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2382
|
+
// src/commands/too-many-try-catch/too-many-try-catch.command.ts
|
|
2383
|
+
var registerTooManyTryCatchCommand = (program) => {
|
|
2384
|
+
program.command("too-many-try-catch").description('Detecta fun\xE7\xF5es com muitos blocos "try/catch"').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2385
|
+
(path, options) => scanAndReport(path, [tooManyTryCatchRule], "Checking try/catch count...", options)
|
|
2386
|
+
);
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
// src/commands/too-many-while-loops/too-many-while-loops.command.ts
|
|
2390
|
+
var registerTooManyWhileLoopsCommand = (program) => {
|
|
2391
|
+
program.command("too-many-while-loops").description('Detecta fun\xE7\xF5es com muitos loops "while"/"do-while"').argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2392
|
+
(path, options) => scanAndReport(path, [tooManyWhileLoopsRule], "Checking while-loop count...", options)
|
|
2393
|
+
);
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// src/commands/unhandled-promises/unhandled-promises.command.ts
|
|
2397
|
+
var registerUnhandledPromisesCommand = (program) => {
|
|
2398
|
+
program.command("unhandled-promises").description("Detecta promises sem tratamento (sem .catch, await sem try/catch ou promises soltas)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2399
|
+
(path, options) => scanAndReport(
|
|
2400
|
+
path,
|
|
2401
|
+
[promiseNoCatchRule, awaitNoTryCatchRule, floatingPromiseRule],
|
|
2402
|
+
"Checking unhandled promises...",
|
|
2403
|
+
options
|
|
2404
|
+
)
|
|
2405
|
+
);
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
// src/commands/unsafe-sql/unsafe-sql.command.ts
|
|
2409
|
+
var registerUnsafeSqlCommand = (program) => {
|
|
2410
|
+
program.command("unsafe-sql").description("Detecta concatena\xE7\xE3o insegura de SQL (risco de SQL injection)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2411
|
+
(path, options) => scanAndReport(path, [unsafeSqlRule], "Checking unsafe SQL...", options)
|
|
2412
|
+
);
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
// src/commands/weak-hash-algorithm/weak-hash-algorithm.command.ts
|
|
2416
|
+
var registerWeakHashAlgorithmCommand = (program) => {
|
|
2417
|
+
program.command("weak-hash-algorithm").description("Detecta o uso de algoritmos de hash fracos (MD5, SHA-1)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2418
|
+
(path, options) => scanAndReport(path, [weakHashAlgorithmRule], "Checking weak hash algorithms...", options)
|
|
2419
|
+
);
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
// src/commands/weak-secret-fallback/weak-secret-fallback.command.ts
|
|
2423
|
+
var registerWeakSecretFallbackCommand = (program) => {
|
|
2424
|
+
program.command("weak-secret-fallback").description(
|
|
2425
|
+
"Detecta uma vari\xE1vel de ambiente de segredo/chave com um valor hardcoded como fallback (|| ou ??)"
|
|
2426
|
+
).argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(async (path, options) => {
|
|
2427
|
+
await scanAndReport(path, [weakSecretFallbackRule], "Checking weak secret fallbacks...", options);
|
|
2428
|
+
});
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
// src/commands/xss/xss.command.ts
|
|
2432
|
+
var registerXssCommand = (program) => {
|
|
2433
|
+
program.command("xss").description("Detecta sinks perigosos de XSS (innerHTML, document.write, etc.)").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--json", "exibe o resultado em JSON").action(
|
|
2434
|
+
(path, options) => scanAndReport(path, [xssRule], "Checking XSS sinks...", options)
|
|
2435
|
+
);
|
|
2436
|
+
};
|
|
2437
|
+
|
|
2438
|
+
// src/commands/xxe-unsafe-xml-parsing/xxe-unsafe-xml-parsing.command.ts
|
|
2439
|
+
var registerXxeUnsafeXmlParsingCommand = (program) => {
|
|
2440
|
+
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) => {
|
|
2441
|
+
await scanAndReport(path, [xxeUnsafeXmlParsingRule], "Checking unsafe XML parsing...", options);
|
|
2442
|
+
});
|
|
2443
|
+
};
|
|
2444
|
+
|
|
2445
|
+
// src/cli.ts
|
|
2446
|
+
var printBanner = () => {
|
|
2447
|
+
if (!process.stdout.isTTY) {
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const banner = figlet.textSync("CodeSentry", { font: "Standard" });
|
|
2451
|
+
console.log(gradient(["cyan", "magenta"])(banner));
|
|
2452
|
+
};
|
|
2453
|
+
var registerAnalysisCommands = (program) => {
|
|
2454
|
+
registerScanCommand(program);
|
|
2455
|
+
registerLongFunctionsCommand(program);
|
|
2456
|
+
registerDeepNestingCommand(program);
|
|
2457
|
+
registerHighComplexityCommand(program);
|
|
2458
|
+
registerTooManyIfsCommand(program);
|
|
2459
|
+
registerTooManyForLoopsCommand(program);
|
|
2460
|
+
registerTooManyWhileLoopsCommand(program);
|
|
2461
|
+
registerTooManyTryCatchCommand(program);
|
|
2462
|
+
registerTooManySwitchCasesCommand(program);
|
|
2463
|
+
};
|
|
2464
|
+
var registerQualityCommands = (program) => {
|
|
2465
|
+
registerUnhandledPromisesCommand(program);
|
|
2466
|
+
registerNoAnyCommand(program);
|
|
2467
|
+
registerEmptyCatchCommand(program);
|
|
2468
|
+
registerRulesCommand(program);
|
|
2469
|
+
registerHelpCommand(program);
|
|
2470
|
+
};
|
|
2471
|
+
var registerSecurityCommands = (program) => {
|
|
2472
|
+
registerNoEvalCommand(program);
|
|
2473
|
+
registerCommandInjectionCommand(program);
|
|
2474
|
+
registerUnsafeSqlCommand(program);
|
|
2475
|
+
registerJwtNoExpirationCommand(program);
|
|
2476
|
+
registerNoHardcodedSecretCommand(program);
|
|
2477
|
+
registerPermissiveCorsCommand(program);
|
|
2478
|
+
registerInsecureRandomTokenCommand(program);
|
|
2479
|
+
registerWeakHashAlgorithmCommand(program);
|
|
2480
|
+
registerTlsValidationDisabledCommand(program);
|
|
2481
|
+
registerExpressMissingBodyLimitCommand(program);
|
|
2482
|
+
registerXssCommand(program);
|
|
2483
|
+
registerSecurityLintCommand(program);
|
|
2484
|
+
registerDependencyAuditCommand(program);
|
|
2485
|
+
registerWeakSecretFallbackCommand(program);
|
|
2486
|
+
registerJwtDecodeWithoutVerifyCommand(program);
|
|
2487
|
+
registerXxeUnsafeXmlParsingCommand(program);
|
|
2488
|
+
registerSensitiveDataInLogsCommand(program);
|
|
2489
|
+
registerPublicEnvVarSecretCommand(program);
|
|
2490
|
+
};
|
|
2491
|
+
var createCli = () => {
|
|
2492
|
+
printBanner();
|
|
2493
|
+
const program = new Command().name("codesentry").description("CLI de verifica\xE7\xE3o de vulnerabilidades e qualidade de c\xF3digo").version("0.1.0").helpCommand(false);
|
|
2494
|
+
registerInitCommand(program);
|
|
2495
|
+
registerAnalysisCommands(program);
|
|
2496
|
+
registerQualityCommands(program);
|
|
2497
|
+
registerSecurityCommands(program);
|
|
2498
|
+
return program;
|
|
2499
|
+
};
|
|
2500
|
+
|
|
2501
|
+
// src/index.ts
|
|
2502
|
+
createCli().parseAsync(process.argv);
|