agent-master-devsecops 3.9.0 → 5.1.0
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 +205 -371
- package/agents/AGENT_DEVSECOPS.md +32 -417
- package/agents/AGENT_INFRA.md +31 -0
- package/agents/AGENT_ORCHESTRATOR.md +39 -264
- package/agents/AGENT_QA.md +58 -186
- package/agents/AGENT_SCRUM.md +23 -190
- package/agents/AGENT_SRE.md +22 -173
- package/agents/AGENT_UIUX.md +26 -162
- package/docs/ARCHITECTURE.md +152 -148
- package/docs/CALCULATIONS.md +17 -17
- package/docs/CHANGELOG.md +112 -49
- package/docs/ENGINEERING_RULES.md +56 -252
- package/docs/IDEAS.md +17 -17
- package/docs/LOCAL_BACKLOG.md +20 -0
- package/docs/STATE.md +111 -43
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -58
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -58
- package/package.json +59 -47
- package/scripts/auditors/a11y_auditor.js +462 -0
- package/scripts/auditors/code_quality_auditor.js +444 -0
- package/scripts/auditors/contract_test_validator.js +396 -0
- package/scripts/auditors/eval_harness.js +213 -0
- package/scripts/auditors/git_hygiene_linter.js +391 -0
- package/scripts/auditors/pr_reviewer.js +627 -0
- package/scripts/auditors/sast_scanner.js +442 -0
- package/scripts/auditors/sca_dependency_auditor.js +673 -0
- package/scripts/{bootstrap_cli.js → bin/bootstrap_cli.js} +597 -468
- package/scripts/bin/domain_idea_miner.js +658 -0
- package/scripts/bin/smart_onboarding.js +905 -0
- package/scripts/{web_inspector.js → bin/web_inspector.js} +578 -355
- package/scripts/core/architecture_advisor.js +378 -0
- package/scripts/core/ast_skeletonizer.js +413 -0
- package/scripts/core/autonomous_remediation_engine.js +305 -0
- package/scripts/{circuit_breaker.js → core/circuit_breaker.js} +546 -330
- package/scripts/core/context_compressor.js +237 -0
- package/scripts/core/fast_feature_scaffold.js +840 -0
- package/scripts/core/hot_state_cache.js +238 -0
- package/scripts/core/knowledge_gateway.js +888 -0
- package/scripts/{magic_prompt_generator.js → core/magic_prompt_generator.js} +250 -205
- package/scripts/{multi_agent_orchestrator.js → core/multi_agent_orchestrator.js} +421 -258
- package/scripts/{otel_tracer.js → core/otel_tracer.js} +335 -228
- package/scripts/{perf_benchmark.js → core/perf_benchmark.js} +212 -153
- package/scripts/core/sarif_engine.js +299 -0
- package/scripts/{sre_remediation_engine.js → core/sre_remediation_engine.js} +236 -174
- package/scripts/{universal_packager.js → core/universal_packager.js} +357 -313
- package/scripts/integrations/atlassian_client.js +600 -0
- package/scripts/integrations/atlassian_sync_action.js +576 -0
- package/scripts/integrations/confluence_sync.js +684 -0
- package/scripts/integrations/credentials_guard.js +500 -0
- package/scripts/integrations/jira_manager.js +722 -0
- package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
- package/scripts/mcp/handlers/core_handlers.js +419 -0
- package/scripts/mcp/handlers/qa_handlers.js +502 -0
- package/scripts/mcp/handlers/rules_handlers.js +379 -0
- package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
- package/scripts/mcp/handlers/sre_handlers.js +443 -0
- package/scripts/mcp/resources_registry.js +453 -0
- package/scripts/mcp/server.js +408 -0
- package/scripts/mcp/tool_handlers.js +444 -0
- package/scripts/mcp/tools_registry.js +1123 -0
- package/scripts/mcp_server.js +236 -1884
- package/.cursorignore +0 -23
- package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +0 -53
- package/scripts/a11y_auditor.js +0 -187
- package/scripts/ast_skeletonizer.js +0 -220
- package/scripts/atlassian_client.js +0 -238
- package/scripts/atlassian_sync_action.js +0 -269
- package/scripts/code_quality_auditor.js +0 -142
- package/scripts/confluence_sync.js +0 -337
- package/scripts/context_compressor.js +0 -139
- package/scripts/contract_test_validator.js +0 -166
- package/scripts/credentials_guard.js +0 -216
- package/scripts/domain_idea_miner.js +0 -364
- package/scripts/eval_harness.js +0 -100
- package/scripts/fast_feature_scaffold.js +0 -338
- package/scripts/git_hygiene_linter.js +0 -186
- package/scripts/hot_state_cache.js +0 -95
- package/scripts/jira_manager.js +0 -408
- package/scripts/pr_reviewer.js +0 -373
- package/scripts/sast_scanner.js +0 -180
- package/scripts/sca_dependency_auditor.js +0 -167
- package/scripts/smart_onboarding.js +0 -610
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/auditors/sast_scanner.js
|
|
3
|
+
* SAST & Secret Detection Engine — Shift-Left DevSecOps (v5.1 Enterprise)
|
|
4
|
+
* Scanner estático nativo de vulnerabilidades OWASP Top 10, CWEs e segredos em texto plano.
|
|
5
|
+
* Em estrita conformidade com a Regra 5, Regra 12 e NIST SSDF SP 800-218.
|
|
6
|
+
* Suporta modo híbrido industrial (Semgrep CLI) com motor heurístico determinístico embutido.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { spawnSync } = require('node:child_process');
|
|
13
|
+
const { createSarifReport, parseSarifReport } = require('../core/sarif_engine.js');
|
|
14
|
+
|
|
15
|
+
const SECURITY_RULES = Object.freeze([
|
|
16
|
+
Object.freeze({
|
|
17
|
+
id: 'DEVSEC-CWE-78',
|
|
18
|
+
cwe: 'CWE-78',
|
|
19
|
+
name: 'Command Injection',
|
|
20
|
+
severity: 'CRITICAL',
|
|
21
|
+
pattern: /(?:child_process\s*\.\s*exec\s*\(|\bexecSync\s*\(|\bpopen\s*\(|(?<![\w$.])exec\s*\(|`[^`]*\$\{.*?\}[^`]*`\s*(?:\|\||&&)?\s*exec)/i,
|
|
22
|
+
message: 'Execução de comando via shell detectada. Utilize execFile ou spawn com argumentos em array sanitizados.',
|
|
23
|
+
remediation: 'Substitua child_process.exec por child_process.execFile ou spawn sem invocação de shell intermediário.'
|
|
24
|
+
}),
|
|
25
|
+
Object.freeze({
|
|
26
|
+
id: 'DEVSEC-CWE-89',
|
|
27
|
+
cwe: 'CWE-89',
|
|
28
|
+
name: 'SQL Injection',
|
|
29
|
+
severity: 'CRITICAL',
|
|
30
|
+
pattern: /(?<!\.)\b(?:SELECT|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_.]+\s+SET|DELETE\s+FROM|DROP\s+TABLE)\b\s+[\s\S]*?['"`]\s*\+\s*[a-zA-Z0-9_$]|\b(?:SELECT\b[\s\S]*?\bFROM\b|INSERT\s+INTO\b|DELETE\s+FROM\b)[\s\S]*?\$\{.*?\}|\$\{.*?\}[\s\S]*?\b(?:WHERE|VALUES)\b|(?<!\.)\b(?:SELECT\s+[\s\S]*?\bFROM|INSERT\s+INTO|UPDATE\s+[a-zA-Z0-9_.]+\s+SET|DELETE\s+FROM)\b[^;]*?\+\s*[a-zA-Z0-9_$]/i,
|
|
31
|
+
message: 'Interpolação dinâmica de parâmetros em instrução SQL detectada.',
|
|
32
|
+
remediation: 'Utilize consultas parametrizadas (Prepared Statements) ou query builders com bind de parâmetros.'
|
|
33
|
+
}),
|
|
34
|
+
Object.freeze({
|
|
35
|
+
id: 'DEVSEC-CWE-1333',
|
|
36
|
+
cwe: 'CWE-1333',
|
|
37
|
+
name: 'ReDoS (Regular Expression Denial of Service)',
|
|
38
|
+
severity: 'HIGH',
|
|
39
|
+
pattern: /\/([^\/\\]*(\([^\(\)]*[\+\*][^\(\)]*\)[\+\*])[^\/\\]*)\/[gimsuy]*/,
|
|
40
|
+
message: 'Expressão regular com quantificadores aninhados propensa a catastrophic backtracking exponencial.',
|
|
41
|
+
remediation: 'Refatore a regex para eliminar quantificadores aninhados ou substitua por autômato linear / busca determinística.'
|
|
42
|
+
}),
|
|
43
|
+
Object.freeze({
|
|
44
|
+
id: 'DEVSEC-CWE-79',
|
|
45
|
+
cwe: 'CWE-79',
|
|
46
|
+
name: 'Cross-Site Scripting (XSS)',
|
|
47
|
+
severity: 'HIGH',
|
|
48
|
+
pattern: /(?:innerHTML\s*=|dangerouslySetInnerHTML\s*=|document\s*\.\s*write\s*\()/i,
|
|
49
|
+
message: 'Atribuição insegura de HTML direto no DOM sem sanitização contextual.',
|
|
50
|
+
remediation: 'Utilize textContent ou bibliotecas canônicas de sanitização estrita (DOMPurify).'
|
|
51
|
+
}),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
id: 'DEVSEC-CWE-798-SECRET',
|
|
54
|
+
cwe: 'CWE-798',
|
|
55
|
+
name: 'Hardcoded Credentials & API Keys',
|
|
56
|
+
severity: 'CRITICAL',
|
|
57
|
+
pattern: /(?:(?:AKIA[0-9A-Z]{16})|(?:gh[pousr]_[A-Za-z0-9_]{30,255})|(?:xox[baprs]-[0-9a-zA-Z]{10,48})|(?:-----BEGIN\s+PRIVATE\s+KEY-----)|(?:(?:api[_-]?key|secret|password|auth[_-]?token)\s*[:=]\s*['"][A-Za-z0-9_\-\.+=]{16,}['"]))/i,
|
|
58
|
+
message: 'Credencial, token de acesso ou chave privada em texto plano embutida no código-fonte.',
|
|
59
|
+
remediation: 'Remova imediatamente o segredo, revogue a credencial e carregue via variáveis de ambiente seguras (process.env).'
|
|
60
|
+
}),
|
|
61
|
+
Object.freeze({
|
|
62
|
+
id: 'DEVSEC-CWE-798-EMAIL',
|
|
63
|
+
cwe: 'CWE-798',
|
|
64
|
+
name: 'Hardcoded Email Address',
|
|
65
|
+
severity: 'CRITICAL',
|
|
66
|
+
pattern: /(?:(?:email|user|account)\s*(?:[:=]|\|\|)\s*['"][a-zA-Z0-9._%+-]+@(gmail|hotmail|outlook|yahoo|[a-zA-Z0-9.-]+)\.[a-zA-Z]{2,}['"])/i,
|
|
67
|
+
message: 'Endereço de email hardcoded ou utilizado como fallback no código-fonte.',
|
|
68
|
+
remediation: 'Remova o email estático e obtenha o valor exclusivamente de process.env (ex: process.env.ATLASSIAN_EMAIL).'
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
id: 'DEVSEC-CWE-798-TENANT-DOMAIN',
|
|
72
|
+
cwe: 'CWE-798',
|
|
73
|
+
name: 'Hardcoded Atlassian Tenant Domain',
|
|
74
|
+
severity: 'CRITICAL',
|
|
75
|
+
pattern: /https?:\/\/[a-zA-Z0-9-]+\.atlassian\.net/i,
|
|
76
|
+
message: 'Domínio privado de tenant Atlassian hardcoded no código.',
|
|
77
|
+
remediation: 'Substitua por variável de ambiente dinâmica (ex: process.env.ATLASSIAN_DOMAIN).'
|
|
78
|
+
}),
|
|
79
|
+
Object.freeze({
|
|
80
|
+
id: 'DEVSEC-CWE-798-EXTERNAL-FILE',
|
|
81
|
+
cwe: 'CWE-798',
|
|
82
|
+
name: 'External Credential File Reference',
|
|
83
|
+
severity: 'CRITICAL',
|
|
84
|
+
pattern: /['"][^'"]*(?:id_rsa|id_ed25519|\.pgpass|\.netrc|credentials\.json|service-account.*\.json|Atlassian Account|Token\.txt|Account\.txt)['"]/i,
|
|
85
|
+
message: 'Referência a arquivo de credenciais, chave privada ou segredo em texto plano.',
|
|
86
|
+
remediation: 'Elimine referências a arquivos estáticos de credenciais. Utilize variáveis de ambiente ou .env protegido por .gitignore.'
|
|
87
|
+
}),
|
|
88
|
+
Object.freeze({
|
|
89
|
+
id: 'DEVSEC-CWE-95',
|
|
90
|
+
cwe: 'CWE-95',
|
|
91
|
+
name: 'Dynamic Code Evaluation',
|
|
92
|
+
severity: 'CRITICAL',
|
|
93
|
+
pattern: /\beval\s*\(|\bnew\s+Function\s*\(/,
|
|
94
|
+
message: 'Uso de eval() ou new Function() que permite execução arbitrária de código.',
|
|
95
|
+
remediation: 'Substitua por parsers seguros de JSON ou mapeamentos estáticos de funções.'
|
|
96
|
+
}),
|
|
97
|
+
Object.freeze({
|
|
98
|
+
id: 'DEVSEC-CWE-22',
|
|
99
|
+
cwe: 'CWE-22',
|
|
100
|
+
name: 'Path Traversal / Arbitrary File Access',
|
|
101
|
+
severity: 'HIGH',
|
|
102
|
+
pattern: /(?:fs\.(?:readFile|readFileSync|writeFile|writeFileSync|unlink|unlinkSync)\s*\([^,)]*(?:\+|`[^`]*\$\{)[^,)]*(?:\.\.\/|\.\.\\))/i,
|
|
103
|
+
message: 'Concatenação insegura de caminho de arquivo propensa a Path Traversal (CWE-22).',
|
|
104
|
+
remediation: 'Utilize path.resolve com validação de caminho relativo dentro de um diretório raiz seguro.'
|
|
105
|
+
})
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Executa comandos CLI de ferramentas industriais de forma blindada contra injection e timeouts.
|
|
110
|
+
*/
|
|
111
|
+
function runCliSafely(bin, args = [], options = {}) {
|
|
112
|
+
try {
|
|
113
|
+
const timeout = options.timeout || 30000;
|
|
114
|
+
const maxBuffer = options.maxBuffer || 15 * 1024 * 1024;
|
|
115
|
+
const cwd = options.cwd || process.cwd();
|
|
116
|
+
|
|
117
|
+
if (process.platform === 'win32') {
|
|
118
|
+
return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', bin, ...args], {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
timeout,
|
|
121
|
+
maxBuffer,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
cwd
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return spawnSync(bin, args, {
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
timeout,
|
|
129
|
+
maxBuffer,
|
|
130
|
+
windowsHide: true,
|
|
131
|
+
cwd
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return { status: -1, error: err };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Motor Canônico de Análise Estática de Segurança e Detecção de Segredos.
|
|
140
|
+
*/
|
|
141
|
+
class SastScanner {
|
|
142
|
+
constructor(rules = SECURITY_RULES) {
|
|
143
|
+
this.rules = Array.isArray(rules) ? rules : SECURITY_RULES;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Verifica se o Semgrep CLI está instalado e disponível no PATH do host.
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
isSemgrepAvailable() {
|
|
151
|
+
try {
|
|
152
|
+
const res = runCliSafely('semgrep', ['--version'], { timeout: 10000 });
|
|
153
|
+
return res.status === 0;
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Executa varredura industrial via Semgrep CLI caso instalado.
|
|
161
|
+
* @param {string} targetPath
|
|
162
|
+
* @returns {object|null}
|
|
163
|
+
*/
|
|
164
|
+
scanWithSemgrep(targetPath) {
|
|
165
|
+
try {
|
|
166
|
+
const res = runCliSafely('semgrep', ['scan', '--config', 'auto', '--json', '--quiet', targetPath], {
|
|
167
|
+
timeout: 60000
|
|
168
|
+
});
|
|
169
|
+
if (res.status === 0 && res.stdout) {
|
|
170
|
+
const data = JSON.parse(res.stdout);
|
|
171
|
+
const results = data.results || [];
|
|
172
|
+
const findings = results.map(r => ({
|
|
173
|
+
ruleId: r.check_id,
|
|
174
|
+
name: r.extra?.metadata?.cwe ? `CWE Violation: ${r.extra.metadata.cwe}` : r.check_id,
|
|
175
|
+
cwe: Array.isArray(r.extra?.metadata?.cwe) ? r.extra.metadata.cwe[0] : (r.extra?.metadata?.cwe || 'CWE-Unknown'),
|
|
176
|
+
severity: (r.extra?.severity || 'MEDIUM').toUpperCase(),
|
|
177
|
+
filename: r.path,
|
|
178
|
+
line: r.start?.line || 1,
|
|
179
|
+
snippet: (r.extra?.lines || '').trim().substring(0, 100),
|
|
180
|
+
message: r.extra?.message || 'Semgrep finding',
|
|
181
|
+
remediation: r.extra?.metadata?.shortlink || 'Consulte a documentação da regra Semgrep para correção.'
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
engine: 'semgrep_cli_industrial',
|
|
186
|
+
status: findings.some(f => f.severity === 'CRITICAL' || f.severity === 'HIGH' || f.severity === 'ERROR') ? 'FAILED' : 'PASSED',
|
|
187
|
+
totalFindings: findings.length,
|
|
188
|
+
criticalCount: findings.filter(f => f.severity === 'CRITICAL' || f.severity === 'ERROR').length,
|
|
189
|
+
highCount: findings.filter(f => f.severity === 'HIGH' || f.severity === 'WARNING').length,
|
|
190
|
+
mediumCount: findings.filter(f => f.severity === 'MEDIUM' || f.severity === 'INFO').length,
|
|
191
|
+
findings
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
} catch (_) {
|
|
195
|
+
// Fallback gracioso para o motor heurístico embutido
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Varre um arquivo físico no disco.
|
|
202
|
+
* @param {string} filePath
|
|
203
|
+
* @param {object} [options={}]
|
|
204
|
+
* @returns {object}
|
|
205
|
+
*/
|
|
206
|
+
scanFile(filePath, options = {}) {
|
|
207
|
+
if (!filePath || typeof filePath !== 'string' || !fs.existsSync(filePath)) {
|
|
208
|
+
throw new Error(`Arquivo não encontrado para varredura SAST: ${filePath}`);
|
|
209
|
+
}
|
|
210
|
+
const preferSemgrep = options.preferSemgrep === true;
|
|
211
|
+
if (preferSemgrep && this.isSemgrepAvailable()) {
|
|
212
|
+
const semgrepRes = this.scanWithSemgrep(filePath);
|
|
213
|
+
if (semgrepRes) return semgrepRes;
|
|
214
|
+
}
|
|
215
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
216
|
+
return this.scanCode(content, path.basename(filePath));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Varre uma string de código-fonte em memória de forma pura e defensiva.
|
|
221
|
+
* @param {string} code
|
|
222
|
+
* @param {string} [filename='snippet.js']
|
|
223
|
+
* @returns {object}
|
|
224
|
+
*/
|
|
225
|
+
scanCode(code, filename = 'snippet.js') {
|
|
226
|
+
const safeCode = typeof code === 'string' ? code : '';
|
|
227
|
+
const safeFilename = typeof filename === 'string' ? filename : 'snippet.js';
|
|
228
|
+
const findings = [];
|
|
229
|
+
const lines = safeCode.split('\n');
|
|
230
|
+
|
|
231
|
+
const isDocOrConfig = /\.(md|json|ya?ml)$/i.test(safeFilename);
|
|
232
|
+
const activeRules = isDocOrConfig
|
|
233
|
+
? this.rules.filter(r => r.cwe === 'CWE-798')
|
|
234
|
+
: this.rules;
|
|
235
|
+
|
|
236
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
237
|
+
const lineText = lines[lineIdx];
|
|
238
|
+
const isComment = /^\s*(?:\/\/|\/\*|\*)/.test(lineText);
|
|
239
|
+
|
|
240
|
+
for (const rule of activeRules) {
|
|
241
|
+
// Pula regras de injeção, XSS e domínios de tenant se for comentário explicativo
|
|
242
|
+
if (isComment && (rule.cwe !== 'CWE-798' || rule.id === 'DEVSEC-CWE-798-TENANT-DOMAIN')) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (rule.pattern.test(lineText)) {
|
|
247
|
+
findings.push({
|
|
248
|
+
ruleId: rule.id,
|
|
249
|
+
name: rule.name,
|
|
250
|
+
cwe: rule.cwe,
|
|
251
|
+
severity: rule.severity,
|
|
252
|
+
filename: safeFilename,
|
|
253
|
+
line: lineIdx + 1,
|
|
254
|
+
snippet: lineText.trim().substring(0, 100),
|
|
255
|
+
message: rule.message,
|
|
256
|
+
remediation: rule.remediation
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const hasCritical = findings.some(f => f.severity === 'CRITICAL');
|
|
263
|
+
const hasHigh = findings.some(f => f.severity === 'HIGH');
|
|
264
|
+
const status = (hasCritical || hasHigh) ? 'FAILED' : (findings.length > 0 ? 'WARNING' : 'PASSED');
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
engine: 'embedded_heuristic',
|
|
268
|
+
status,
|
|
269
|
+
filename: safeFilename,
|
|
270
|
+
totalFindings: findings.length,
|
|
271
|
+
criticalCount: findings.filter(f => f.severity === 'CRITICAL').length,
|
|
272
|
+
highCount: findings.filter(f => f.severity === 'HIGH').length,
|
|
273
|
+
mediumCount: findings.filter(f => f.severity === 'MEDIUM').length,
|
|
274
|
+
findings
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Varre um diretório recursivamente.
|
|
280
|
+
* @param {string} dirPath
|
|
281
|
+
* @param {object|Array} [options={}]
|
|
282
|
+
* @returns {object}
|
|
283
|
+
*/
|
|
284
|
+
scanDirectory(dirPath, options = {}) {
|
|
285
|
+
if (!dirPath || typeof dirPath !== 'string' || !fs.existsSync(dirPath)) {
|
|
286
|
+
return {
|
|
287
|
+
engine: 'embedded_heuristic_fallback',
|
|
288
|
+
filesScanned: 0,
|
|
289
|
+
totalFindings: 0,
|
|
290
|
+
criticalCount: 0,
|
|
291
|
+
highCount: 0,
|
|
292
|
+
findings: [],
|
|
293
|
+
status: 'PASSED'
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const extensions = Array.isArray(options) ? options : (options && options.extensions ? options.extensions : ['.js', '.ts', '.jsx', '.tsx', '.json', '.md']);
|
|
298
|
+
const preferSemgrep = typeof options === 'object' && options.preferSemgrep !== false;
|
|
299
|
+
|
|
300
|
+
if (preferSemgrep && this.isSemgrepAvailable()) {
|
|
301
|
+
const semgrepRes = this.scanWithSemgrep(dirPath);
|
|
302
|
+
if (semgrepRes) {
|
|
303
|
+
return semgrepRes;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const allFindings = [];
|
|
308
|
+
let filesScanned = 0;
|
|
309
|
+
|
|
310
|
+
const walk = (dir) => {
|
|
311
|
+
try {
|
|
312
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const fullPath = path.join(dir, entry.name);
|
|
315
|
+
if (entry.isDirectory()) {
|
|
316
|
+
if (!['node_modules', '.git', 'dist', 'build', '.gemini', 'test', 'saida', 'coverage'].includes(entry.name)) {
|
|
317
|
+
walk(fullPath);
|
|
318
|
+
}
|
|
319
|
+
} else if (entry.isFile()) {
|
|
320
|
+
if (entry.name === 'sast_scanner.js') continue;
|
|
321
|
+
const ext = path.extname(entry.name);
|
|
322
|
+
if (extensions.includes(ext)) {
|
|
323
|
+
filesScanned++;
|
|
324
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
325
|
+
const result = this.scanCode(content, path.relative(dirPath, fullPath));
|
|
326
|
+
if (result.findings.length > 0) {
|
|
327
|
+
allFindings.push(...result.findings);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (_) {
|
|
333
|
+
// Ignora diretórios inacessíveis
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
walk(dirPath);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
engine: 'embedded_heuristic_fallback',
|
|
341
|
+
advisoryNotice: 'Semgrep CLI não detectado no host. A auditoria utilizou regras heurísticas estáticas embutidas. Para auditoria SAST industrial completa (NIST SSDF SP 800-218), instale Semgrep CLI.',
|
|
342
|
+
filesScanned,
|
|
343
|
+
totalFindings: allFindings.length,
|
|
344
|
+
criticalCount: allFindings.filter(f => f.severity === 'CRITICAL').length,
|
|
345
|
+
highCount: allFindings.filter(f => f.severity === 'HIGH').length,
|
|
346
|
+
findings: allFindings,
|
|
347
|
+
status: allFindings.some(f => f.severity === 'CRITICAL' || f.severity === 'HIGH') ? 'FAILED' : 'PASSED'
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Formata os resultados da auditoria SAST em relatório executivo Markdown/texto.
|
|
353
|
+
* @param {object} scanResult
|
|
354
|
+
* @returns {string}
|
|
355
|
+
*/
|
|
356
|
+
formatReport(scanResult) {
|
|
357
|
+
if (!scanResult || typeof scanResult !== 'object') {
|
|
358
|
+
return 'Relatório SAST inválido ou vazio.';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const findings = Array.isArray(scanResult.findings) ? scanResult.findings : [];
|
|
362
|
+
let output = '================================================================================\n';
|
|
363
|
+
output += `RELATÓRIO DE AUDITORIA SAST & VULNERABILIDADES [ ${scanResult.status || 'UNKNOWN'} ]\n`;
|
|
364
|
+
output += '================================================================================\n\n';
|
|
365
|
+
|
|
366
|
+
output += `Mecanismo de Análise: ${scanResult.engine || 'heurística embutida'}\n`;
|
|
367
|
+
output += `Total de Ocorrências: ${scanResult.totalFindings || findings.length}\n`;
|
|
368
|
+
output += `- Críticas: ${scanResult.criticalCount || findings.filter(f => f.severity === 'CRITICAL').length}\n`;
|
|
369
|
+
output += `- Altas: ${scanResult.highCount || findings.filter(f => f.severity === 'HIGH').length}\n`;
|
|
370
|
+
output += `- Médias: ${scanResult.mediumCount || findings.filter(f => f.severity === 'MEDIUM').length}\n\n`;
|
|
371
|
+
|
|
372
|
+
if (scanResult.advisoryNotice) {
|
|
373
|
+
output += `Nota Consultiva: ${scanResult.advisoryNotice}\n\n`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
output += 'VULNERABILIDADES E SEGREDOS IDENTIFICADOS:\n';
|
|
377
|
+
if (findings.length === 0) {
|
|
378
|
+
output += 'Nenhuma vulnerabilidade ou credencial exposta detectada no escopo auditado.\n';
|
|
379
|
+
} else {
|
|
380
|
+
findings.forEach((f, idx) => {
|
|
381
|
+
output += `${idx + 1}. <${f.filename}:${f.line}> · [${f.severity}] ${f.name} (${f.ruleId} · ${f.cwe})\n`;
|
|
382
|
+
output += ` Trecho: "${f.snippet}"\n`;
|
|
383
|
+
output += ` Descrição: ${f.message}\n`;
|
|
384
|
+
output += ` Remediação: ${f.remediation}\n\n`;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
output += '================================================================================\n';
|
|
389
|
+
return output;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Emite relatório oficial OASIS SARIF v2.1.0 para snippet de código em memória.
|
|
394
|
+
* @param {string} code
|
|
395
|
+
* @param {string} [filename='snippet.js']
|
|
396
|
+
* @returns {object}
|
|
397
|
+
*/
|
|
398
|
+
scanCodeToSarif(code, filename = 'snippet.js') {
|
|
399
|
+
const scanResult = this.scanCode(code, filename);
|
|
400
|
+
return createSarifReport({
|
|
401
|
+
toolName: 'agent-master-sast-scanner',
|
|
402
|
+
toolVersion: '5.1.0',
|
|
403
|
+
rules: this.rules,
|
|
404
|
+
findings: scanResult.findings,
|
|
405
|
+
properties: {
|
|
406
|
+
engine: scanResult.engine,
|
|
407
|
+
status: scanResult.status,
|
|
408
|
+
filename
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Emite relatório oficial OASIS SARIF v2.1.0 para arquivo ou diretório físico.
|
|
415
|
+
* @param {string} targetPath
|
|
416
|
+
* @param {object} [options={}]
|
|
417
|
+
* @returns {object}
|
|
418
|
+
*/
|
|
419
|
+
scanToSarif(targetPath, options = {}) {
|
|
420
|
+
const isFile = fs.existsSync(targetPath) && fs.statSync(targetPath).isFile();
|
|
421
|
+
const scanResult = isFile ? this.scanFile(targetPath, options) : this.scanDirectory(targetPath, options);
|
|
422
|
+
|
|
423
|
+
return createSarifReport({
|
|
424
|
+
toolName: scanResult.engine === 'semgrep_cli_industrial' ? 'semgrep-sast' : 'agent-master-sast-scanner',
|
|
425
|
+
toolVersion: '5.1.0',
|
|
426
|
+
rules: this.rules,
|
|
427
|
+
findings: scanResult.findings,
|
|
428
|
+
properties: {
|
|
429
|
+
engine: scanResult.engine,
|
|
430
|
+
status: scanResult.status,
|
|
431
|
+
targetPath,
|
|
432
|
+
advisoryNotice: scanResult.advisoryNotice || null
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
SastScanner,
|
|
440
|
+
SECURITY_RULES,
|
|
441
|
+
runCliSafely
|
|
442
|
+
};
|