codesentry 0.1.10 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -3
- package/dist/index.js +932 -208
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { createRequire as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
|
+
import { createRequire as createRequire5 } from "module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import figlet from "figlet";
|
|
8
8
|
import gradient from "gradient-string";
|
|
@@ -45,13 +45,13 @@ var isExecImport = (specifier) => {
|
|
|
45
45
|
return specifier.type === "ImportSpecifier" && imported?.type === "Identifier" && EXEC_METHOD_NAMES.has(imported.name);
|
|
46
46
|
};
|
|
47
47
|
var isNamespaceImport = (specifier) => specifier.type === "ImportDefaultSpecifier" || specifier.type === "ImportNamespaceSpecifier";
|
|
48
|
+
var bindingSetForImportSpecifier = (specifier, bindings) => isExecImport(specifier) ? bindings.directCalls : isNamespaceImport(specifier) ? bindings.namespaces : void 0;
|
|
48
49
|
var collectImportBinding = (specifier, bindings) => {
|
|
49
50
|
const name = localName(specifier);
|
|
50
51
|
if (!name) {
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
bindingSet?.add(name);
|
|
54
|
+
bindingSetForImportSpecifier(specifier, bindings)?.add(name);
|
|
55
55
|
};
|
|
56
56
|
var collectFromImportDeclaration = (node, bindings) => {
|
|
57
57
|
if (!isChildProcessModuleSpecifier(node.source)) {
|
|
@@ -71,13 +71,15 @@ var collectDirectCallBindings = (id, bindings) => {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
+
var namespaceNameFromDeclaratorId = (id) => id?.type === "Identifier" ? id.name : void 0;
|
|
75
|
+
var destructuredBindingsFromDeclaratorId = (id) => id?.type === "ObjectPattern" ? id : void 0;
|
|
74
76
|
var collectFromVariableDeclarator = (node, bindings) => {
|
|
75
77
|
if (!isRequireCall(node.init)) {
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
const id = node.id;
|
|
79
|
-
const namespaceName = id
|
|
80
|
-
const destructuredBindings = id
|
|
81
|
+
const namespaceName = namespaceNameFromDeclaratorId(id);
|
|
82
|
+
const destructuredBindings = destructuredBindingsFromDeclaratorId(id);
|
|
81
83
|
namespaceName && bindings.namespaces.add(namespaceName);
|
|
82
84
|
destructuredBindings && collectDirectCallBindings(destructuredBindings, bindings);
|
|
83
85
|
};
|
|
@@ -92,11 +94,8 @@ var collectChildProcessBindings = (sourceFile) => {
|
|
|
92
94
|
});
|
|
93
95
|
return bindings;
|
|
94
96
|
};
|
|
95
|
-
var
|
|
96
|
-
if (callee
|
|
97
|
-
return bindings.directCalls.has(callee.name);
|
|
98
|
-
}
|
|
99
|
-
if (callee?.type !== "MemberExpression") {
|
|
97
|
+
var isKnownChildProcessMemberCallee = (callee, bindings) => {
|
|
98
|
+
if (callee.type !== "MemberExpression") {
|
|
100
99
|
return false;
|
|
101
100
|
}
|
|
102
101
|
const object = callee.object;
|
|
@@ -104,6 +103,12 @@ var isKnownChildProcessCallee = (callee, bindings) => {
|
|
|
104
103
|
const methodName = property?.type === "Identifier" ? property.name : void 0;
|
|
105
104
|
return !!methodName && EXEC_METHOD_NAMES.has(methodName) && object?.type === "Identifier" && bindings.namespaces.has(object.name);
|
|
106
105
|
};
|
|
106
|
+
var isKnownChildProcessCallee = (callee, bindings) => {
|
|
107
|
+
if (callee?.type === "Identifier") {
|
|
108
|
+
return bindings.directCalls.has(callee.name);
|
|
109
|
+
}
|
|
110
|
+
return callee !== void 0 && isKnownChildProcessMemberCallee(callee, bindings);
|
|
111
|
+
};
|
|
107
112
|
var isDynamicCommandArgument = (argument) => argument?.type === "StringLiteral" ? false : argument?.type === "TemplateLiteral" ? (argument.expressions?.length ?? 0) > 0 : argument !== void 0;
|
|
108
113
|
var isCommandInjectionCall = (node, bindings) => {
|
|
109
114
|
if (node.type !== "CallExpression") {
|
|
@@ -141,9 +146,12 @@ var commandInjectionRule = {
|
|
|
141
146
|
}
|
|
142
147
|
};
|
|
143
148
|
|
|
149
|
+
// src/commands/scan/scan-options.ts
|
|
150
|
+
var withScanOptions = (command) => command.option("--json", "exibe o resultado em JSON").option("--tests", "inclui arquivos de teste na an\xE1lise (por padr\xE3o s\xE3o ignorados)");
|
|
151
|
+
|
|
144
152
|
// src/commands/scan/scan-runner.ts
|
|
145
153
|
import { writeFile } from "fs/promises";
|
|
146
|
-
import { join as
|
|
154
|
+
import { join as join3 } from "path";
|
|
147
155
|
import chalk2 from "chalk";
|
|
148
156
|
import { Listr } from "listr2";
|
|
149
157
|
|
|
@@ -169,15 +177,53 @@ var formatErrorChain = (error) => {
|
|
|
169
177
|
// src/reporters/console.reporter.ts
|
|
170
178
|
import chalk from "chalk";
|
|
171
179
|
import Table from "cli-table3";
|
|
180
|
+
|
|
181
|
+
// src/scanner/scan-result.ts
|
|
182
|
+
var DEPENDENCY_AUDIT_NOTE = 'Dependency audit n\xE3o foi inclu\xEDdo neste scan \u2014 rode "codesentry dependency-audit" separadamente.';
|
|
183
|
+
var ZERO_SEMGREP_COVERAGE_WARNING = "Semgrep n\xE3o analisou nenhum arquivo nesta execu\xE7\xE3o \u2014 verifique se o ruleset offline est\xE1 instalado/preparado.";
|
|
184
|
+
var mergeScanResults = (nativeResult, semgrepResult) => ({
|
|
185
|
+
scannedFiles: nativeResult.scannedFiles,
|
|
186
|
+
findings: [...nativeResult.findings, ...semgrepResult.findings],
|
|
187
|
+
durationMs: nativeResult.durationMs + semgrepResult.durationMs,
|
|
188
|
+
engines: {
|
|
189
|
+
codesentry: nativeResult.engines?.codesentry ?? nativeResult.scannedFiles,
|
|
190
|
+
semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
var finalizeScanResult = (result, dependencyAuditCoverage2 = false) => ({
|
|
194
|
+
...result,
|
|
195
|
+
engines: { ...result.engines, dependencyAudit: dependencyAuditCoverage2 },
|
|
196
|
+
warnings: result.engines?.semgrep === 0 ? [...result.warnings ?? [], ZERO_SEMGREP_COVERAGE_WARNING] : result.warnings
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/reporters/console.reporter.ts
|
|
172
200
|
var SEVERITY_COLOR = {
|
|
173
201
|
low: (text2) => chalk.gray(text2),
|
|
174
202
|
medium: (text2) => chalk.yellow(text2),
|
|
175
203
|
high: (text2) => chalk.red(text2),
|
|
176
204
|
critical: (text2) => chalk.bgRed.white(text2)
|
|
177
205
|
};
|
|
178
|
-
var
|
|
206
|
+
var semgrepCoverage = (result) => result.engines?.semgrep === void 0 ? void 0 : `CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s)`;
|
|
207
|
+
var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) via npm audit` : void 0;
|
|
208
|
+
var osvCoverage = (result) => result.engines?.osv ? `OSV.dev: ${result.engines.osv.checked}/${result.engines.osv.total} verificados` : void 0;
|
|
209
|
+
var coverageParts = (result) => [semgrepCoverage(result), dependencyAuditCoverage(result), osvCoverage(result)].filter(
|
|
210
|
+
(part) => part !== void 0
|
|
211
|
+
);
|
|
212
|
+
var coverageText = (result) => {
|
|
213
|
+
const parts = coverageParts(result);
|
|
214
|
+
return parts.length ? ` ${parts.join("; ")}.` : "";
|
|
215
|
+
};
|
|
216
|
+
var printNotes = (result) => {
|
|
217
|
+
if (result.engines?.dependencyAudit === false) {
|
|
218
|
+
console.log(chalk.cyan(DEPENDENCY_AUDIT_NOTE));
|
|
219
|
+
}
|
|
220
|
+
for (const warning of result.warnings ?? []) {
|
|
221
|
+
console.log(chalk.yellow(`Aviso: ${warning}`));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
179
224
|
var printCleanReport = (result, coverage) => {
|
|
180
225
|
console.log(chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`));
|
|
226
|
+
printNotes(result);
|
|
181
227
|
};
|
|
182
228
|
var findingsTable = (result) => {
|
|
183
229
|
const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
|
|
@@ -201,6 +247,7 @@ var printFindingsReport = (result, coverage) => {
|
|
|
201
247
|
${result.findings.length} problema(s) encontrado(s) em ${result.scannedFiles} arquivo(s) (${result.durationMs}ms).${coverage}`
|
|
202
248
|
)
|
|
203
249
|
);
|
|
250
|
+
printNotes(result);
|
|
204
251
|
};
|
|
205
252
|
var printConsoleReport = (result) => {
|
|
206
253
|
const coverage = coverageText(result);
|
|
@@ -261,8 +308,10 @@ var reportHeader = (result, generatedAt) => [
|
|
|
261
308
|
],
|
|
262
309
|
`- **Dura\xE7\xE3o:** ${result.durationMs}ms`,
|
|
263
310
|
`- **Total de problemas:** ${result.findings.length}`,
|
|
311
|
+
...result.engines?.dependencyAudit === false ? [`- **Nota:** ${DEPENDENCY_AUDIT_NOTE}`] : [],
|
|
264
312
|
""
|
|
265
313
|
];
|
|
314
|
+
var warningsSection = (result) => (result.warnings ?? []).length === 0 ? [] : ["## Avisos", "", ...(result.warnings ?? []).map((warning) => `- ${warning}`), ""];
|
|
266
315
|
var summaryTable = (bySeverity) => {
|
|
267
316
|
const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
|
|
268
317
|
for (const severity of SEVERITY_ORDER) {
|
|
@@ -281,76 +330,410 @@ var severitySections = (bySeverity) => {
|
|
|
281
330
|
}
|
|
282
331
|
return lines;
|
|
283
332
|
};
|
|
333
|
+
var osvCheckedSection = (result) => {
|
|
334
|
+
const { osvCheckedPackages } = result;
|
|
335
|
+
if (!osvCheckedPackages || osvCheckedPackages.length === 0) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const checked = result.engines?.osv?.checked ?? osvCheckedPackages.length;
|
|
339
|
+
const total = result.engines?.osv?.total ?? osvCheckedPackages.length;
|
|
340
|
+
return [
|
|
341
|
+
`## Depend\xEAncias verificadas no OSV.dev (${checked}/${total})`,
|
|
342
|
+
"",
|
|
343
|
+
...osvCheckedPackages.map((pkg) => `- ${pkg}`),
|
|
344
|
+
""
|
|
345
|
+
];
|
|
346
|
+
};
|
|
284
347
|
var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
|
|
285
348
|
const bySeverity = groupBy(result.findings, (f) => f.severity);
|
|
286
|
-
return [
|
|
287
|
-
|
|
288
|
-
|
|
349
|
+
return [
|
|
350
|
+
...reportHeader(result, generatedAt),
|
|
351
|
+
...warningsSection(result),
|
|
352
|
+
...summaryTable(bySeverity),
|
|
353
|
+
...severitySections(bySeverity),
|
|
354
|
+
...osvCheckedSection(result)
|
|
355
|
+
].join("\n");
|
|
289
356
|
};
|
|
290
357
|
|
|
291
|
-
// src/scanner/
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
358
|
+
// src/scanner/dependency-audit.ts
|
|
359
|
+
import { execFile } from "child_process";
|
|
360
|
+
import { readFile } from "fs/promises";
|
|
361
|
+
import { join } from "path";
|
|
362
|
+
import { promisify } from "util";
|
|
363
|
+
|
|
364
|
+
// src/scanner/run-with-concurrency-limit.ts
|
|
365
|
+
import pLimit from "p-limit";
|
|
366
|
+
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
367
|
+
const limit = pLimit(concurrency);
|
|
368
|
+
return Promise.all(items.map((item) => limit(() => task(item))));
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/scanner/osv-client.ts
|
|
372
|
+
var OSV_BATCH_CHUNK_SIZE = 100;
|
|
373
|
+
var OSV_DETAIL_CONCURRENCY = 10;
|
|
374
|
+
var OSV_API_BASE = "https://api.osv.dev/v1";
|
|
375
|
+
var SEVERITY_MAP = {
|
|
376
|
+
LOW: "low",
|
|
377
|
+
MODERATE: "medium",
|
|
378
|
+
HIGH: "high",
|
|
379
|
+
CRITICAL: "critical"
|
|
380
|
+
};
|
|
381
|
+
var chunk = (items, size) => {
|
|
382
|
+
const chunks = [];
|
|
383
|
+
for (let i = 0; i < items.length; i += size) {
|
|
384
|
+
chunks.push(items.slice(i, i + size));
|
|
299
385
|
}
|
|
300
|
-
|
|
386
|
+
return chunks;
|
|
387
|
+
};
|
|
388
|
+
var zipVulnIdsByPackage = (packages, body) => {
|
|
389
|
+
const vulnIdsByPackage = /* @__PURE__ */ new Map();
|
|
390
|
+
packages.forEach((pkg, index) => {
|
|
391
|
+
const ids = body.results?.[index]?.vulns?.map((v) => v.id);
|
|
392
|
+
if (ids?.length) {
|
|
393
|
+
vulnIdsByPackage.set(`${pkg.name}@${pkg.version}`, ids);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return vulnIdsByPackage;
|
|
397
|
+
};
|
|
398
|
+
var queryBatchChunk = async (packages, fetchImpl) => {
|
|
399
|
+
try {
|
|
400
|
+
const response = await fetchImpl(`${OSV_API_BASE}/querybatch`, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
body: JSON.stringify({
|
|
404
|
+
queries: packages.map((p) => ({
|
|
405
|
+
package: { name: p.name, ecosystem: "npm" },
|
|
406
|
+
version: p.version
|
|
407
|
+
}))
|
|
408
|
+
})
|
|
409
|
+
});
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
|
|
412
|
+
}
|
|
413
|
+
const body = await response.json();
|
|
414
|
+
return { vulnIdsByPackage: zipVulnIdsByPackage(packages, body), checkedPackages: packages, failedCount: 0 };
|
|
415
|
+
} catch {
|
|
416
|
+
return { vulnIdsByPackage: /* @__PURE__ */ new Map(), checkedPackages: [], failedCount: packages.length };
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var queryOsvBatch = async (packages, fetchImpl = fetch) => {
|
|
420
|
+
const vulnIdsByPackage = /* @__PURE__ */ new Map();
|
|
421
|
+
const checkedPackages = [];
|
|
422
|
+
let failedCount = 0;
|
|
423
|
+
try {
|
|
424
|
+
for (const packageChunk of chunk(packages, OSV_BATCH_CHUNK_SIZE)) {
|
|
425
|
+
const result = await queryBatchChunk(packageChunk, fetchImpl);
|
|
426
|
+
result.vulnIdsByPackage.forEach((ids, key) => vulnIdsByPackage.set(key, ids));
|
|
427
|
+
checkedPackages.push(...result.checkedPackages);
|
|
428
|
+
failedCount += result.failedCount;
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
failedCount += packages.length - checkedPackages.length;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
vulnIdsByPackage,
|
|
435
|
+
checkedPackages,
|
|
436
|
+
warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel consultar o OSV.dev para ${failedCount} pacote(s).` : void 0
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
var fetchOneVulnerabilityDetail = async (id, fetchImpl, detailsById) => {
|
|
440
|
+
try {
|
|
441
|
+
const response = await fetchImpl(`${OSV_API_BASE}/vulns/${id}`);
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
detailsById.set(id, await response.json());
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var fetchOsvVulnerabilityDetails = async (ids, fetchImpl = fetch) => {
|
|
452
|
+
const uniqueIds = [...new Set(ids)];
|
|
453
|
+
const detailsById = /* @__PURE__ */ new Map();
|
|
454
|
+
let results;
|
|
455
|
+
try {
|
|
456
|
+
results = await runWithConcurrencyLimit(
|
|
457
|
+
uniqueIds,
|
|
458
|
+
OSV_DETAIL_CONCURRENCY,
|
|
459
|
+
(id) => fetchOneVulnerabilityDetail(id, fetchImpl, detailsById)
|
|
460
|
+
);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
const failedCount = results.filter((ok) => !ok).length;
|
|
465
|
+
return {
|
|
466
|
+
detailsById,
|
|
467
|
+
warning: failedCount > 0 ? `N\xE3o foi poss\xEDvel obter detalhes do OSV.dev para ${failedCount} advisory(s).` : void 0
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
var isMatchingNpmPackage = (affected, packageName) => affected.package.ecosystem === "npm" && affected.package.name === packageName;
|
|
471
|
+
var extractFixedVersions = (vuln, packageName) => {
|
|
472
|
+
const fixedVersions = (vuln.affected ?? []).filter((affected) => isMatchingNpmPackage(affected, packageName)).flatMap((affected) => affected.ranges ?? []).flatMap((range) => range.events).map((event) => event.fixed).filter((fixed) => Boolean(fixed));
|
|
473
|
+
return [...new Set(fixedVersions)];
|
|
474
|
+
};
|
|
475
|
+
var mapOsvSeverity = (vuln) => {
|
|
476
|
+
const severity = vuln.database_specific?.severity;
|
|
477
|
+
if (!severity) {
|
|
478
|
+
return "medium";
|
|
479
|
+
}
|
|
480
|
+
return SEVERITY_MAP[severity] ?? "medium";
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/scanner/package-lock-parser.ts
|
|
484
|
+
var NODE_MODULES_SEGMENT = "node_modules/";
|
|
485
|
+
var isUnresolvableSource = (resolved) => resolved !== void 0 && (resolved.startsWith("file:") || resolved.startsWith("git"));
|
|
486
|
+
var nameFromKey = (key) => {
|
|
487
|
+
const lastIndex = key.lastIndexOf(NODE_MODULES_SEGMENT);
|
|
488
|
+
return lastIndex === -1 ? void 0 : key.slice(lastIndex + NODE_MODULES_SEGMENT.length);
|
|
489
|
+
};
|
|
490
|
+
var toLockedPackage = (key, entry) => {
|
|
491
|
+
if (entry.link || !entry.version || isUnresolvableSource(entry.resolved)) {
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
494
|
+
const name = entry.name ?? nameFromKey(key);
|
|
495
|
+
return name ? { name, version: entry.version } : void 0;
|
|
496
|
+
};
|
|
497
|
+
var assertSupportedLockfileVersion = (lockfileVersion) => {
|
|
498
|
+
if (lockfileVersion !== 2 && lockfileVersion !== 3) {
|
|
499
|
+
throw new Error("lockfileVersion 1 n\xE3o \xE9 suportado \u2014 regenere o lockfile com npm 7+.");
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var parsePackageLock = (rawJson) => {
|
|
503
|
+
const parsed = JSON.parse(rawJson);
|
|
504
|
+
assertSupportedLockfileVersion(parsed.lockfileVersion);
|
|
505
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
506
|
+
Object.entries(parsed.packages ?? {}).filter(([key]) => key.includes(NODE_MODULES_SEGMENT)).forEach(([key, entry]) => {
|
|
507
|
+
const locked = toLockedPackage(key, entry);
|
|
508
|
+
if (locked) {
|
|
509
|
+
deduped.set(`${locked.name}@${locked.version}`, locked);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
return [...deduped.values()];
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/scanner/dependency-audit.ts
|
|
516
|
+
var execFileAsync = promisify(execFile);
|
|
517
|
+
var SEVERITY_MAP2 = {
|
|
518
|
+
info: "low",
|
|
519
|
+
low: "low",
|
|
520
|
+
moderate: "medium",
|
|
521
|
+
high: "high",
|
|
522
|
+
critical: "critical"
|
|
523
|
+
};
|
|
524
|
+
var vulnerabilityTitle = (vulnerability) => {
|
|
525
|
+
const firstVia = vulnerability.via[0];
|
|
526
|
+
if (typeof firstVia === "object" && firstVia?.title) {
|
|
527
|
+
return firstVia.title;
|
|
528
|
+
}
|
|
529
|
+
return 'ver "npm audit" para detalhes';
|
|
530
|
+
};
|
|
531
|
+
var npmFixSuggestion = (vulnerability) => {
|
|
532
|
+
const { fixAvailable } = vulnerability;
|
|
533
|
+
if (fixAvailable === false) {
|
|
534
|
+
return "sem corre\xE7\xE3o dispon\xEDvel ainda";
|
|
535
|
+
}
|
|
536
|
+
if (fixAvailable === true) {
|
|
537
|
+
return `atualize para uma vers\xE3o fora do intervalo vulner\xE1vel (${vulnerability.range})`;
|
|
538
|
+
}
|
|
539
|
+
return `atualize para ${fixAvailable.name}@${fixAvailable.version}`;
|
|
540
|
+
};
|
|
541
|
+
var mapAuditReportToFindings = (report) => {
|
|
542
|
+
return Object.values(report.vulnerabilities).map((vulnerability) => ({
|
|
543
|
+
ruleId: "dependency-audit",
|
|
544
|
+
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)} \u2014 ${npmFixSuggestion(vulnerability)}`,
|
|
545
|
+
file: "package.json",
|
|
546
|
+
line: 1,
|
|
547
|
+
severity: SEVERITY_MAP2[vulnerability.severity]
|
|
548
|
+
}));
|
|
549
|
+
};
|
|
550
|
+
var osvFixSuggestion = (fixedVersions) => fixedVersions.length ? `atualize para ${fixedVersions.join(" ou ")}` : "nenhuma vers\xE3o corrigida publicada pelo OSV.dev ainda";
|
|
551
|
+
var buildOsvFinding = (pkg, vuln) => {
|
|
552
|
+
const fixedVersions = extractFixedVersions(vuln, pkg.name);
|
|
553
|
+
return {
|
|
554
|
+
ruleId: "dependency-audit",
|
|
555
|
+
message: `OSV ${vuln.id}: ${pkg.name}@${pkg.version} (${mapOsvSeverity(vuln)}) \u2014 ${vuln.summary ?? "ver OSV.dev para detalhes"} \u2014 ${osvFixSuggestion(fixedVersions)}`,
|
|
556
|
+
file: "package-lock.json",
|
|
557
|
+
line: 1,
|
|
558
|
+
severity: mapOsvSeverity(vuln)
|
|
559
|
+
};
|
|
560
|
+
};
|
|
561
|
+
var findingsForLockedPackage = (pkg, vulnIdsByPackage, detailsById) => {
|
|
562
|
+
const ids = vulnIdsByPackage.get(`${pkg.name}@${pkg.version}`) ?? [];
|
|
563
|
+
return ids.map((id) => detailsById.get(id)).filter((vuln) => vuln !== void 0).map((vuln) => buildOsvFinding(pkg, vuln));
|
|
564
|
+
};
|
|
565
|
+
var mapOsvFindingsToRuleFindings = (lockedPackages, vulnIdsByPackage, detailsById, npmFlaggedNames) => lockedPackages.filter((pkg) => !npmFlaggedNames.has(pkg.name)).flatMap((pkg) => findingsForLockedPackage(pkg, vulnIdsByPackage, detailsById));
|
|
566
|
+
var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
567
|
+
var hasVulnerabilitiesRecord = (value) => typeof value === "object" && value !== null && typeof value.vulnerabilities === "object" && value.vulnerabilities !== null;
|
|
568
|
+
var normalizeNpmAuditReport = (raw) => {
|
|
569
|
+
if (hasVulnerabilitiesRecord(raw)) {
|
|
570
|
+
return { report: { vulnerabilities: raw.vulnerabilities } };
|
|
571
|
+
}
|
|
572
|
+
const errorSummary = typeof raw === "object" && raw !== null && "error" in raw ? raw.error?.summary ?? "formato de resposta inesperado" : "formato de resposta inesperado";
|
|
573
|
+
return {
|
|
574
|
+
report: { vulnerabilities: {} },
|
|
575
|
+
warning: `"npm audit" n\xE3o retornou um relat\xF3rio v\xE1lido: ${errorSummary}.`
|
|
576
|
+
};
|
|
577
|
+
};
|
|
578
|
+
var combineWarnings = (...warnings) => {
|
|
579
|
+
const present = warnings.filter((warning) => Boolean(warning));
|
|
580
|
+
return present.length ? present.join(" ") : void 0;
|
|
581
|
+
};
|
|
582
|
+
var fetchDetailsForIds = (ids, fetchImpl) => ids.length ? fetchOsvVulnerabilityDetails(ids, fetchImpl) : Promise.resolve({ detailsById: /* @__PURE__ */ new Map(), warning: void 0 });
|
|
583
|
+
var auditPackagesWithOsv = async (lockedPackages, npmFlaggedNames, fetchImpl = fetch) => {
|
|
584
|
+
try {
|
|
585
|
+
return await collectOsvAuditResult(lockedPackages, npmFlaggedNames, fetchImpl);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return {
|
|
588
|
+
findings: [],
|
|
589
|
+
checkedPackages: [],
|
|
590
|
+
warning: `N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) => {
|
|
595
|
+
try {
|
|
596
|
+
const {
|
|
597
|
+
vulnIdsByPackage,
|
|
598
|
+
checkedPackages,
|
|
599
|
+
warning: batchWarning
|
|
600
|
+
} = await queryOsvBatch(lockedPackages, fetchImpl);
|
|
601
|
+
const ids = [...new Set([...vulnIdsByPackage.values()].flat())];
|
|
602
|
+
const { detailsById, warning: detailsWarning } = await fetchDetailsForIds(ids, fetchImpl);
|
|
603
|
+
return {
|
|
604
|
+
findings: mapOsvFindingsToRuleFindings(
|
|
605
|
+
lockedPackages,
|
|
606
|
+
vulnIdsByPackage,
|
|
607
|
+
detailsById,
|
|
608
|
+
npmFlaggedNames
|
|
609
|
+
),
|
|
610
|
+
checkedPackages,
|
|
611
|
+
warning: combineWarnings(batchWarning, detailsWarning)
|
|
612
|
+
};
|
|
613
|
+
} catch (error) {
|
|
614
|
+
throw new Error("N\xE3o foi poss\xEDvel consolidar os resultados do OSV.dev.", { cause: error });
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var runNpmAudit = async (targetDir) => {
|
|
618
|
+
let stdout;
|
|
619
|
+
try {
|
|
620
|
+
({ stdout } = await execFileAsync("npm", ["audit", "--json"], {
|
|
621
|
+
cwd: targetDir,
|
|
622
|
+
maxBuffer: 10 * 1024 * 1024
|
|
623
|
+
}));
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const stdoutFromError = error.stdout;
|
|
626
|
+
if (!stdoutFromError) {
|
|
627
|
+
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
628
|
+
}
|
|
629
|
+
stdout = stdoutFromError;
|
|
630
|
+
}
|
|
631
|
+
return JSON.parse(stdout);
|
|
632
|
+
};
|
|
633
|
+
var auditPackagesFromLockfile = async (targetDir, npmFlaggedNames, fetchImpl) => {
|
|
634
|
+
try {
|
|
635
|
+
const raw = await readFile(join(targetDir, "package-lock.json"), "utf-8");
|
|
636
|
+
const lockedPackages = parsePackageLock(raw);
|
|
637
|
+
const osvResult = await auditPackagesWithOsv(lockedPackages, npmFlaggedNames, fetchImpl);
|
|
638
|
+
return {
|
|
639
|
+
findings: osvResult.findings,
|
|
640
|
+
packagesAudited: lockedPackages.length,
|
|
641
|
+
osvChecked: { checked: osvResult.checkedPackages.length, total: lockedPackages.length },
|
|
642
|
+
osvCheckedPackages: osvResult.checkedPackages.map((pkg) => `${pkg.name}@${pkg.version}`).sort(),
|
|
643
|
+
warning: osvResult.warning
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return {
|
|
647
|
+
findings: [],
|
|
648
|
+
packagesAudited: false,
|
|
649
|
+
warning: `N\xE3o foi poss\xEDvel checar o OSV.dev: ${errorMessage(error)}.`
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
var runDependencyAudit = async (targetDir, fetchImpl = fetch) => {
|
|
654
|
+
const startedAt = Date.now();
|
|
655
|
+
try {
|
|
656
|
+
const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(await runNpmAudit(targetDir));
|
|
657
|
+
const npmFindings = mapAuditReportToFindings(npmReport);
|
|
658
|
+
const npmFlaggedNames = new Set(Object.keys(npmReport.vulnerabilities));
|
|
659
|
+
const osv = await auditPackagesFromLockfile(targetDir, npmFlaggedNames, fetchImpl);
|
|
660
|
+
const warnings = combineWarnings(npmWarning, osv.warning);
|
|
661
|
+
return {
|
|
662
|
+
scannedFiles: 1,
|
|
663
|
+
findings: [...npmFindings, ...osv.findings],
|
|
664
|
+
durationMs: Date.now() - startedAt,
|
|
665
|
+
engines: {
|
|
666
|
+
dependencyAudit: osv.packagesAudited === false ? npmFlaggedNames.size : osv.packagesAudited,
|
|
667
|
+
osv: osv.osvChecked
|
|
668
|
+
},
|
|
669
|
+
osvCheckedPackages: osv.osvCheckedPackages,
|
|
670
|
+
warnings: warnings ? [warnings] : void 0
|
|
671
|
+
};
|
|
672
|
+
} catch (error) {
|
|
673
|
+
throw new Error(`N\xE3o foi poss\xEDvel auditar depend\xEAncias em "${targetDir}".`, { cause: error });
|
|
674
|
+
}
|
|
675
|
+
};
|
|
301
676
|
|
|
302
677
|
// src/scanner/scanner.ts
|
|
303
|
-
import { readFile } from "fs/promises";
|
|
678
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
304
679
|
import { availableParallelism } from "os";
|
|
305
680
|
|
|
306
681
|
// src/scanner/file-finder.ts
|
|
307
682
|
import { readdir } from "fs/promises";
|
|
308
|
-
import { join } from "path";
|
|
683
|
+
import { join as join2 } from "path";
|
|
684
|
+
|
|
685
|
+
// src/scanner/ignore-patterns.ts
|
|
686
|
+
var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next", ".angular"];
|
|
687
|
+
var TEST_DIR_NAMES = ["tests", "test", "__tests__"];
|
|
688
|
+
var TEST_FILE_GLOBS = ["*.spec.*", "*.test.*"];
|
|
689
|
+
var TEST_FILE_NAME_PATTERN = /\.(spec|test)\.[^./]+$/;
|
|
690
|
+
var isIgnoredDirName = (name, includeTests = false) => ALWAYS_IGNORED_DIR_NAMES.includes(name) || !includeTests && TEST_DIR_NAMES.includes(name);
|
|
691
|
+
var isTestFileName = (fileName) => TEST_FILE_NAME_PATTERN.test(fileName);
|
|
692
|
+
|
|
693
|
+
// src/scanner/file-finder.ts
|
|
309
694
|
var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
|
|
310
|
-
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "tests"]);
|
|
311
695
|
var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
312
|
-
var
|
|
696
|
+
var filesFromDirectory = async (currentDir, entry, includeTests) => {
|
|
697
|
+
return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join2(currentDir, entry.name), includeTests);
|
|
698
|
+
};
|
|
699
|
+
var filesFromFile = (currentDir, entry, includeTests) => {
|
|
700
|
+
return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join2(currentDir, entry.name)] : [];
|
|
701
|
+
};
|
|
702
|
+
var filesFromEntry = async (currentDir, entry, includeTests) => {
|
|
313
703
|
if (entry.isDirectory()) {
|
|
314
|
-
return
|
|
704
|
+
return filesFromDirectory(currentDir, entry, includeTests);
|
|
315
705
|
}
|
|
316
|
-
return
|
|
706
|
+
return filesFromFile(currentDir, entry, includeTests);
|
|
317
707
|
};
|
|
318
|
-
var walk = async (currentDir) => {
|
|
708
|
+
var walk = async (currentDir, includeTests) => {
|
|
319
709
|
try {
|
|
320
710
|
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
321
|
-
return (await Promise.all(entries.map((entry) => filesFromEntry(currentDir, entry)))).flat();
|
|
711
|
+
return (await Promise.all(entries.map((entry) => filesFromEntry(currentDir, entry, includeTests)))).flat();
|
|
322
712
|
} catch (error) {
|
|
323
713
|
throw new Error(`N\xE3o foi poss\xEDvel listar os arquivos em "${currentDir}".`, { cause: error });
|
|
324
714
|
}
|
|
325
715
|
};
|
|
326
|
-
var findFiles = async (targetDir) => {
|
|
716
|
+
var findFiles = async (targetDir, includeTests = false) => {
|
|
327
717
|
try {
|
|
328
|
-
return await walk(targetDir);
|
|
718
|
+
return await walk(targetDir, includeTests);
|
|
329
719
|
} catch (error) {
|
|
330
720
|
throw new Error(`N\xE3o foi poss\xEDvel listar os arquivos em "${targetDir}".`, { cause: error });
|
|
331
721
|
}
|
|
332
722
|
};
|
|
333
723
|
|
|
334
|
-
// src/scanner/run-with-concurrency-limit.ts
|
|
335
|
-
import pLimit from "p-limit";
|
|
336
|
-
var runWithConcurrencyLimit = async (items, concurrency, task) => {
|
|
337
|
-
const limit = pLimit(concurrency);
|
|
338
|
-
return Promise.all(items.map((item) => limit(() => task(item))));
|
|
339
|
-
};
|
|
340
|
-
|
|
341
724
|
// src/scanner/scanner.ts
|
|
342
725
|
var DEFAULT_SCAN_CONCURRENCY = Math.max(1, Math.min(8, availableParallelism()));
|
|
343
|
-
var
|
|
726
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
344
727
|
var parseErrorFinding = (filePath, error) => ({
|
|
345
728
|
ruleId: "parse-error",
|
|
346
|
-
message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${
|
|
729
|
+
message: `N\xE3o foi poss\xEDvel analisar este arquivo (erro de sintaxe): ${errorMessage2(error)}`,
|
|
347
730
|
file: filePath,
|
|
348
731
|
line: 1,
|
|
349
732
|
severity: "low"
|
|
350
733
|
});
|
|
351
734
|
var readFileContent = async (filePath) => {
|
|
352
735
|
try {
|
|
353
|
-
return await
|
|
736
|
+
return await readFile2(filePath, "utf-8");
|
|
354
737
|
} catch (error) {
|
|
355
738
|
throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
|
|
356
739
|
}
|
|
@@ -380,10 +763,10 @@ var scanFile = async (filePath, rules) => {
|
|
|
380
763
|
throw new Error(`N\xE3o foi poss\xEDvel analisar o arquivo "${filePath}".`, { cause: error });
|
|
381
764
|
}
|
|
382
765
|
};
|
|
383
|
-
var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY) => {
|
|
766
|
+
var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY, includeTests = false) => {
|
|
384
767
|
try {
|
|
385
768
|
const startedAt = Date.now();
|
|
386
|
-
const files = await findFiles(targetDir);
|
|
769
|
+
const files = await findFiles(targetDir, includeTests);
|
|
387
770
|
const findings = (await runWithConcurrencyLimit(files, concurrency, (filePath) => scanFile(filePath, rules))).flat();
|
|
388
771
|
return {
|
|
389
772
|
scannedFiles: files.length,
|
|
@@ -397,9 +780,9 @@ var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY) =
|
|
|
397
780
|
};
|
|
398
781
|
|
|
399
782
|
// src/scanner/semgrep.ts
|
|
400
|
-
import { execFile } from "child_process";
|
|
783
|
+
import { execFile as execFile2 } from "child_process";
|
|
401
784
|
import { delimiter, dirname as dirname2 } from "path";
|
|
402
|
-
import { promisify } from "util";
|
|
785
|
+
import { promisify as promisify2 } from "util";
|
|
403
786
|
|
|
404
787
|
// src/scanner/semgrep-runtime.ts
|
|
405
788
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -416,6 +799,7 @@ var packageForPlatform = (platform, architecture) => {
|
|
|
416
799
|
return void 0;
|
|
417
800
|
};
|
|
418
801
|
var unsupportedRuntimeMessage = (platform, architecture) => `O runtime Semgrep embutido n\xE3o est\xE1 dispon\xEDvel para ${platform}-${architecture}. O CodeSentry oferece suporte a Linux x64 e Windows x64.`;
|
|
802
|
+
var errorReason = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
419
803
|
var resolveBundledSemgrepRuntime = (platform = process.platform, architecture = process.arch, resolveManifestPath = (packageName) => require2.resolve(`${packageName}/runtime.json`)) => {
|
|
420
804
|
const packageName = packageForPlatform(platform, architecture);
|
|
421
805
|
if (!packageName) {
|
|
@@ -432,8 +816,7 @@ var resolveBundledSemgrepRuntime = (platform = process.platform, architecture =
|
|
|
432
816
|
}
|
|
433
817
|
return runtime;
|
|
434
818
|
} catch (error) {
|
|
435
|
-
|
|
436
|
-
throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${reason}`, { cause: error });
|
|
819
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar o runtime Semgrep embutido: ${errorReason(error)}`, { cause: error });
|
|
437
820
|
}
|
|
438
821
|
};
|
|
439
822
|
|
|
@@ -455,7 +838,7 @@ var resolveBundledSemgrepRuleset = () => {
|
|
|
455
838
|
};
|
|
456
839
|
|
|
457
840
|
// src/scanner/semgrep.ts
|
|
458
|
-
var
|
|
841
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
459
842
|
var SEMGREP_SEVERITIES = {
|
|
460
843
|
INFO: "low",
|
|
461
844
|
WARNING: "medium",
|
|
@@ -482,23 +865,16 @@ var mapSemgrepReportToFindings = (report) => report.results.map((finding) => ({
|
|
|
482
865
|
severity: SEMGREP_SEVERITIES[finding.extra.severity] ?? "medium"
|
|
483
866
|
}));
|
|
484
867
|
var outputFromError = (error) => typeof error.stdout === "string" ? error.stdout : void 0;
|
|
485
|
-
var
|
|
868
|
+
var excludeArgs = (names) => names.flatMap((name) => ["--exclude", name]);
|
|
869
|
+
var semgrepArgs = (ruleset, includeTests) => [
|
|
486
870
|
"scan",
|
|
487
871
|
"--config",
|
|
488
872
|
ruleset,
|
|
489
873
|
"--metrics=off",
|
|
490
874
|
"--json",
|
|
491
875
|
"--quiet",
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"--exclude",
|
|
495
|
-
".git",
|
|
496
|
-
"--exclude",
|
|
497
|
-
"dist",
|
|
498
|
-
"--exclude",
|
|
499
|
-
".next",
|
|
500
|
-
"--exclude",
|
|
501
|
-
"tests",
|
|
876
|
+
...excludeArgs(ALWAYS_IGNORED_DIR_NAMES),
|
|
877
|
+
...includeTests ? [] : excludeArgs([...TEST_DIR_NAMES, ...TEST_FILE_GLOBS]),
|
|
502
878
|
// Não repetir targetDir aqui: o processo já roda com cwd = targetDir,
|
|
503
879
|
// então o alvo relativo a esse cwd é o diretório atual.
|
|
504
880
|
"."
|
|
@@ -511,9 +887,9 @@ var semgrepEnvironment = (runtime) => {
|
|
|
511
887
|
PATH: `${semgrepDir}${delimiter}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
|
|
512
888
|
};
|
|
513
889
|
};
|
|
514
|
-
var executeSemgrep = async (targetDir, runtime, ruleset, execute) => {
|
|
890
|
+
var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests) => {
|
|
515
891
|
try {
|
|
516
|
-
const { stdout } = await execute(runtime.semgrep, semgrepArgs(ruleset), {
|
|
892
|
+
const { stdout } = await execute(runtime.semgrep, semgrepArgs(ruleset, includeTests), {
|
|
517
893
|
cwd: targetDir,
|
|
518
894
|
maxBuffer: 20 * 1024 * 1024,
|
|
519
895
|
env: semgrepEnvironment(runtime)
|
|
@@ -527,10 +903,10 @@ var executeSemgrep = async (targetDir, runtime, ruleset, execute) => {
|
|
|
527
903
|
throw new Error("N\xE3o foi poss\xEDvel executar o Semgrep embutido.", { cause: error });
|
|
528
904
|
}
|
|
529
905
|
};
|
|
530
|
-
var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute =
|
|
906
|
+
var runBundledSemgrep = async (targetDir, runtime = resolveBundledSemgrepRuntime(), ruleset = resolveBundledSemgrepRuleset(), execute = execFileAsync2, includeTests = false) => {
|
|
531
907
|
const startedAt = Date.now();
|
|
532
908
|
try {
|
|
533
|
-
const report = parseSemgrepReport(await executeSemgrep(targetDir, runtime, ruleset, execute));
|
|
909
|
+
const report = parseSemgrepReport(await executeSemgrep(targetDir, runtime, ruleset, execute, includeTests));
|
|
534
910
|
const scannedFiles = report.paths?.scanned.length ?? 0;
|
|
535
911
|
return {
|
|
536
912
|
scannedFiles,
|
|
@@ -551,7 +927,7 @@ var generateMarkdownReportFilename = (date = /* @__PURE__ */ new Date()) => {
|
|
|
551
927
|
};
|
|
552
928
|
|
|
553
929
|
// src/commands/scan/scan-runner.ts
|
|
554
|
-
var
|
|
930
|
+
var errorMessage3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
555
931
|
var reportScanFailure = (error) => {
|
|
556
932
|
process.exitCode = 1;
|
|
557
933
|
console.error(`Falha ao executar o scan: ${formatErrorChain(error)}`);
|
|
@@ -567,25 +943,53 @@ var writeMarkdownReportIfNeeded = async (result, targetDir) => {
|
|
|
567
943
|
if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
|
|
568
944
|
return;
|
|
569
945
|
}
|
|
570
|
-
const filePath =
|
|
946
|
+
const filePath = join3(targetDir, generateMarkdownReportFilename());
|
|
571
947
|
try {
|
|
572
948
|
await writeFile(filePath, toMarkdownReport(result), "utf-8");
|
|
573
949
|
console.log(chalk2.cyan(`
|
|
574
950
|
Relat\xF3rio detalhado gerado em: ${filePath}`));
|
|
575
951
|
} catch (error) {
|
|
576
|
-
console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${
|
|
952
|
+
console.error(chalk2.red(`N\xE3o foi poss\xEDvel gerar o relat\xF3rio Markdown: ${errorMessage3(error)}`));
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
var runNativeAndSemgrep = async (path, rules, options) => {
|
|
956
|
+
try {
|
|
957
|
+
const includeTests = options.tests ?? false;
|
|
958
|
+
const nativeResult = await runScan(path, rules, options.concurrency, includeTests);
|
|
959
|
+
return options.semgrep ? mergeScanResults(
|
|
960
|
+
nativeResult,
|
|
961
|
+
await runBundledSemgrep(path, void 0, options.config, void 0, includeTests)
|
|
962
|
+
) : nativeResult;
|
|
963
|
+
} catch (error) {
|
|
964
|
+
throw new Error(`Falha durante a an\xE1lise: ${errorMessage3(error)}`, { cause: error });
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
var runOptionalDependencyAudit = async (path, merged) => {
|
|
968
|
+
try {
|
|
969
|
+
const auditResult = await runDependencyAudit(path);
|
|
970
|
+
return finalizeScanResult(
|
|
971
|
+
{
|
|
972
|
+
...merged,
|
|
973
|
+
findings: [...merged.findings, ...auditResult.findings],
|
|
974
|
+
warnings: [...merged.warnings ?? [], ...auditResult.warnings ?? []],
|
|
975
|
+
engines: { ...merged.engines, osv: auditResult.engines?.osv },
|
|
976
|
+
osvCheckedPackages: auditResult.osvCheckedPackages
|
|
977
|
+
},
|
|
978
|
+
auditResult.engines?.dependencyAudit ?? false
|
|
979
|
+
);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
return finalizeScanResult({
|
|
982
|
+
...merged,
|
|
983
|
+
warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`]
|
|
984
|
+
});
|
|
577
985
|
}
|
|
578
986
|
};
|
|
579
987
|
var runScanEngines = async (path, rules, options) => {
|
|
580
988
|
try {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
return nativeResult;
|
|
584
|
-
}
|
|
585
|
-
const semgrepResult = await runBundledSemgrep(path, void 0, options.config);
|
|
586
|
-
return mergeScanResults(nativeResult, semgrepResult);
|
|
989
|
+
const merged = await runNativeAndSemgrep(path, rules, options);
|
|
990
|
+
return options.deps ? runOptionalDependencyAudit(path, merged) : finalizeScanResult(merged);
|
|
587
991
|
} catch (error) {
|
|
588
|
-
throw
|
|
992
|
+
throw error;
|
|
589
993
|
}
|
|
590
994
|
};
|
|
591
995
|
var createScanTasks = (path, rules, taskTitle, options, onResult) => new Listr([
|
|
@@ -618,7 +1022,9 @@ var scanAndReport = async (path, rules, taskTitle, options) => {
|
|
|
618
1022
|
|
|
619
1023
|
// src/commands/command-injection/command-injection.command.ts
|
|
620
1024
|
var registerCommandInjectionCommand = (program) => {
|
|
621
|
-
|
|
1025
|
+
withScanOptions(
|
|
1026
|
+
program.command("command-injection").description("Detecta child_process.exec()/execSync() recebendo entrada externa").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1027
|
+
).action(
|
|
622
1028
|
(path, options) => scanAndReport(path, [commandInjectionRule], "Checking command injection...", options)
|
|
623
1029
|
);
|
|
624
1030
|
};
|
|
@@ -689,8 +1095,11 @@ var walkNestingBlock = (node, depth, results) => {
|
|
|
689
1095
|
checkDepth(newDepth, node.loc?.start.line, results);
|
|
690
1096
|
walkChildren(node, newDepth, results);
|
|
691
1097
|
};
|
|
1098
|
+
var selectNonFunctionWalker = (node) => {
|
|
1099
|
+
return node.type === "IfStatement" ? walkIfChain : NESTING_TYPES.has(node.type) ? walkNestingBlock : walkChildren;
|
|
1100
|
+
};
|
|
692
1101
|
var selectWalker = (node) => {
|
|
693
|
-
return FUNCTION_TYPES.has(node.type) ? walkFunction :
|
|
1102
|
+
return FUNCTION_TYPES.has(node.type) ? walkFunction : selectNonFunctionWalker(node);
|
|
694
1103
|
};
|
|
695
1104
|
var walk2 = (node, depth, results) => {
|
|
696
1105
|
selectWalker(node)(node, depth, results);
|
|
@@ -717,61 +1126,13 @@ var deepNestingRule = {
|
|
|
717
1126
|
|
|
718
1127
|
// src/commands/deep-nesting/deep-nesting.command.ts
|
|
719
1128
|
var registerDeepNestingCommand = (program) => {
|
|
720
|
-
|
|
1129
|
+
withScanOptions(
|
|
1130
|
+
program.command("deep-nesting").description("Detecta blocos aninhados al\xE9m do limite recomendado").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1131
|
+
).action(
|
|
721
1132
|
(path, options) => scanAndReport(path, [deepNestingRule], "Checking nesting depth...", options)
|
|
722
1133
|
);
|
|
723
1134
|
};
|
|
724
1135
|
|
|
725
|
-
// src/scanner/dependency-audit.ts
|
|
726
|
-
import { execFile as execFile2 } from "child_process";
|
|
727
|
-
import { promisify as promisify2 } from "util";
|
|
728
|
-
var execFileAsync2 = promisify2(execFile2);
|
|
729
|
-
var SEVERITY_MAP = {
|
|
730
|
-
info: "low",
|
|
731
|
-
low: "low",
|
|
732
|
-
moderate: "medium",
|
|
733
|
-
high: "high",
|
|
734
|
-
critical: "critical"
|
|
735
|
-
};
|
|
736
|
-
var vulnerabilityTitle = (vulnerability) => {
|
|
737
|
-
const firstVia = vulnerability.via[0];
|
|
738
|
-
if (typeof firstVia === "object" && firstVia?.title) {
|
|
739
|
-
return firstVia.title;
|
|
740
|
-
}
|
|
741
|
-
return 'ver "npm audit" para detalhes';
|
|
742
|
-
};
|
|
743
|
-
var mapAuditReportToFindings = (report) => {
|
|
744
|
-
return Object.values(report.vulnerabilities).map((vulnerability) => ({
|
|
745
|
-
ruleId: "dependency-audit",
|
|
746
|
-
message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)}`,
|
|
747
|
-
file: "package.json",
|
|
748
|
-
line: 1,
|
|
749
|
-
severity: SEVERITY_MAP[vulnerability.severity]
|
|
750
|
-
}));
|
|
751
|
-
};
|
|
752
|
-
var runDependencyAudit = async (targetDir) => {
|
|
753
|
-
const startedAt = Date.now();
|
|
754
|
-
let stdout;
|
|
755
|
-
try {
|
|
756
|
-
({ stdout } = await execFileAsync2("npm", ["audit", "--json"], {
|
|
757
|
-
cwd: targetDir,
|
|
758
|
-
maxBuffer: 10 * 1024 * 1024
|
|
759
|
-
}));
|
|
760
|
-
} catch (error) {
|
|
761
|
-
const stdoutFromError = error.stdout;
|
|
762
|
-
if (!stdoutFromError) {
|
|
763
|
-
throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
|
|
764
|
-
}
|
|
765
|
-
stdout = stdoutFromError;
|
|
766
|
-
}
|
|
767
|
-
const report = JSON.parse(stdout);
|
|
768
|
-
return {
|
|
769
|
-
scannedFiles: 1,
|
|
770
|
-
findings: mapAuditReportToFindings(report),
|
|
771
|
-
durationMs: Date.now() - startedAt
|
|
772
|
-
};
|
|
773
|
-
};
|
|
774
|
-
|
|
775
1136
|
// src/commands/dependency-audit/dependency-audit.command.ts
|
|
776
1137
|
var printAuditResult = (result, json) => {
|
|
777
1138
|
if (json) {
|
|
@@ -791,7 +1152,7 @@ var auditAndReport = async (path, options) => {
|
|
|
791
1152
|
};
|
|
792
1153
|
var registerDependencyAuditCommand = (program) => {
|
|
793
1154
|
program.command("dependency-audit").description(
|
|
794
|
-
'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit"; requer npm no PATH e acesso \xE0 rede)'
|
|
1155
|
+
'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit" e OSV.dev; requer npm no PATH e acesso \xE0 rede)'
|
|
795
1156
|
).argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").action((path, options) => auditAndReport(path, options));
|
|
796
1157
|
};
|
|
797
1158
|
|
|
@@ -829,7 +1190,9 @@ var emptyCatchRule = {
|
|
|
829
1190
|
|
|
830
1191
|
// src/commands/empty-catch/empty-catch.command.ts
|
|
831
1192
|
var registerEmptyCatchCommand = (program) => {
|
|
832
|
-
|
|
1193
|
+
withScanOptions(
|
|
1194
|
+
program.command("empty-catch").description("Detecta blocos catch vazios").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1195
|
+
).action(
|
|
833
1196
|
(path, options) => scanAndReport(path, [emptyCatchRule], "Checking empty catch blocks...", options)
|
|
834
1197
|
);
|
|
835
1198
|
};
|
|
@@ -847,12 +1210,24 @@ var hasLimitOption = (options) => {
|
|
|
847
1210
|
return key?.type === "Identifier" && key.name === "limit";
|
|
848
1211
|
}) ?? false;
|
|
849
1212
|
};
|
|
1213
|
+
var getMemberCallParts = (node) => {
|
|
1214
|
+
if (node.type !== "CallExpression") {
|
|
1215
|
+
return { object: void 0, property: void 0, args: void 0 };
|
|
1216
|
+
}
|
|
1217
|
+
const callee = node.callee;
|
|
1218
|
+
const args = node.arguments;
|
|
1219
|
+
if (callee?.type !== "MemberExpression") {
|
|
1220
|
+
return { object: void 0, property: void 0, args };
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
object: callee.object,
|
|
1224
|
+
property: callee.property,
|
|
1225
|
+
args
|
|
1226
|
+
};
|
|
1227
|
+
};
|
|
850
1228
|
var isBodyParserWithoutLimit = (node) => {
|
|
851
|
-
const
|
|
852
|
-
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
853
|
-
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
1229
|
+
const { object, property, args } = getMemberCallParts(node);
|
|
854
1230
|
const isBodyParserCall = object?.type === "Identifier" && BODY_PARSER_OBJECTS.has(object.name) && property?.type === "Identifier" && BODY_PARSER_METHODS.has(property.name);
|
|
855
|
-
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
856
1231
|
return isBodyParserCall && !hasLimitOption(args?.[0]);
|
|
857
1232
|
};
|
|
858
1233
|
var findMissingBodyLimitLines = (filePath, content) => {
|
|
@@ -881,11 +1256,85 @@ var expressMissingBodyLimitRule = {
|
|
|
881
1256
|
|
|
882
1257
|
// src/commands/express-missing-body-limit/express-missing-body-limit.command.ts
|
|
883
1258
|
var registerExpressMissingBodyLimitCommand = (program) => {
|
|
884
|
-
|
|
1259
|
+
withScanOptions(
|
|
1260
|
+
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", ".")
|
|
1261
|
+
).action(
|
|
885
1262
|
(path, options) => scanAndReport(path, [expressMissingBodyLimitRule], "Checking Express body limits...", options)
|
|
886
1263
|
);
|
|
887
1264
|
};
|
|
888
1265
|
|
|
1266
|
+
// src/rules/hardcoded-authorization-value.rule.ts
|
|
1267
|
+
var HEADER_OR_COOKIE_NAME_PATTERN = /headers|cookies?/i;
|
|
1268
|
+
var accessObjectName = (object) => {
|
|
1269
|
+
const objectProperty = object?.type === "MemberExpression" ? object.property : object;
|
|
1270
|
+
return objectProperty?.type === "Identifier" ? objectProperty.name : void 0;
|
|
1271
|
+
};
|
|
1272
|
+
var isHeaderOrCookieAccessCall = (node) => {
|
|
1273
|
+
const callee = node?.callee;
|
|
1274
|
+
const property = callee?.property;
|
|
1275
|
+
const object = callee?.object;
|
|
1276
|
+
return node?.type === "CallExpression" && callee?.type === "MemberExpression" && property?.type === "Identifier" && property.name === "get" && HEADER_OR_COOKIE_NAME_PATTERN.test(accessObjectName(object) ?? "");
|
|
1277
|
+
};
|
|
1278
|
+
var collectStringConstants = (sourceFile) => {
|
|
1279
|
+
const constants = /* @__PURE__ */ new Map();
|
|
1280
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1281
|
+
if (node.type !== "VariableDeclarator") {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const id = node.id;
|
|
1285
|
+
const init = node.init;
|
|
1286
|
+
if (id?.type === "Identifier" && init?.type === "StringLiteral") {
|
|
1287
|
+
constants.set(id.name, init.value);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
return constants;
|
|
1291
|
+
};
|
|
1292
|
+
var isConstantStringSide = (node, constants) => {
|
|
1293
|
+
if (node?.type === "StringLiteral") {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
return node?.type === "Identifier" && constants.has(node.name);
|
|
1297
|
+
};
|
|
1298
|
+
var isHardcodedAuthorizationComparison = (node, constants) => {
|
|
1299
|
+
const left = node.left;
|
|
1300
|
+
const right = node.right;
|
|
1301
|
+
const isEquality = node.operator === "===" || node.operator === "==";
|
|
1302
|
+
return node.type === "BinaryExpression" && isEquality && (isHeaderOrCookieAccessCall(left) && isConstantStringSide(right, constants) || isHeaderOrCookieAccessCall(right) && isConstantStringSide(left, constants));
|
|
1303
|
+
};
|
|
1304
|
+
var findHardcodedAuthorizationLines = (filePath, content) => {
|
|
1305
|
+
const lines = /* @__PURE__ */ new Set();
|
|
1306
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
1307
|
+
const constants = collectStringConstants(sourceFile);
|
|
1308
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
1309
|
+
if (isHardcodedAuthorizationComparison(node, constants) && node.loc) {
|
|
1310
|
+
lines.add(node.loc.start.line);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
return [...lines].sort((a, b) => a - b);
|
|
1314
|
+
};
|
|
1315
|
+
var hardcodedAuthorizationValueRule = {
|
|
1316
|
+
id: "hardcoded-authorization-value",
|
|
1317
|
+
description: "Detecta um header/cookie de requisi\xE7\xE3o comparado com um valor fixo no c\xF3digo para conceder acesso",
|
|
1318
|
+
check(filePath, content) {
|
|
1319
|
+
return findHardcodedAuthorizationLines(filePath, content).map((line) => ({
|
|
1320
|
+
ruleId: "hardcoded-authorization-value",
|
|
1321
|
+
message: "Autoriza\xE7\xE3o baseada em compara\xE7\xE3o de header/cookie com valor fixo no c\xF3digo-fonte",
|
|
1322
|
+
file: filePath,
|
|
1323
|
+
line,
|
|
1324
|
+
severity: "critical"
|
|
1325
|
+
}));
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// src/commands/hardcoded-authorization-value/hardcoded-authorization-value.command.ts
|
|
1330
|
+
var registerHardcodedAuthorizationValueCommand = (program) => {
|
|
1331
|
+
withScanOptions(
|
|
1332
|
+
program.command("hardcoded-authorization-value").description("Detecta compara\xE7\xE3o de headers/cookies de autoriza\xE7\xE3o com um valor fixo no c\xF3digo").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1333
|
+
).action(
|
|
1334
|
+
(path, options) => scanAndReport(path, [hardcodedAuthorizationValueRule], "Checking hardcoded authorization values...", options)
|
|
1335
|
+
);
|
|
1336
|
+
};
|
|
1337
|
+
|
|
889
1338
|
// src/commands/help/help.command.ts
|
|
890
1339
|
import Table2 from "cli-table3";
|
|
891
1340
|
var buildHelpTable = (commands) => {
|
|
@@ -1023,9 +1472,10 @@ var createFunctionStatementCountRule = (config) => ({
|
|
|
1023
1472
|
// src/rules/high-complexity.rule.ts
|
|
1024
1473
|
var highComplexityRule = createFunctionStatementCountRule({
|
|
1025
1474
|
id: "high-complexity",
|
|
1026
|
-
description: "Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)",
|
|
1475
|
+
description: "Detecta fun\xE7\xF5es com muitos condicionais/loops/tern\xE1rios (complexidade alta)",
|
|
1027
1476
|
statementTypes: /* @__PURE__ */ new Set([
|
|
1028
1477
|
"IfStatement",
|
|
1478
|
+
"ConditionalExpression",
|
|
1029
1479
|
"ForStatement",
|
|
1030
1480
|
"ForInStatement",
|
|
1031
1481
|
"ForOfStatement",
|
|
@@ -1035,12 +1485,14 @@ var highComplexityRule = createFunctionStatementCountRule({
|
|
|
1035
1485
|
"CatchClause"
|
|
1036
1486
|
]),
|
|
1037
1487
|
maxCount: 5,
|
|
1038
|
-
unitLabel: "condicionais/loops"
|
|
1488
|
+
unitLabel: "condicionais/loops/tern\xE1rios"
|
|
1039
1489
|
});
|
|
1040
1490
|
|
|
1041
1491
|
// src/commands/high-complexity/high-complexity.command.ts
|
|
1042
1492
|
var registerHighComplexityCommand = (program) => {
|
|
1043
|
-
|
|
1493
|
+
withScanOptions(
|
|
1494
|
+
program.command("high-complexity").description("Detecta fun\xE7\xF5es com muitos condicionais/loops (complexidade alta)").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1495
|
+
).action(
|
|
1044
1496
|
(path, options) => scanAndReport(path, [highComplexityRule], "Checking function complexity...", options)
|
|
1045
1497
|
);
|
|
1046
1498
|
};
|
|
@@ -1104,13 +1556,16 @@ var isMathRandomCall = (node) => {
|
|
|
1104
1556
|
return object?.type === "Identifier" && object.name === "Math" && property?.type === "Identifier" && property.name === "random";
|
|
1105
1557
|
};
|
|
1106
1558
|
var HASH_LIKE_NAME_PATTERN = /hash|md5|sha1/i;
|
|
1559
|
+
var memberPropertyName = (callee) => {
|
|
1560
|
+
const property = callee.property;
|
|
1561
|
+
return property?.type === "Identifier" ? property.name : void 0;
|
|
1562
|
+
};
|
|
1107
1563
|
var calleeName = (callee) => {
|
|
1108
1564
|
if (callee?.type === "Identifier") {
|
|
1109
1565
|
return callee.name;
|
|
1110
1566
|
}
|
|
1111
1567
|
if (callee?.type === "MemberExpression") {
|
|
1112
|
-
|
|
1113
|
-
return property?.type === "Identifier" ? property.name : void 0;
|
|
1568
|
+
return memberPropertyName(callee);
|
|
1114
1569
|
}
|
|
1115
1570
|
return void 0;
|
|
1116
1571
|
};
|
|
@@ -1190,7 +1645,9 @@ var insecureRandomTokenRule = {
|
|
|
1190
1645
|
|
|
1191
1646
|
// src/commands/insecure-random-token/insecure-random-token.command.ts
|
|
1192
1647
|
var registerInsecureRandomTokenCommand = (program) => {
|
|
1193
|
-
|
|
1648
|
+
withScanOptions(
|
|
1649
|
+
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", ".")
|
|
1650
|
+
).action(
|
|
1194
1651
|
(path, options) => scanAndReport(path, [insecureRandomTokenRule], "Checking insecure random tokens...", options)
|
|
1195
1652
|
);
|
|
1196
1653
|
};
|
|
@@ -1274,13 +1731,12 @@ var jwtDecodeWithoutVerifyRule = {
|
|
|
1274
1731
|
|
|
1275
1732
|
// src/commands/jwt-decode-without-verify/jwt-decode-without-verify.command.ts
|
|
1276
1733
|
var registerJwtDecodeWithoutVerifyCommand = (program) => {
|
|
1277
|
-
|
|
1278
|
-
(
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
)
|
|
1734
|
+
withScanOptions(
|
|
1735
|
+
program.command("jwt-decode-without-verify").description(
|
|
1736
|
+
"Detecta decodifica\xE7\xE3o manual de um token (base64 + JSON.parse) sem verificar a assinatura"
|
|
1737
|
+
).argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1738
|
+
).action(
|
|
1739
|
+
(path, options) => scanAndReport(path, [jwtDecodeWithoutVerifyRule], "Checking JWT decode without verification...", options)
|
|
1284
1740
|
);
|
|
1285
1741
|
};
|
|
1286
1742
|
|
|
@@ -1295,12 +1751,24 @@ var objectHasProperty = (object, propertyName) => {
|
|
|
1295
1751
|
return key?.type === "Identifier" && key.name === propertyName;
|
|
1296
1752
|
}) ?? false;
|
|
1297
1753
|
};
|
|
1298
|
-
var
|
|
1754
|
+
var getCallExpressionParts = (node) => {
|
|
1299
1755
|
const callee = node.type === "CallExpression" ? node.callee : void 0;
|
|
1756
|
+
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
1757
|
+
return { callee, args };
|
|
1758
|
+
};
|
|
1759
|
+
var getMemberExpressionParts = (callee) => {
|
|
1300
1760
|
const object = callee?.type === "MemberExpression" ? callee.object : void 0;
|
|
1301
1761
|
const property = callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
1762
|
+
return { object, property };
|
|
1763
|
+
};
|
|
1764
|
+
var getJwtSignCallParts = (node) => {
|
|
1765
|
+
const { callee, args } = getCallExpressionParts(node);
|
|
1766
|
+
const { object, property } = getMemberExpressionParts(callee);
|
|
1767
|
+
return { object, property, args };
|
|
1768
|
+
};
|
|
1769
|
+
var isJwtSignWithoutExpiration = (node) => {
|
|
1770
|
+
const { object, property, args } = getJwtSignCallParts(node);
|
|
1302
1771
|
const isJwtSignCall = object?.type === "Identifier" && object.name === "jwt" && property?.type === "Identifier" && property.name === "sign";
|
|
1303
|
-
const args = node.type === "CallExpression" ? node.arguments : void 0;
|
|
1304
1772
|
const payload = args?.[0];
|
|
1305
1773
|
const options = args?.[2];
|
|
1306
1774
|
const hasExpInPayload = objectHasProperty(payload, "exp");
|
|
@@ -1333,7 +1801,9 @@ var jwtNoExpirationRule = {
|
|
|
1333
1801
|
|
|
1334
1802
|
// src/commands/jwt-no-expiration/jwt-no-expiration.command.ts
|
|
1335
1803
|
var registerJwtNoExpirationCommand = (program) => {
|
|
1336
|
-
|
|
1804
|
+
withScanOptions(
|
|
1805
|
+
program.command("jwt-no-expiration").description("Detecta jwt.sign() sem expira\xE7\xE3o configurada").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1806
|
+
).action(
|
|
1337
1807
|
(path, options) => scanAndReport(path, [jwtNoExpirationRule], "Checking JWT expiration...", options)
|
|
1338
1808
|
);
|
|
1339
1809
|
};
|
|
@@ -1386,7 +1856,9 @@ var longFunctionRule = {
|
|
|
1386
1856
|
|
|
1387
1857
|
// src/commands/long-functions/long-functions.command.ts
|
|
1388
1858
|
var registerLongFunctionsCommand = (program) => {
|
|
1389
|
-
|
|
1859
|
+
withScanOptions(
|
|
1860
|
+
program.command("long-functions").description("Detecta fun\xE7\xF5es com mais de 30 linhas").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1861
|
+
).action(
|
|
1390
1862
|
(path, options) => scanAndReport(path, [longFunctionRule], "Checking function length...", options)
|
|
1391
1863
|
);
|
|
1392
1864
|
};
|
|
@@ -1418,7 +1890,9 @@ var noAnyRule = {
|
|
|
1418
1890
|
|
|
1419
1891
|
// src/commands/no-any/no-any.command.ts
|
|
1420
1892
|
var registerNoAnyCommand = (program) => {
|
|
1421
|
-
|
|
1893
|
+
withScanOptions(
|
|
1894
|
+
program.command("no-any").description('Detecta o uso do tipo "any" no TypeScript').argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1895
|
+
).action(
|
|
1422
1896
|
(path, options) => scanAndReport(path, [noAnyRule], "Checking any usage...", options)
|
|
1423
1897
|
);
|
|
1424
1898
|
};
|
|
@@ -1466,7 +1940,9 @@ var noEvalRule = {
|
|
|
1466
1940
|
|
|
1467
1941
|
// src/commands/no-eval/no-eval.command.ts
|
|
1468
1942
|
var registerNoEvalCommand = (program) => {
|
|
1469
|
-
|
|
1943
|
+
withScanOptions(
|
|
1944
|
+
program.command("no-eval").description("Detecta o uso de eval() ou new Function()").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
1945
|
+
).action(
|
|
1470
1946
|
(path, options) => scanAndReport(path, [noEvalRule], "Checking eval/new Function usage...", options)
|
|
1471
1947
|
);
|
|
1472
1948
|
};
|
|
@@ -1534,7 +2010,9 @@ var noHardcodedSecretRule = {
|
|
|
1534
2010
|
|
|
1535
2011
|
// src/commands/no-hardcoded-secret/no-hardcoded-secret.command.ts
|
|
1536
2012
|
var registerNoHardcodedSecretCommand = (program) => {
|
|
1537
|
-
|
|
2013
|
+
withScanOptions(
|
|
2014
|
+
program.command("no-hardcoded-secret").description("Detecta segredos/credenciais hardcoded no c\xF3digo-fonte").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
2015
|
+
).action(
|
|
1538
2016
|
(path, options) => scanAndReport(path, [noHardcodedSecretRule], "Checking hardcoded secrets...", options)
|
|
1539
2017
|
);
|
|
1540
2018
|
};
|
|
@@ -1563,12 +2041,26 @@ var isPermissiveCorsCall = (node) => {
|
|
|
1563
2041
|
const args = node.arguments;
|
|
1564
2042
|
return (args?.length ?? 0) === 0 || isWildcardOriginOption(args?.[0]);
|
|
1565
2043
|
};
|
|
2044
|
+
var callExpressionParts = (node) => {
|
|
2045
|
+
if (node.type !== "CallExpression") {
|
|
2046
|
+
return void 0;
|
|
2047
|
+
}
|
|
2048
|
+
return {
|
|
2049
|
+
callee: node.callee,
|
|
2050
|
+
args: node.arguments
|
|
2051
|
+
};
|
|
2052
|
+
};
|
|
2053
|
+
var memberCalleeProperty = (callee) => {
|
|
2054
|
+
if (callee?.type !== "MemberExpression") {
|
|
2055
|
+
return void 0;
|
|
2056
|
+
}
|
|
2057
|
+
return callee.property;
|
|
2058
|
+
};
|
|
1566
2059
|
var isWildcardOriginHeader = (node) => {
|
|
1567
|
-
const
|
|
1568
|
-
const property =
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
const headerValue = args?.[1];
|
|
2060
|
+
const parts = callExpressionParts(node);
|
|
2061
|
+
const property = memberCalleeProperty(parts?.callee);
|
|
2062
|
+
const headerName = parts?.args?.[0];
|
|
2063
|
+
const headerValue = parts?.args?.[1];
|
|
1572
2064
|
return property?.type === "Identifier" && HEADER_SETTER_METHODS.has(property.name) && headerName?.type === "StringLiteral" && headerName.value === "Access-Control-Allow-Origin" && headerValue?.type === "StringLiteral" && headerValue.value === "*";
|
|
1573
2065
|
};
|
|
1574
2066
|
var findPermissiveCorsLines = (filePath, content) => {
|
|
@@ -1597,7 +2089,9 @@ var permissiveCorsRule = {
|
|
|
1597
2089
|
|
|
1598
2090
|
// src/commands/permissive-cors/permissive-cors.command.ts
|
|
1599
2091
|
var registerPermissiveCorsCommand = (program) => {
|
|
1600
|
-
|
|
2092
|
+
withScanOptions(
|
|
2093
|
+
program.command("permissive-cors").description("Detecta CORS configurado para permitir qualquer origem").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
2094
|
+
).action(
|
|
1601
2095
|
(path, options) => scanAndReport(path, [permissiveCorsRule], "Checking permissive CORS...", options)
|
|
1602
2096
|
);
|
|
1603
2097
|
};
|
|
@@ -1642,7 +2136,11 @@ var publicEnvVarSecretRule = {
|
|
|
1642
2136
|
|
|
1643
2137
|
// src/commands/public-env-var-secret/public-env-var-secret.command.ts
|
|
1644
2138
|
var registerPublicEnvVarSecretCommand = (program) => {
|
|
1645
|
-
|
|
2139
|
+
withScanOptions(
|
|
2140
|
+
program.command("public-env-var-secret").description(
|
|
2141
|
+
"Detecta uma vari\xE1vel de ambiente p\xFAblica (NEXT_PUBLIC_/VITE_/REACT_APP_) com nome de segredo"
|
|
2142
|
+
).argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
2143
|
+
).action(
|
|
1646
2144
|
(path, options) => scanAndReport(path, [publicEnvVarSecretRule], "Checking public env var secrets...", options)
|
|
1647
2145
|
);
|
|
1648
2146
|
};
|
|
@@ -1716,13 +2214,26 @@ var awaitNoTryCatchRule = {
|
|
|
1716
2214
|
|
|
1717
2215
|
// src/rules/floating-promise.rule.ts
|
|
1718
2216
|
var PROMISE_STATIC_METHODS = /* @__PURE__ */ new Set(["all", "race", "allSettled", "any"]);
|
|
1719
|
-
var
|
|
2217
|
+
var functionDeclarationName = (node) => {
|
|
1720
2218
|
const declarationId = node.type === "FunctionDeclaration" ? node.id : void 0;
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2219
|
+
return declarationId?.type === "Identifier" ? declarationId.name : void 0;
|
|
2220
|
+
};
|
|
2221
|
+
var isAsyncFunctionValue = (init) => {
|
|
2222
|
+
return (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") && init.async;
|
|
2223
|
+
};
|
|
2224
|
+
var asyncVariableDeclaratorId = (node) => {
|
|
2225
|
+
return node.type === "VariableDeclarator" ? node.id : void 0;
|
|
2226
|
+
};
|
|
2227
|
+
var asyncVariableDeclaratorInit = (node) => {
|
|
2228
|
+
return node.type === "VariableDeclarator" ? node.init : void 0;
|
|
2229
|
+
};
|
|
2230
|
+
var asyncVariableDeclaratorName = (node) => {
|
|
2231
|
+
const id = asyncVariableDeclaratorId(node);
|
|
2232
|
+
const init = asyncVariableDeclaratorInit(node);
|
|
2233
|
+
return id?.type === "Identifier" && isAsyncFunctionValue(init) ? id.name : void 0;
|
|
2234
|
+
};
|
|
2235
|
+
var asyncFunctionName = (node) => {
|
|
2236
|
+
return functionDeclarationName(node) ?? asyncVariableDeclaratorName(node);
|
|
1726
2237
|
};
|
|
1727
2238
|
var collectAsyncFunctionNames = (sourceFile) => {
|
|
1728
2239
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -1734,16 +2245,20 @@ var collectAsyncFunctionNames = (sourceFile) => {
|
|
|
1734
2245
|
});
|
|
1735
2246
|
return names;
|
|
1736
2247
|
};
|
|
2248
|
+
var isKnownFunctionCall = (callee, asyncFunctionNames) => {
|
|
2249
|
+
return callee.type === "Identifier" && (callee.name === "fetch" || asyncFunctionNames.has(callee.name));
|
|
2250
|
+
};
|
|
2251
|
+
var isPromiseStaticMethodCall = (callee) => {
|
|
2252
|
+
const object = callee.type === "MemberExpression" ? callee.object : void 0;
|
|
2253
|
+
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
2254
|
+
return object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
|
|
2255
|
+
};
|
|
1737
2256
|
var isKnownPromiseReturningCall = (call, asyncFunctionNames) => {
|
|
1738
2257
|
const callee = call.callee;
|
|
1739
2258
|
if (!callee) {
|
|
1740
2259
|
return false;
|
|
1741
2260
|
}
|
|
1742
|
-
|
|
1743
|
-
const object = callee.type === "MemberExpression" ? callee.object : void 0;
|
|
1744
|
-
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
1745
|
-
const isPromiseStaticMethod = object?.type === "Identifier" && object.name === "Promise" && property?.type === "Identifier" && PROMISE_STATIC_METHODS.has(property.name);
|
|
1746
|
-
return isKnownFunction || isPromiseStaticMethod;
|
|
2261
|
+
return isKnownFunctionCall(callee, asyncFunctionNames) || isPromiseStaticMethodCall(callee);
|
|
1747
2262
|
};
|
|
1748
2263
|
var findFloatingPromiseLines = (filePath, content) => {
|
|
1749
2264
|
const lines = /* @__PURE__ */ new Set();
|
|
@@ -1773,7 +2288,7 @@ var floatingPromiseRule = {
|
|
|
1773
2288
|
};
|
|
1774
2289
|
|
|
1775
2290
|
// src/rules/promise-no-catch.rule.ts
|
|
1776
|
-
var
|
|
2291
|
+
var memberPropertyName2 = (member) => {
|
|
1777
2292
|
const property = member.property;
|
|
1778
2293
|
return property?.type === "Identifier" ? property.name : void 0;
|
|
1779
2294
|
};
|
|
@@ -1786,7 +2301,11 @@ var isThenCall = (node) => {
|
|
|
1786
2301
|
}
|
|
1787
2302
|
const callee = node.callee;
|
|
1788
2303
|
const args = node.arguments;
|
|
1789
|
-
return callee?.type === "MemberExpression" &&
|
|
2304
|
+
return callee?.type === "MemberExpression" && memberPropertyName2(callee) === "then" && (args?.length ?? 0) < 2;
|
|
2305
|
+
};
|
|
2306
|
+
var chainMethodName = (currentCall, member, nextCall) => {
|
|
2307
|
+
const isChainMember = member !== void 0 && member.type === "MemberExpression" && member.object === currentCall && isCallOf(nextCall, member);
|
|
2308
|
+
return isChainMember ? memberPropertyName2(member) : void 0;
|
|
1790
2309
|
};
|
|
1791
2310
|
var chainReachesCatch = (thenCall, ancestors) => {
|
|
1792
2311
|
let currentCall = thenCall;
|
|
@@ -1794,8 +2313,7 @@ var chainReachesCatch = (thenCall, ancestors) => {
|
|
|
1794
2313
|
while (i < ancestors.length) {
|
|
1795
2314
|
const member = ancestors.at(i);
|
|
1796
2315
|
const nextCall = ancestors.at(i + 1);
|
|
1797
|
-
const
|
|
1798
|
-
const methodName = isChainMember ? memberPropertyName(member) : void 0;
|
|
2316
|
+
const methodName = chainMethodName(currentCall, member, nextCall);
|
|
1799
2317
|
const continuesChain = methodName === "then" || methodName === "finally";
|
|
1800
2318
|
if (methodName === "catch") {
|
|
1801
2319
|
return true;
|
|
@@ -2037,10 +2555,10 @@ var tooManyForLoopsRule = createFunctionStatementCountRule({
|
|
|
2037
2555
|
// src/rules/too-many-ifs.rule.ts
|
|
2038
2556
|
var tooManyIfsRule = createFunctionStatementCountRule({
|
|
2039
2557
|
id: "too-many-ifs",
|
|
2040
|
-
description: 'Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")',
|
|
2041
|
-
statementTypes: /* @__PURE__ */ new Set(["IfStatement"]),
|
|
2558
|
+
description: 'Detecta fun\xE7\xF5es com muitos "if" ou tern\xE1rios (incluindo "else if")',
|
|
2559
|
+
statementTypes: /* @__PURE__ */ new Set(["IfStatement", "ConditionalExpression"]),
|
|
2042
2560
|
maxCount: 2,
|
|
2043
|
-
unitLabel: "declara\xE7\xF5es if (incluindo else if)"
|
|
2561
|
+
unitLabel: "declara\xE7\xF5es if e express\xF5es tern\xE1rias (incluindo else if)"
|
|
2044
2562
|
});
|
|
2045
2563
|
|
|
2046
2564
|
// src/rules/too-many-switch-cases.rule.ts
|
|
@@ -2143,14 +2661,61 @@ var unsafeSqlRule = {
|
|
|
2143
2661
|
}
|
|
2144
2662
|
};
|
|
2145
2663
|
|
|
2664
|
+
// src/rules/weak-cipher-mode.rule.ts
|
|
2665
|
+
var CIPHER_METHOD_NAMES = /* @__PURE__ */ new Set(["createCipheriv", "createDecipheriv"]);
|
|
2666
|
+
var WEAK_MODE_PATTERN = /-(cbc|ecb)$/i;
|
|
2667
|
+
var isWeakCipherModeCall = (node) => {
|
|
2668
|
+
const callee = node.callee;
|
|
2669
|
+
const property = callee?.property;
|
|
2670
|
+
const args = node.arguments;
|
|
2671
|
+
const algorithm = args?.[0];
|
|
2672
|
+
return node.type === "CallExpression" && callee?.type === "MemberExpression" && property?.type === "Identifier" && CIPHER_METHOD_NAMES.has(property.name) && algorithm?.type === "StringLiteral" && WEAK_MODE_PATTERN.test(algorithm.value);
|
|
2673
|
+
};
|
|
2674
|
+
var findWeakCipherModeLines = (filePath, content) => {
|
|
2675
|
+
const lines = /* @__PURE__ */ new Set();
|
|
2676
|
+
const sourceFile = parseSourceFile(filePath, content);
|
|
2677
|
+
visitSourceNodes(sourceFile, (node) => {
|
|
2678
|
+
if (isWeakCipherModeCall(node) && node.loc) {
|
|
2679
|
+
lines.add(node.loc.start.line);
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
return [...lines].sort((a, b) => a - b);
|
|
2683
|
+
};
|
|
2684
|
+
var weakCipherModeRule = {
|
|
2685
|
+
id: "weak-cipher-mode",
|
|
2686
|
+
description: "Detecta cifragem em modo n\xE3o autenticado (CBC/ECB) sem AEAD/HMAC associado",
|
|
2687
|
+
check(filePath, content) {
|
|
2688
|
+
return findWeakCipherModeLines(filePath, content).map((line) => ({
|
|
2689
|
+
ruleId: "weak-cipher-mode",
|
|
2690
|
+
message: "Modo de cifra sem autentica\xE7\xE3o (CBC/ECB) \u2014 considere um modo AEAD como GCM",
|
|
2691
|
+
file: filePath,
|
|
2692
|
+
line,
|
|
2693
|
+
severity: "high"
|
|
2694
|
+
}));
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2146
2698
|
// src/rules/weak-hash-algorithm.rule.ts
|
|
2147
2699
|
var WEAK_ALGORITHMS = /^(md5|sha1)$/i;
|
|
2700
|
+
var getCallExpressionCallee = (node) => {
|
|
2701
|
+
return node.type === "CallExpression" ? node.callee : void 0;
|
|
2702
|
+
};
|
|
2703
|
+
var getCallExpressionArgs = (node) => {
|
|
2704
|
+
return node.type === "CallExpression" ? node.arguments : void 0;
|
|
2705
|
+
};
|
|
2706
|
+
var getCreateHashProperty = (callee) => {
|
|
2707
|
+
return callee?.type === "MemberExpression" ? callee.property : void 0;
|
|
2708
|
+
};
|
|
2709
|
+
var isCreateHashProperty = (property) => {
|
|
2710
|
+
return property?.type === "Identifier" && property.name === "createHash";
|
|
2711
|
+
};
|
|
2712
|
+
var isWeakAlgorithmLiteral = (algorithm) => {
|
|
2713
|
+
return algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
|
|
2714
|
+
};
|
|
2148
2715
|
var isWeakCreateHashCall = (node) => {
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
const algorithm = args?.[0];
|
|
2153
|
-
return property?.type === "Identifier" && property.name === "createHash" && algorithm?.type === "StringLiteral" && WEAK_ALGORITHMS.test(algorithm.value);
|
|
2716
|
+
const property = getCreateHashProperty(getCallExpressionCallee(node));
|
|
2717
|
+
const algorithm = getCallExpressionArgs(node)?.[0];
|
|
2718
|
+
return isCreateHashProperty(property) && isWeakAlgorithmLiteral(algorithm);
|
|
2154
2719
|
};
|
|
2155
2720
|
var findWeakHashLines = (filePath, content) => {
|
|
2156
2721
|
const lines = /* @__PURE__ */ new Set();
|
|
@@ -2352,7 +2917,9 @@ var allRules = [
|
|
|
2352
2917
|
jwtDecodeWithoutVerifyRule,
|
|
2353
2918
|
xxeUnsafeXmlParsingRule,
|
|
2354
2919
|
sensitiveDataInLogsRule,
|
|
2355
|
-
publicEnvVarSecretRule
|
|
2920
|
+
publicEnvVarSecretRule,
|
|
2921
|
+
weakCipherModeRule,
|
|
2922
|
+
hardcodedAuthorizationValueRule
|
|
2356
2923
|
];
|
|
2357
2924
|
|
|
2358
2925
|
// src/commands/rules/rules.command.ts
|
|
@@ -2382,70 +2949,93 @@ var parseLocalSemgrepConfig = (value) => {
|
|
|
2382
2949
|
return resolve2(value);
|
|
2383
2950
|
};
|
|
2384
2951
|
var registerScanCommand = (program) => {
|
|
2385
|
-
|
|
2952
|
+
withScanOptions(
|
|
2953
|
+
program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig).option(
|
|
2954
|
+
"--no-deps",
|
|
2955
|
+
"n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev) neste scan \u2014 permite rodar offline"
|
|
2956
|
+
)
|
|
2957
|
+
).action(
|
|
2386
2958
|
(path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
|
|
2387
2959
|
);
|
|
2388
2960
|
};
|
|
2389
2961
|
|
|
2390
2962
|
// src/commands/security-lint/security-lint.command.ts
|
|
2391
2963
|
var registerSecurityLintCommand = (program) => {
|
|
2392
|
-
|
|
2964
|
+
withScanOptions(
|
|
2965
|
+
program.command("security-lint").description("Detecta padr\xF5es de seguran\xE7a gen\xE9ricos via eslint-plugin-security").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
2966
|
+
).action(
|
|
2393
2967
|
(path, options) => scanAndReport(path, [securityLintRule], "Checking generic security patterns...", options)
|
|
2394
2968
|
);
|
|
2395
2969
|
};
|
|
2396
2970
|
|
|
2397
2971
|
// src/commands/sensitive-data-in-logs/sensitive-data-in-logs.command.ts
|
|
2398
2972
|
var registerSensitiveDataInLogsCommand = (program) => {
|
|
2399
|
-
|
|
2973
|
+
withScanOptions(
|
|
2974
|
+
program.command("sensitive-data-in-logs").description("Detecta senhas/segredos/tokens sendo passados para chamadas de log").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
2975
|
+
).action(
|
|
2400
2976
|
(path, options) => scanAndReport(path, [sensitiveDataInLogsRule], "Checking sensitive data in logs...", options)
|
|
2401
2977
|
);
|
|
2402
2978
|
};
|
|
2403
2979
|
|
|
2404
2980
|
// src/commands/tls-validation-disabled/tls-validation-disabled.command.ts
|
|
2405
2981
|
var registerTlsValidationDisabledCommand = (program) => {
|
|
2406
|
-
|
|
2982
|
+
withScanOptions(
|
|
2983
|
+
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", ".")
|
|
2984
|
+
).action(
|
|
2407
2985
|
(path, options) => scanAndReport(path, [tlsValidationDisabledRule], "Checking TLS validation...", options)
|
|
2408
2986
|
);
|
|
2409
2987
|
};
|
|
2410
2988
|
|
|
2411
2989
|
// src/commands/too-many-for-loops/too-many-for-loops.command.ts
|
|
2412
2990
|
var registerTooManyForLoopsCommand = (program) => {
|
|
2413
|
-
|
|
2991
|
+
withScanOptions(
|
|
2992
|
+
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", ".")
|
|
2993
|
+
).action(
|
|
2414
2994
|
(path, options) => scanAndReport(path, [tooManyForLoopsRule], "Checking for-loop count...", options)
|
|
2415
2995
|
);
|
|
2416
2996
|
};
|
|
2417
2997
|
|
|
2418
2998
|
// src/commands/too-many-ifs/too-many-ifs.command.ts
|
|
2419
2999
|
var registerTooManyIfsCommand = (program) => {
|
|
2420
|
-
|
|
3000
|
+
withScanOptions(
|
|
3001
|
+
program.command("too-many-ifs").description('Detecta fun\xE7\xF5es com muitos "if" (incluindo "else if")').argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3002
|
+
).action(
|
|
2421
3003
|
(path, options) => scanAndReport(path, [tooManyIfsRule], "Checking if count...", options)
|
|
2422
3004
|
);
|
|
2423
3005
|
};
|
|
2424
3006
|
|
|
2425
3007
|
// src/commands/too-many-switch-cases/too-many-switch-cases.command.ts
|
|
2426
3008
|
var registerTooManySwitchCasesCommand = (program) => {
|
|
2427
|
-
|
|
3009
|
+
withScanOptions(
|
|
3010
|
+
program.command("too-many-switch-cases").description('Detecta "switch" com muitos "case" (considere um mapa/lookup)').argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3011
|
+
).action(
|
|
2428
3012
|
(path, options) => scanAndReport(path, [tooManySwitchCasesRule], "Checking switch cases...", options)
|
|
2429
3013
|
);
|
|
2430
3014
|
};
|
|
2431
3015
|
|
|
2432
3016
|
// src/commands/too-many-try-catch/too-many-try-catch.command.ts
|
|
2433
3017
|
var registerTooManyTryCatchCommand = (program) => {
|
|
2434
|
-
|
|
3018
|
+
withScanOptions(
|
|
3019
|
+
program.command("too-many-try-catch").description('Detecta fun\xE7\xF5es com muitos blocos "try/catch"').argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3020
|
+
).action(
|
|
2435
3021
|
(path, options) => scanAndReport(path, [tooManyTryCatchRule], "Checking try/catch count...", options)
|
|
2436
3022
|
);
|
|
2437
3023
|
};
|
|
2438
3024
|
|
|
2439
3025
|
// src/commands/too-many-while-loops/too-many-while-loops.command.ts
|
|
2440
3026
|
var registerTooManyWhileLoopsCommand = (program) => {
|
|
2441
|
-
|
|
3027
|
+
withScanOptions(
|
|
3028
|
+
program.command("too-many-while-loops").description('Detecta fun\xE7\xF5es com muitos loops "while"/"do-while"').argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3029
|
+
).action(
|
|
2442
3030
|
(path, options) => scanAndReport(path, [tooManyWhileLoopsRule], "Checking while-loop count...", options)
|
|
2443
3031
|
);
|
|
2444
3032
|
};
|
|
2445
3033
|
|
|
2446
3034
|
// src/commands/unhandled-promises/unhandled-promises.command.ts
|
|
2447
3035
|
var registerUnhandledPromisesCommand = (program) => {
|
|
2448
|
-
|
|
3036
|
+
withScanOptions(
|
|
3037
|
+
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", ".")
|
|
3038
|
+
).action(
|
|
2449
3039
|
(path, options) => scanAndReport(
|
|
2450
3040
|
path,
|
|
2451
3041
|
[promiseNoCatchRule, awaitNoTryCatchRule, floatingPromiseRule],
|
|
@@ -2457,37 +3047,168 @@ var registerUnhandledPromisesCommand = (program) => {
|
|
|
2457
3047
|
|
|
2458
3048
|
// src/commands/unsafe-sql/unsafe-sql.command.ts
|
|
2459
3049
|
var registerUnsafeSqlCommand = (program) => {
|
|
2460
|
-
|
|
3050
|
+
withScanOptions(
|
|
3051
|
+
program.command("unsafe-sql").description("Detecta concatena\xE7\xE3o insegura de SQL (risco de SQL injection)").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3052
|
+
).action(
|
|
2461
3053
|
(path, options) => scanAndReport(path, [unsafeSqlRule], "Checking unsafe SQL...", options)
|
|
2462
3054
|
);
|
|
2463
3055
|
};
|
|
2464
3056
|
|
|
3057
|
+
// src/engines/engine-metadata.ts
|
|
3058
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3059
|
+
import { createRequire as createRequire4 } from "module";
|
|
3060
|
+
var require5 = createRequire4(import.meta.url);
|
|
3061
|
+
var packageForPlatform2 = (platform, architecture) => {
|
|
3062
|
+
if (platform === "linux" && architecture === "x64") {
|
|
3063
|
+
return "codesentry-semgrep-linux-x64";
|
|
3064
|
+
}
|
|
3065
|
+
if (platform === "win32" && architecture === "x64") {
|
|
3066
|
+
return "codesentry-semgrep-win32-x64";
|
|
3067
|
+
}
|
|
3068
|
+
return void 0;
|
|
3069
|
+
};
|
|
3070
|
+
var unsupportedRuntimeMessage2 = (platform, architecture) => `O runtime Semgrep embutido n\xE3o est\xE1 dispon\xEDvel para ${platform}-${architecture}. O CodeSentry oferece suporte a Linux x64 e Windows x64.`;
|
|
3071
|
+
var readJsonFile = (path) => {
|
|
3072
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
3073
|
+
};
|
|
3074
|
+
var nonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
3075
|
+
var errorReason2 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
3076
|
+
var validRuntimeLock = (lock) => lock.schemaVersion === 1 && nonEmptyString(lock.semgrepVersion) && nonEmptyString(lock.pythonVersion) && /^[a-f0-9]{64}$/i.test(lock.runtimeSha256);
|
|
3077
|
+
var validRulesetLock = (lock) => lock.schemaVersion === 1 && nonEmptyString(lock.packageVersion) && nonEmptyString(lock.ruleset) && nonEmptyString(lock.source) && /^[a-f0-9]{64}$/i.test(lock.sha256) && nonEmptyString(lock.capturedAt) && !Number.isNaN(Date.parse(lock.capturedAt)) && (lock.upstreamRevision === void 0 || nonEmptyString(lock.upstreamRevision));
|
|
3078
|
+
var loadBundledSemgrepRuntimeMetadata = (platform = process.platform, architecture = process.arch, resolveLockPath = (packageName) => require5.resolve(`${packageName}/runtime.lock.json`)) => {
|
|
3079
|
+
const packageName = packageForPlatform2(platform, architecture);
|
|
3080
|
+
if (!packageName) {
|
|
3081
|
+
throw new Error(unsupportedRuntimeMessage2(platform, architecture));
|
|
3082
|
+
}
|
|
3083
|
+
try {
|
|
3084
|
+
const lock = readJsonFile(resolveLockPath(packageName));
|
|
3085
|
+
if (!validRuntimeLock(lock)) {
|
|
3086
|
+
throw new Error("manifesto de vers\xE3o inv\xE1lido");
|
|
3087
|
+
}
|
|
3088
|
+
return {
|
|
3089
|
+
packageName,
|
|
3090
|
+
semgrepVersion: lock.semgrepVersion,
|
|
3091
|
+
pythonVersion: lock.pythonVersion,
|
|
3092
|
+
runtimeSha256: lock.runtimeSha256
|
|
3093
|
+
};
|
|
3094
|
+
} catch (error) {
|
|
3095
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do runtime Semgrep embutido: ${errorReason2(error)}`, {
|
|
3096
|
+
cause: error
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
var loadBundledOwaspRulesetMetadata = (resolveLockPath = () => require5.resolve("codesentry-semgrep-rules/rules/ruleset.lock.json")) => {
|
|
3101
|
+
try {
|
|
3102
|
+
const lock = readJsonFile(resolveLockPath());
|
|
3103
|
+
if (!validRulesetLock(lock)) {
|
|
3104
|
+
throw new Error("manifesto de vers\xE3o inv\xE1lido");
|
|
3105
|
+
}
|
|
3106
|
+
return {
|
|
3107
|
+
packageVersion: lock.packageVersion,
|
|
3108
|
+
ruleset: lock.ruleset,
|
|
3109
|
+
source: lock.source,
|
|
3110
|
+
sha256: lock.sha256,
|
|
3111
|
+
capturedAt: lock.capturedAt,
|
|
3112
|
+
...lock.upstreamRevision ? { upstreamRevision: lock.upstreamRevision } : {}
|
|
3113
|
+
};
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
throw new Error(`N\xE3o foi poss\xEDvel carregar os metadados do ruleset OWASP embutido: ${errorReason2(error)}`, {
|
|
3116
|
+
cause: error
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
var loadEngineMetadata = () => ({
|
|
3121
|
+
semgrep: loadBundledSemgrepRuntimeMetadata(),
|
|
3122
|
+
owaspRuleset: loadBundledOwaspRulesetMetadata()
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
// src/commands/version/version.command.ts
|
|
3126
|
+
var buildVersionReport = (codesentryVersion, engines) => ({
|
|
3127
|
+
codesentryVersion,
|
|
3128
|
+
engines
|
|
3129
|
+
});
|
|
3130
|
+
var formatEngineVersions = (report) => {
|
|
3131
|
+
const { semgrep, owaspRuleset } = report.engines;
|
|
3132
|
+
return [
|
|
3133
|
+
`CodeSentry: ${report.codesentryVersion}`,
|
|
3134
|
+
"",
|
|
3135
|
+
"Runtime Semgrep CE",
|
|
3136
|
+
` Vers\xE3o: ${semgrep.semgrepVersion}`,
|
|
3137
|
+
` Python: ${semgrep.pythonVersion}`,
|
|
3138
|
+
` Pacote: ${semgrep.packageName}`,
|
|
3139
|
+
` SHA-256: ${semgrep.runtimeSha256}`,
|
|
3140
|
+
"",
|
|
3141
|
+
"Ruleset OWASP",
|
|
3142
|
+
` Snapshot: ${owaspRuleset.ruleset}`,
|
|
3143
|
+
` Vers\xE3o do pacote: ${owaspRuleset.packageVersion}`,
|
|
3144
|
+
` Origem: ${owaspRuleset.source}`,
|
|
3145
|
+
` Capturado em: ${owaspRuleset.capturedAt}`,
|
|
3146
|
+
...owaspRuleset.upstreamRevision ? [` Revis\xE3o upstream: ${owaspRuleset.upstreamRevision}`] : [],
|
|
3147
|
+
` SHA-256: ${owaspRuleset.sha256}`
|
|
3148
|
+
].join("\n");
|
|
3149
|
+
};
|
|
3150
|
+
var errorReason3 = (error) => error instanceof Error ? error.message : "erro desconhecido";
|
|
3151
|
+
var formatVersionOutput = (options, report) => options.json ? JSON.stringify(report, null, 2) : formatEngineVersions(report);
|
|
3152
|
+
var registerVersionCommand = (program, readPackageVersion2, readEngineMetadata = loadEngineMetadata) => {
|
|
3153
|
+
program.command("version").description("Exibe a vers\xE3o do CodeSentry e dos motores embutidos").option("--engines", "inclui vers\xF5es e proveni\xEAncia do Semgrep e ruleset OWASP").option("--json", "emite o relat\xF3rio de vers\xF5es em JSON").action((options) => {
|
|
3154
|
+
const codesentryVersion = readPackageVersion2();
|
|
3155
|
+
if (!options.engines && !options.json) {
|
|
3156
|
+
console.log(`CodeSentry: ${codesentryVersion}`);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
try {
|
|
3160
|
+
const report = buildVersionReport(codesentryVersion, readEngineMetadata());
|
|
3161
|
+
console.log(formatVersionOutput(options, report));
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
process.exitCode = 1;
|
|
3164
|
+
console.error(`N\xE3o foi poss\xEDvel auditar as vers\xF5es dos motores: ${errorReason3(error)}`);
|
|
3165
|
+
}
|
|
3166
|
+
});
|
|
3167
|
+
};
|
|
3168
|
+
|
|
3169
|
+
// src/commands/weak-cipher-mode/weak-cipher-mode.command.ts
|
|
3170
|
+
var registerWeakCipherModeCommand = (program) => {
|
|
3171
|
+
withScanOptions(
|
|
3172
|
+
program.command("weak-cipher-mode").description("Detecta o uso de modos de cifra fracos (CBC, ECB) em createCipheriv/createDecipheriv").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3173
|
+
).action(
|
|
3174
|
+
(path, options) => scanAndReport(path, [weakCipherModeRule], "Checking weak cipher modes...", options)
|
|
3175
|
+
);
|
|
3176
|
+
};
|
|
3177
|
+
|
|
2465
3178
|
// src/commands/weak-hash-algorithm/weak-hash-algorithm.command.ts
|
|
2466
3179
|
var registerWeakHashAlgorithmCommand = (program) => {
|
|
2467
|
-
|
|
3180
|
+
withScanOptions(
|
|
3181
|
+
program.command("weak-hash-algorithm").description("Detecta o uso de algoritmos de hash fracos (MD5, SHA-1)").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3182
|
+
).action(
|
|
2468
3183
|
(path, options) => scanAndReport(path, [weakHashAlgorithmRule], "Checking weak hash algorithms...", options)
|
|
2469
3184
|
);
|
|
2470
3185
|
};
|
|
2471
3186
|
|
|
2472
3187
|
// src/commands/weak-secret-fallback/weak-secret-fallback.command.ts
|
|
2473
3188
|
var registerWeakSecretFallbackCommand = (program) => {
|
|
2474
|
-
|
|
2475
|
-
"
|
|
2476
|
-
|
|
3189
|
+
withScanOptions(
|
|
3190
|
+
program.command("weak-secret-fallback").description(
|
|
3191
|
+
"Detecta uma vari\xE1vel de ambiente de segredo/chave com um valor hardcoded como fallback (|| ou ??)"
|
|
3192
|
+
).argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3193
|
+
).action(
|
|
2477
3194
|
(path, options) => scanAndReport(path, [weakSecretFallbackRule], "Checking weak secret fallbacks...", options)
|
|
2478
3195
|
);
|
|
2479
3196
|
};
|
|
2480
3197
|
|
|
2481
3198
|
// src/commands/xss/xss.command.ts
|
|
2482
3199
|
var registerXssCommand = (program) => {
|
|
2483
|
-
|
|
3200
|
+
withScanOptions(
|
|
3201
|
+
program.command("xss").description("Detecta sinks perigosos de XSS (innerHTML, document.write, etc.)").argument("[path]", "diret\xF3rio a ser analisado", ".")
|
|
3202
|
+
).action(
|
|
2484
3203
|
(path, options) => scanAndReport(path, [xssRule], "Checking XSS sinks...", options)
|
|
2485
3204
|
);
|
|
2486
3205
|
};
|
|
2487
3206
|
|
|
2488
3207
|
// src/commands/xxe-unsafe-xml-parsing/xxe-unsafe-xml-parsing.command.ts
|
|
2489
3208
|
var registerXxeUnsafeXmlParsingCommand = (program) => {
|
|
2490
|
-
|
|
3209
|
+
withScanOptions(
|
|
3210
|
+
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", ".")
|
|
3211
|
+
).action(
|
|
2491
3212
|
(path, options) => scanAndReport(path, [xxeUnsafeXmlParsingRule], "Checking unsafe XML parsing...", options)
|
|
2492
3213
|
);
|
|
2493
3214
|
};
|
|
@@ -2537,17 +3258,20 @@ var registerSecurityCommands = (program) => {
|
|
|
2537
3258
|
registerXxeUnsafeXmlParsingCommand(program);
|
|
2538
3259
|
registerSensitiveDataInLogsCommand(program);
|
|
2539
3260
|
registerPublicEnvVarSecretCommand(program);
|
|
3261
|
+
registerWeakCipherModeCommand(program);
|
|
3262
|
+
registerHardcodedAuthorizationValueCommand(program);
|
|
2540
3263
|
};
|
|
2541
|
-
var
|
|
3264
|
+
var require6 = createRequire5(import.meta.url);
|
|
2542
3265
|
var readPackageVersion = () => {
|
|
2543
|
-
const packageJsonPath =
|
|
2544
|
-
const { version } = JSON.parse(
|
|
3266
|
+
const packageJsonPath = require6.resolve("../package.json");
|
|
3267
|
+
const { version } = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
2545
3268
|
return version;
|
|
2546
3269
|
};
|
|
2547
3270
|
var createCli = () => {
|
|
2548
3271
|
printBanner();
|
|
2549
3272
|
const program = new Command().name("codesentry").description("CLI de verifica\xE7\xE3o de vulnerabilidades e qualidade de c\xF3digo").version(readPackageVersion()).helpCommand(false);
|
|
2550
3273
|
registerInitCommand(program);
|
|
3274
|
+
registerVersionCommand(program, readPackageVersion);
|
|
2551
3275
|
registerAnalysisCommands(program);
|
|
2552
3276
|
registerQualityCommands(program);
|
|
2553
3277
|
registerSecurityCommands(program);
|