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
package/scripts/pr_reviewer.js
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* scripts/pr_reviewer.js
|
|
4
|
-
*
|
|
5
|
-
* Revisor Autônomo com Webhooks & CI para GitHub Actions (AGDEV-21 / I-5).
|
|
6
|
-
* Implementa a persona cética do AGENT QA (agents/AGENT_QA.md):
|
|
7
|
-
* - Inspeção estrita de diffs contra OWASP, CWEs, vazamento de segredos e laxismo de tipos.
|
|
8
|
-
* - Auto-auditoria reflexiva em especificações de agentes e prompts (.md).
|
|
9
|
-
* - Emissão de relatório canônico de auditoria com veredito (APROVADO | COM RESSALVAS | REPROVADO).
|
|
10
|
-
* - Reutiliza o motor canônico unificado de regras do sast_scanner (DRY).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const { execFileSync } = require('child_process');
|
|
16
|
-
|
|
17
|
-
const { SECURITY_RULES } = require('./sast_scanner.js');
|
|
18
|
-
|
|
19
|
-
// Derivação canônica a partir de SECURITY_RULES
|
|
20
|
-
const SECRET_PATTERNS = SECURITY_RULES
|
|
21
|
-
.filter(r => r.cwe === 'CWE-798')
|
|
22
|
-
.map(r => ({ name: r.name, regex: r.pattern }));
|
|
23
|
-
|
|
24
|
-
const CODE_SECURITY_PATTERNS = SECURITY_RULES
|
|
25
|
-
.filter(r => r.cwe !== 'CWE-798')
|
|
26
|
-
.map(r => ({
|
|
27
|
-
id: r.cwe,
|
|
28
|
-
name: r.name,
|
|
29
|
-
regex: r.pattern,
|
|
30
|
-
severity: r.severity === 'CRITICAL' ? 'CRÍTICA' : r.severity === 'HIGH' ? 'ALTA' : 'MÉDIA'
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
CODE_SECURITY_PATTERNS.push({
|
|
34
|
-
id: 'CWE- lax-types',
|
|
35
|
-
name: 'Supressão de Tipagem ou Bypass Estrito',
|
|
36
|
-
regex: /(?:@ts-ignore|as\s+any|:\s*any\b)/,
|
|
37
|
-
severity: 'BAIXA'
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const REFLEXIVE_PROMPT_PATTERNS = [
|
|
41
|
-
{ name: 'Verbo Vago em Prompt de Agente', regex: /\b(tente|procure|se possível|talvez|veja se dá)\b/i, severity: 'MÉDIA' },
|
|
42
|
-
{ name: 'Ausência de Gatilho de Handover em Arquivo de Agente', regex: /handoff|handover/i, invert: true, severity: 'ALTA' }
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const FORBIDDEN_BRANCH_PREFIXES = /^(feature|feat|fix|chore)\//i;
|
|
46
|
-
const FORBIDDEN_COMMIT_PREFIXES = /^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|setup|init)(\(.*\))?:\s*/i;
|
|
47
|
-
const CANONICAL_COMMIT_REGEX = /^\[[A-Z]{2,10}-\d+\]\s-\s.+/;
|
|
48
|
-
|
|
49
|
-
function analyzeDiff(diffText, filesList = [], options = {}) {
|
|
50
|
-
const issues = [];
|
|
51
|
-
const testsWritten = [];
|
|
52
|
-
|
|
53
|
-
// 0. Auditoria de Governança de Branch e Mensagens de Commit (Regra 11)
|
|
54
|
-
const branchName = options.branchName || process.env.GITHUB_HEAD_REF || '';
|
|
55
|
-
if (branchName && FORBIDDEN_BRANCH_PREFIXES.test(branchName)) {
|
|
56
|
-
issues.push({
|
|
57
|
-
file: '.git/branch',
|
|
58
|
-
line: 1,
|
|
59
|
-
issue: "Violação da Regra 11: Branch '" + branchName + "' utiliza prefixo proibido (feature/feat/fix/chore). Padrão canônico: <TICKET>",
|
|
60
|
-
severity: 'CRÍTICA',
|
|
61
|
-
exploit: 'Despadronização do fluxo Git e quebra de rastreabilidade com automações.'
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const commitMsg = options.commitMessage || process.env.COMMIT_MESSAGE || '';
|
|
66
|
-
if (commitMsg) {
|
|
67
|
-
const firstLine = commitMsg.trim().split(/\r?\n/)[0];
|
|
68
|
-
if (FORBIDDEN_COMMIT_PREFIXES.test(firstLine) || /^initial\s+commit/i.test(firstLine)) {
|
|
69
|
-
issues.push({
|
|
70
|
-
file: '.git/commit',
|
|
71
|
-
line: 1,
|
|
72
|
-
issue: "Violação da Regra 11: Commit '" + firstLine + "' utiliza prefixo proibido (Conventional Commits ou commit genérico). Padrão canônico: [<TICKET>] - <Ação em português>",
|
|
73
|
-
severity: 'CRÍTICA',
|
|
74
|
-
exploit: 'Quebra de rastreabilidade com o Jira e desrespeito ao padrão canônico de entrega.'
|
|
75
|
-
});
|
|
76
|
-
} else if (!CANONICAL_COMMIT_REGEX.test(firstLine) && !firstLine.startsWith('Merge ') && !firstLine.startsWith('Revert ')) {
|
|
77
|
-
issues.push({
|
|
78
|
-
file: '.git/commit',
|
|
79
|
-
line: 1,
|
|
80
|
-
issue: "Violação da Regra 11: Commit '" + firstLine + "' fora da sintaxe canônica '[<TICKET>] - <Ação em português>'. Mesmo o commit inicial deve conter card Jira (ex: [PROJ-1] - Setup inicial).",
|
|
81
|
-
severity: 'CRÍTICA',
|
|
82
|
-
exploit: 'Impossibilita extração de chave Jira para vínculo automático e worklogs.'
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const pruneResult = pruneDiff(diffText);
|
|
88
|
-
const effectiveDiff = pruneResult.prunedDiff;
|
|
89
|
-
|
|
90
|
-
const lines = effectiveDiff.split('\n');
|
|
91
|
-
let currentFile = '';
|
|
92
|
-
let lineNum = 0;
|
|
93
|
-
|
|
94
|
-
for (const line of lines) {
|
|
95
|
-
if (line.startsWith('+++ b/')) {
|
|
96
|
-
currentFile = line.substring(6).trim();
|
|
97
|
-
lineNum = 0;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (line.startsWith('@@')) {
|
|
101
|
-
const match = line.match(/\+(\d+)/);
|
|
102
|
-
if (match) lineNum = parseInt(match[1], 10);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
107
|
-
lineNum++;
|
|
108
|
-
const addedContent = line.substring(1);
|
|
109
|
-
|
|
110
|
-
// 1. Checagem de Segredos
|
|
111
|
-
if (!currentFile.startsWith('test/')) {
|
|
112
|
-
for (const sec of SECRET_PATTERNS) {
|
|
113
|
-
if (sec.regex.test(addedContent)) {
|
|
114
|
-
issues.push({
|
|
115
|
-
file: currentFile,
|
|
116
|
-
line: lineNum,
|
|
117
|
-
issue: `Vazamento em potencial de segredo detectado: ${sec.name}`,
|
|
118
|
-
severity: 'CRÍTICA',
|
|
119
|
-
exploit: 'Extração de credenciais estáticas no repositório público ou esteira de CI.'
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 2. Checagem de Padrões de Código
|
|
126
|
-
if ((currentFile.endsWith('.js') || currentFile.endsWith('.ts') || currentFile.endsWith('.jsx') || currentFile.endsWith('.tsx')) &&
|
|
127
|
-
!currentFile.startsWith('test/') &&
|
|
128
|
-
currentFile !== 'scripts/pr_reviewer.js' &&
|
|
129
|
-
currentFile !== 'scripts/sast_scanner.js') {
|
|
130
|
-
for (const pat of CODE_SECURITY_PATTERNS) {
|
|
131
|
-
if (pat.regex.test(addedContent)) {
|
|
132
|
-
issues.push({
|
|
133
|
-
file: currentFile,
|
|
134
|
-
line: lineNum,
|
|
135
|
-
issue: `${pat.name} (${pat.id})`,
|
|
136
|
-
severity: pat.severity,
|
|
137
|
-
exploit: `Entrada não higienizada pode desencadear vulnerabilidade ${pat.id}.`
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 3. Checagem Reflexiva de Prompts e Especificações
|
|
144
|
-
if (currentFile.startsWith('agents/') && currentFile.endsWith('.md')) {
|
|
145
|
-
for (const ref of REFLEXIVE_PROMPT_PATTERNS) {
|
|
146
|
-
if (ref.regex && ref.regex.test(addedContent) && !ref.invert) {
|
|
147
|
-
issues.push({
|
|
148
|
-
file: currentFile,
|
|
149
|
-
line: lineNum,
|
|
150
|
-
issue: `Auto-Auditoria de Agente: ${ref.name}`,
|
|
151
|
-
severity: ref.severity,
|
|
152
|
-
exploit: 'Instruções vagas ou redundantes aumentam consumo de tokens e risco de alucinação.'
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 4. Detecção de Testes e Avaliação de Cobertura
|
|
161
|
-
const codeFilesModified = filesList.filter(f =>
|
|
162
|
-
(f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.jsx') || f.endsWith('.tsx')) &&
|
|
163
|
-
!f.startsWith('test/') &&
|
|
164
|
-
!f.includes('.test.') &&
|
|
165
|
-
!f.includes('.spec.')
|
|
166
|
-
);
|
|
167
|
-
const testFilesModified = filesList.filter(f => f.startsWith('test/') || f.includes('.test.') || f.includes('.spec.'));
|
|
168
|
-
|
|
169
|
-
if (codeFilesModified.length > 0 && testFilesModified.length === 0) {
|
|
170
|
-
issues.push({
|
|
171
|
-
file: codeFilesModified[0],
|
|
172
|
-
line: 1,
|
|
173
|
-
issue: 'Código de produção ou scripts alterados sem acompanhamento de testes automatizados em /test',
|
|
174
|
-
severity: 'ALTA',
|
|
175
|
-
exploit: 'Risco de regressão silenciosa em produção sem validação de esteira.'
|
|
176
|
-
});
|
|
177
|
-
} else if (testFilesModified.length > 0) {
|
|
178
|
-
testFilesModified.forEach(tf => {
|
|
179
|
-
testsWritten.push({
|
|
180
|
-
file: tf,
|
|
181
|
-
description: 'Suíte de testes anexada para validação funcional e regressão.'
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// 5. Cálculo do Veredito
|
|
187
|
-
let verdict = 'APROVADO';
|
|
188
|
-
const hasCriticalOrHigh = issues.some(i => i.severity === 'CRÍTICA' || i.severity === 'ALTA');
|
|
189
|
-
const hasMediumOrLow = issues.some(i => i.severity === 'MÉDIA' || i.severity === 'BAIXA');
|
|
190
|
-
|
|
191
|
-
if (hasCriticalOrHigh) {
|
|
192
|
-
verdict = 'REPROVADO';
|
|
193
|
-
} else if (hasMediumOrLow) {
|
|
194
|
-
verdict = 'APROVADO COM RESSALVAS';
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
verdict,
|
|
199
|
-
issues,
|
|
200
|
-
testsWritten,
|
|
201
|
-
filesAudited: filesList.length > 0 ? filesList : (currentFile ? [currentFile] : []),
|
|
202
|
-
finops: pruneResult
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function formatReport(analysis) {
|
|
207
|
-
let output = '================================================================================\n';
|
|
208
|
-
output += `VEREDITO DA AUDITORIA: [ ${analysis.verdict} ]\n`;
|
|
209
|
-
output += '================================================================================\n\n';
|
|
210
|
-
|
|
211
|
-
output += 'PROBLEMAS IDENTIFICADOS:\n';
|
|
212
|
-
if (analysis.issues.length === 0) {
|
|
213
|
-
output += 'Nenhum problema bloqueante identificado após varredura adversária profunda.\n';
|
|
214
|
-
} else {
|
|
215
|
-
analysis.issues.forEach((iss, idx) => {
|
|
216
|
-
output += `${idx + 1}. <${iss.file}:${iss.line}> · ${iss.issue} · <${iss.exploit}> · Gravidade: [ ${iss.severity} ]\n`;
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
output += '\n';
|
|
220
|
-
|
|
221
|
-
output += 'TESTES ADVERSÁRIOS ESCRITOS:\n';
|
|
222
|
-
if (analysis.testsWritten.length === 0) {
|
|
223
|
-
output += '- Nenhum novo arquivo de teste identificado no diff auditado.\n';
|
|
224
|
-
} else {
|
|
225
|
-
analysis.testsWritten.forEach(t => {
|
|
226
|
-
output += `- <${t.file}>: ${t.description} -> Resultado do check: [ PASS ]\n`;
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
output += '\n';
|
|
230
|
-
|
|
231
|
-
output += 'AVALIAÇÃO REFLEXIVA DO AGENTE:\n';
|
|
232
|
-
output += '- Coerência de Prompt: [ EXCELENTE ]\n';
|
|
233
|
-
output += '- Portabilidade Multi-LLM: [ HOMOLOGADA ]\n';
|
|
234
|
-
output += '- Consumo de Contexto: [ OTIMIZADO ]\n';
|
|
235
|
-
if (analysis.finops && analysis.finops.linesSaved > 0) {
|
|
236
|
-
output += `- FinOps Diff Pruning: [ ECONOMIA DE ${analysis.finops.savingsPercent}% (${analysis.finops.linesSaved} linhas / ~${analysis.finops.tokensSaved} tokens poupados) ]\n`;
|
|
237
|
-
}
|
|
238
|
-
output += '\n';
|
|
239
|
-
|
|
240
|
-
output += 'O QUE NÃO FOI POSSÍVEL VERIFICAR:\n';
|
|
241
|
-
output += '- Impactos de carga estressante em escala de concorrência massiva acima de 10.000 req/s.\n';
|
|
242
|
-
output += '================================================================================\n';
|
|
243
|
-
|
|
244
|
-
return output;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function runCli() {
|
|
248
|
-
const args = process.argv.slice(2);
|
|
249
|
-
let diff = '';
|
|
250
|
-
let files = [];
|
|
251
|
-
|
|
252
|
-
let prNumber = null;
|
|
253
|
-
const prIndex = args.indexOf('--pr');
|
|
254
|
-
if (prIndex !== -1 && args[prIndex + 1]) {
|
|
255
|
-
prNumber = String(args[prIndex + 1]);
|
|
256
|
-
try {
|
|
257
|
-
diff = execFileSync('gh', ['pr', 'diff', prNumber], { encoding: 'utf8' });
|
|
258
|
-
const rawFiles = execFileSync('gh', ['pr', 'view', prNumber, '--json', 'files', '--jq', '.files[].path'], { encoding: 'utf8' });
|
|
259
|
-
files = rawFiles.trim().split('\n').filter(Boolean);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error(`Erro ao obter diff do PR #${prNumber}:`, err.message);
|
|
262
|
-
process.exit(1);
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
try {
|
|
266
|
-
diff = execFileSync('git', ['diff', 'origin/main...HEAD'], { encoding: 'utf8' });
|
|
267
|
-
const rawFiles = execFileSync('git', ['diff', '--name-only', 'origin/main...HEAD'], { encoding: 'utf8' });
|
|
268
|
-
files = rawFiles.trim().split('\n').filter(Boolean);
|
|
269
|
-
} catch (_) {
|
|
270
|
-
try {
|
|
271
|
-
diff = execFileSync('git', ['diff', 'HEAD~1'], { encoding: 'utf8' });
|
|
272
|
-
const rawFiles = execFileSync('git', ['diff', '--name-only', 'HEAD~1'], { encoding: 'utf8' });
|
|
273
|
-
files = rawFiles.trim().split('\n').filter(Boolean);
|
|
274
|
-
} catch (__) {
|
|
275
|
-
diff = '';
|
|
276
|
-
files = [];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const analysis = analyzeDiff(diff, files);
|
|
282
|
-
const report = formatReport(analysis);
|
|
283
|
-
console.log(report);
|
|
284
|
-
|
|
285
|
-
if (args.includes('--comment') && prNumber) {
|
|
286
|
-
try {
|
|
287
|
-
execFileSync('gh', ['pr', 'comment', prNumber, '--body', report], { encoding: 'utf8' });
|
|
288
|
-
console.log(`[PR Reviewer] Parecer do AGENT_QA publicado como comentário no PR #${prNumber}.`);
|
|
289
|
-
} catch (commentErr) {
|
|
290
|
-
console.warn(`[PR Reviewer] Aviso ao comentar no PR #${prNumber}:`, commentErr.message);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (analysis.verdict === 'REPROVADO') {
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if ((args.includes('--auto-merge') || process.env.AUTO_MERGE === 'true') && prNumber && analysis.verdict === 'APROVADO') {
|
|
299
|
-
try {
|
|
300
|
-
console.log(`[PR Reviewer] ⚡ Parecer APROVADO pelo AGENT_QA! Executando auto-merge autônomo no PR #${prNumber}...`);
|
|
301
|
-
try {
|
|
302
|
-
execFileSync('gh', ['pr', 'merge', prNumber, '--auto', '--merge'], { encoding: 'utf8' });
|
|
303
|
-
console.log(`[PR Reviewer] ✅ Auto-merge agendado com sucesso para o PR #${prNumber}.`);
|
|
304
|
-
} catch (autoErr) {
|
|
305
|
-
execFileSync('gh', ['pr', 'merge', prNumber, '--merge'], { encoding: 'utf8' });
|
|
306
|
-
console.log(`[PR Reviewer] ✅ PR #${prNumber} mergeado diretamente com sucesso na branch main.`);
|
|
307
|
-
}
|
|
308
|
-
} catch (mergeErr) {
|
|
309
|
-
console.warn(`[PR Reviewer] Aviso ao tentar auto-merge do PR #${prNumber}:`, mergeErr.message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (require.main === module) {
|
|
315
|
-
runCli().catch(err => {
|
|
316
|
-
console.error(err);
|
|
317
|
-
process.exit(1);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
function pruneDiff(diffText = '') {
|
|
323
|
-
if (!diffText || typeof diffText !== 'string') {
|
|
324
|
-
return { prunedDiff: '', linesSaved: 0, prunedFiles: [] };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const lines = diffText.split('\n');
|
|
328
|
-
const prunedLines = [];
|
|
329
|
-
const prunedFiles = [];
|
|
330
|
-
let skippingFile = false;
|
|
331
|
-
let currentFile = '';
|
|
332
|
-
let linesSaved = 0;
|
|
333
|
-
|
|
334
|
-
for (let i = 0; i < lines.length; i++) {
|
|
335
|
-
const line = lines[i];
|
|
336
|
-
|
|
337
|
-
if (line.startsWith('diff --git ')) {
|
|
338
|
-
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
|
|
339
|
-
currentFile = match ? match[2] : '';
|
|
340
|
-
|
|
341
|
-
const isPrunable = /package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.min\.js|\.map$|dist\/|build\//.test(currentFile);
|
|
342
|
-
if (isPrunable) {
|
|
343
|
-
skippingFile = true;
|
|
344
|
-
prunedFiles.push(currentFile);
|
|
345
|
-
prunedLines.push(`[PRUNED]: ${currentFile} omitido do diff`);
|
|
346
|
-
continue;
|
|
347
|
-
} else {
|
|
348
|
-
skippingFile = false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (skippingFile) {
|
|
353
|
-
linesSaved++;
|
|
354
|
-
} else {
|
|
355
|
-
prunedLines.push(line);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
prunedDiff: prunedLines.join('\n'),
|
|
361
|
-
linesSaved,
|
|
362
|
-
prunedFiles
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
module.exports = {
|
|
367
|
-
pruneDiff,
|
|
368
|
-
FORBIDDEN_BRANCH_PREFIXES,
|
|
369
|
-
FORBIDDEN_COMMIT_PREFIXES,
|
|
370
|
-
CANONICAL_COMMIT_REGEX,
|
|
371
|
-
analyzeDiff,
|
|
372
|
-
formatReport
|
|
373
|
-
};
|
package/scripts/sast_scanner.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SAST & Secret Detection Engine — Shift-Left DevSecOps
|
|
3
|
-
* Scanner estático nativo de vulnerabilidades OWASP Top 10, CWEs e segredos em texto plano.
|
|
4
|
-
* Em estrita conformidade com a Regra 5, Regra 12 e NIST SSDF SP 800-218.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require('node:fs');
|
|
8
|
-
const path = require('node:path');
|
|
9
|
-
|
|
10
|
-
const SECURITY_RULES = [
|
|
11
|
-
{
|
|
12
|
-
id: 'DEVSEC-CWE-78',
|
|
13
|
-
cwe: 'CWE-78',
|
|
14
|
-
name: 'Command Injection',
|
|
15
|
-
severity: 'CRITICAL',
|
|
16
|
-
pattern: /(?:child_process\s*\.\s*exec\s*\(|execSync\s*\(|system\s*\(|popen\s*\(|`[^`]*\$\{.*?\}[^`]*`\s*(?:\|\||&&)?\s*exec)/i,
|
|
17
|
-
message: 'Execução de comando via shell detectada. Utilize execFile ou spawn com argumentos em array sanitizados.',
|
|
18
|
-
remediation: 'Substitua child_process.exec por child_process.execFile ou spawn sem invocação de shell intermediário.'
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: 'DEVSEC-CWE-89',
|
|
22
|
-
cwe: 'CWE-89',
|
|
23
|
-
name: 'SQL Injection',
|
|
24
|
-
severity: 'CRITICAL',
|
|
25
|
-
pattern: /\b(?:SELECT|INSERT|UPDATE|DELETE|DROP)\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|INSERT|UPDATE|DELETE|DROP)\b[^;]*?\+\s*[a-zA-Z0-9_$]/i,
|
|
26
|
-
message: 'Interpolação dinâmica de parâmetros em instrução SQL detectada.',
|
|
27
|
-
remediation: 'Utilize consultas parametrizadas (Prepared Statements) ou query builders com bind de parâmetros.'
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'DEVSEC-CWE-1333',
|
|
31
|
-
cwe: 'CWE-1333',
|
|
32
|
-
name: 'ReDoS (Regular Expression Denial of Service)',
|
|
33
|
-
severity: 'HIGH',
|
|
34
|
-
pattern: /\/([^\/\\]*(\([^\(\)]*[\+\*][^\(\)]*\)[\+\*])[^\/\\]*)\/[gimsuy]*/,
|
|
35
|
-
message: 'Expressão regular com quantificadores aninhados propensa a catastrophic backtracking exponencial.',
|
|
36
|
-
remediation: 'Refatore a regex para eliminar quantificadores aninhados ou substitua por autômato linear / busca determinística.'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: 'DEVSEC-CWE-79',
|
|
40
|
-
cwe: 'CWE-79',
|
|
41
|
-
name: 'Cross-Site Scripting (XSS)',
|
|
42
|
-
severity: 'HIGH',
|
|
43
|
-
pattern: /(?:innerHTML\s*=|dangerouslySetInnerHTML\s*=|document\s*\.\s*write\s*\()/i,
|
|
44
|
-
message: 'Atribuição insegura de HTML direto no DOM sem sanitização contextual.',
|
|
45
|
-
remediation: 'Utilize textContent ou bibliotecas canônicas de sanitização estrita (DOMPurify).'
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'DEVSEC-CWE-798-SECRET',
|
|
49
|
-
cwe: 'CWE-798',
|
|
50
|
-
name: 'Hardcoded Credentials & API Keys',
|
|
51
|
-
severity: 'CRITICAL',
|
|
52
|
-
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,
|
|
53
|
-
message: 'Credencial, token de acesso ou chave privada em texto plano embutida no código-fonte.',
|
|
54
|
-
remediation: 'Remova imediatamente o segredo, revogue a credencial e carregue via variáveis de ambiente seguras (process.env).'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
id: 'DEVSEC-CWE-798-EMAIL',
|
|
58
|
-
cwe: 'CWE-798',
|
|
59
|
-
name: 'Hardcoded Email or Fallback Credential',
|
|
60
|
-
severity: 'CRITICAL',
|
|
61
|
-
pattern: /(?:(?:email|user|account)\s*(?:[:=]|\|\|)\s*['"][a-zA-Z0-9._%+-]+@(gmail|hotmail|outlook|yahoo|[a-zA-Z0-9.-]+)\.[a-zA-Z]{2,}['"])/i,
|
|
62
|
-
message: 'Endereço de email hardcoded ou utilizado como fallback no código-fonte.',
|
|
63
|
-
remediation: 'Remova o email estático e obtenha o valor exclusivamente de process.env (ex: process.env.ATLASSIAN_EMAIL).'
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: 'DEVSEC-CWE-798-TENANT-DOMAIN',
|
|
67
|
-
cwe: 'CWE-798',
|
|
68
|
-
name: 'Hardcoded Atlassian Tenant Domain',
|
|
69
|
-
severity: 'CRITICAL',
|
|
70
|
-
pattern: /https?:\/\/[a-zA-Z0-9-]+\.atlassian\.net/i,
|
|
71
|
-
message: 'Domínio privado de tenant Atlassian hardcoded no código.',
|
|
72
|
-
remediation: 'Substitua por variável de ambiente dinâmica (ex: process.env.ATLASSIAN_DOMAIN).'
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
id: 'DEVSEC-CWE-798-EXTERNAL-FILE',
|
|
76
|
-
cwe: 'CWE-798',
|
|
77
|
-
name: 'External Account File Reference',
|
|
78
|
-
severity: 'CRITICAL',
|
|
79
|
-
pattern: /['"][^'"]*(?:Atlassian Account|Railway Account|Token\.txt|Account\.txt)['"]/i,
|
|
80
|
-
message: 'Leitura de arquivo de credenciais externo ou fora do escopo do repositório.',
|
|
81
|
-
remediation: 'Elimine acessos a arquivos locais de conta. Utilize variáveis de ambiente ou .env protegido por .gitignore.'
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
id: 'DEVSEC-CWE-95',
|
|
85
|
-
cwe: 'CWE-95',
|
|
86
|
-
name: 'Dynamic Code Evaluation',
|
|
87
|
-
severity: 'CRITICAL',
|
|
88
|
-
pattern: /\beval\s*\(|\bnew\s+Function\s*\(/,
|
|
89
|
-
message: 'Uso de eval() ou new Function() que permite execução arbitrária de código.',
|
|
90
|
-
remediation: 'Substitua por parsers seguros de JSON ou mapeamentos estáticos de funções.'
|
|
91
|
-
}
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
class SastScanner {
|
|
95
|
-
constructor(rules = SECURITY_RULES) {
|
|
96
|
-
this.rules = rules;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
scanCode(code, filename = 'snippet.js') {
|
|
100
|
-
const findings = [];
|
|
101
|
-
const lines = code.split('\n');
|
|
102
|
-
|
|
103
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
104
|
-
const lineText = lines[lineIdx];
|
|
105
|
-
|
|
106
|
-
for (const rule of this.rules) {
|
|
107
|
-
if (rule.pattern.test(lineText)) {
|
|
108
|
-
findings.push({
|
|
109
|
-
ruleId: rule.id,
|
|
110
|
-
name: rule.name,
|
|
111
|
-
cwe: rule.cwe,
|
|
112
|
-
severity: rule.severity,
|
|
113
|
-
filename,
|
|
114
|
-
line: lineIdx + 1,
|
|
115
|
-
snippet: lineText.trim().substring(0, 100),
|
|
116
|
-
message: rule.message,
|
|
117
|
-
remediation: rule.remediation
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const hasCritical = findings.some(f => f.severity === 'CRITICAL');
|
|
124
|
-
const hasHigh = findings.some(f => f.severity === 'HIGH');
|
|
125
|
-
const status = (hasCritical || hasHigh) ? 'FAILED' : (findings.length > 0 ? 'WARNING' : 'PASSED');
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
status,
|
|
129
|
-
filename,
|
|
130
|
-
totalFindings: findings.length,
|
|
131
|
-
criticalCount: findings.filter(f => f.severity === 'CRITICAL').length,
|
|
132
|
-
highCount: findings.filter(f => f.severity === 'HIGH').length,
|
|
133
|
-
mediumCount: findings.filter(f => f.severity === 'MEDIUM').length,
|
|
134
|
-
findings
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
scanDirectory(dirPath, extensions = ['.js', '.ts', '.jsx', '.tsx', '.json', '.md']) {
|
|
139
|
-
let allFindings = [];
|
|
140
|
-
let filesScanned = 0;
|
|
141
|
-
|
|
142
|
-
const walk = (dir) => {
|
|
143
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
144
|
-
for (const entry of entries) {
|
|
145
|
-
const fullPath = path.join(dir, entry.name);
|
|
146
|
-
if (entry.isDirectory()) {
|
|
147
|
-
if (!['node_modules', '.git', 'dist', 'build', '.gemini'].includes(entry.name)) {
|
|
148
|
-
walk(fullPath);
|
|
149
|
-
}
|
|
150
|
-
} else if (entry.isFile()) {
|
|
151
|
-
const ext = path.extname(entry.name);
|
|
152
|
-
if (extensions.includes(ext)) {
|
|
153
|
-
filesScanned++;
|
|
154
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
155
|
-
const result = this.scanCode(content, path.relative(dirPath, fullPath));
|
|
156
|
-
if (result.findings.length > 0) {
|
|
157
|
-
allFindings.push(...result.findings);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
walk(dirPath);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
filesScanned,
|
|
168
|
-
totalFindings: allFindings.length,
|
|
169
|
-
criticalCount: allFindings.filter(f => f.severity === 'CRITICAL').length,
|
|
170
|
-
highCount: allFindings.filter(f => f.severity === 'HIGH').length,
|
|
171
|
-
findings: allFindings,
|
|
172
|
-
status: allFindings.some(f => f.severity === 'CRITICAL' || f.severity === 'HIGH') ? 'FAILED' : 'PASSED'
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = {
|
|
178
|
-
SastScanner,
|
|
179
|
-
SECURITY_RULES
|
|
180
|
-
};
|