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,627 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/auditors/pr_reviewer.js
|
|
4
|
+
*
|
|
5
|
+
* Revisor Autônomo com Webhooks & CI para GitHub Actions (v5.1 Enterprise).
|
|
6
|
+
* Implementa a autoridade de veto do AGENT QA (agents/AGENT_QA.md):
|
|
7
|
+
* - Inspeção adversária de diffs contra OWASP Top 10, CWEs, credenciais e laxismo de tipos.
|
|
8
|
+
* - Auto-auditoria reflexiva em especificações de agentes e prompts (.md).
|
|
9
|
+
* - Governança Dual-Agent e cálculo de Raio de Explosão (Blast Radius).
|
|
10
|
+
* - Verificação da Regra 11 de Higiene Git & Rastreabilidade Jira.
|
|
11
|
+
* - FinOps Diff Pruning com economia mensurável de tokens.
|
|
12
|
+
* - Emissão de relatórios oficiais em padrão OASIS SARIF v2.1.0 (auditToSarif).
|
|
13
|
+
* - Reutilização do motor unificado de regras do sast_scanner (DRY).
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { execFileSync } = require('node:child_process');
|
|
20
|
+
|
|
21
|
+
const { SECURITY_RULES } = require('./sast_scanner.js');
|
|
22
|
+
const { createSarifReport } = require('../core/sarif_engine.js');
|
|
23
|
+
|
|
24
|
+
// Derivação canônica a partir de SECURITY_RULES
|
|
25
|
+
const SECRET_PATTERNS = Object.freeze(
|
|
26
|
+
SECURITY_RULES
|
|
27
|
+
.filter(r => r.cwe === 'CWE-798')
|
|
28
|
+
.map(r => Object.freeze({ id: r.id, name: r.name, regex: r.pattern }))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const CODE_SECURITY_PATTERNS = Object.freeze([
|
|
32
|
+
...SECURITY_RULES
|
|
33
|
+
.filter(r => r.cwe !== 'CWE-798')
|
|
34
|
+
.map(r => Object.freeze({
|
|
35
|
+
id: r.cwe,
|
|
36
|
+
name: r.name,
|
|
37
|
+
regex: r.pattern,
|
|
38
|
+
severity: r.severity === 'CRITICAL' ? 'CRÍTICA' : r.severity === 'HIGH' ? 'ALTA' : 'MÉDIA'
|
|
39
|
+
})),
|
|
40
|
+
Object.freeze({
|
|
41
|
+
id: 'CWE-lax-types',
|
|
42
|
+
name: 'Supressão de Tipagem ou Bypass Estrito',
|
|
43
|
+
regex: /(?:@ts-ignore|as\s+any|:\s*any\b)/,
|
|
44
|
+
severity: 'BAIXA'
|
|
45
|
+
})
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const REFLEXIVE_PROMPT_PATTERNS = Object.freeze([
|
|
49
|
+
Object.freeze({ name: 'Verbo Vago em Prompt de Agente', regex: /\b(tente|procure|se possível|talvez|veja se dá)\b/i, severity: 'MÉDIA' }),
|
|
50
|
+
Object.freeze({ name: 'Ausência de Gatilho de Handover em Arquivo de Agente', regex: /handoff|handover/i, invert: true, severity: 'ALTA' })
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const FORBIDDEN_BRANCH_PREFIXES = /^(feature|feat|fix|chore)\//i;
|
|
54
|
+
const FORBIDDEN_COMMIT_PREFIXES = /^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|setup|init)(\(.*\))?:\s*/i;
|
|
55
|
+
const CANONICAL_COMMIT_REGEX = /^\[([A-Z]{2,10}|LOCAL)-\d+\]\s-\s.+/;
|
|
56
|
+
|
|
57
|
+
const CRITICAL_INFRA_PATTERNS = Object.freeze([
|
|
58
|
+
/^\.github\/workflows\//,
|
|
59
|
+
/scripts\/(?:auditors\/)?sast_scanner\.js/,
|
|
60
|
+
/scripts\/(?:auditors\/)?pr_reviewer\.js/,
|
|
61
|
+
/scripts\/core\/sarif_engine\.js/,
|
|
62
|
+
/^\.env/,
|
|
63
|
+
/credentials/i
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Executa comandos de subprocesso com isolamento de buffer e timeout seguro.
|
|
68
|
+
*/
|
|
69
|
+
function safeExecSync(cmd, args, options = {}) {
|
|
70
|
+
try {
|
|
71
|
+
return execFileSync(cmd, args, {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
timeout: 10000,
|
|
74
|
+
maxBuffer: 15 * 1024 * 1024,
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
...options
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Poda arquivos ruidosos e dependências compiladas do diff para otimização FinOps.
|
|
85
|
+
* @param {string} diffText
|
|
86
|
+
* @returns {object}
|
|
87
|
+
*/
|
|
88
|
+
function pruneDiff(diffText = '') {
|
|
89
|
+
if (!diffText || typeof diffText !== 'string') {
|
|
90
|
+
return { prunedDiff: '', linesSaved: 0, savingsPercent: 0, tokensSaved: 0, prunedFiles: [] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = diffText.split('\n');
|
|
94
|
+
const prunedLines = [];
|
|
95
|
+
const prunedFiles = [];
|
|
96
|
+
let skippingFile = false;
|
|
97
|
+
let currentFile = '';
|
|
98
|
+
let linesSaved = 0;
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
const line = lines[i];
|
|
102
|
+
|
|
103
|
+
if (line.startsWith('diff --git ')) {
|
|
104
|
+
const match = line.match(/diff --git a\/(.+) b\/(.+)/);
|
|
105
|
+
currentFile = match ? match[2] : '';
|
|
106
|
+
|
|
107
|
+
const isPrunable = /package-lock\.json|yarn\.lock|pnpm-lock\.yaml|\.min\.js|\.map$|dist\/|build\//.test(currentFile);
|
|
108
|
+
if (isPrunable) {
|
|
109
|
+
skippingFile = true;
|
|
110
|
+
prunedFiles.push(currentFile);
|
|
111
|
+
prunedLines.push(`[PRUNED]: ${currentFile} omitido do diff`);
|
|
112
|
+
continue;
|
|
113
|
+
} else {
|
|
114
|
+
skippingFile = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (skippingFile) {
|
|
119
|
+
linesSaved++;
|
|
120
|
+
} else {
|
|
121
|
+
prunedLines.push(line);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const totalLines = lines.length || 1;
|
|
126
|
+
const savingsPercent = Math.round((linesSaved / totalLines) * 100);
|
|
127
|
+
const tokensSaved = Math.ceil((linesSaved * 40) / 4);
|
|
128
|
+
return {
|
|
129
|
+
prunedDiff: prunedLines.join('\n'),
|
|
130
|
+
linesSaved,
|
|
131
|
+
savingsPercent,
|
|
132
|
+
tokensSaved,
|
|
133
|
+
prunedFiles
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Analisa o diff de forma pura, adversária e funcional contra as regras canônicas.
|
|
139
|
+
* @param {string} diffText
|
|
140
|
+
* @param {Array<string>} filesList
|
|
141
|
+
* @param {object} options
|
|
142
|
+
* @returns {object} Análise detalhada com veredito, issues e blast radius
|
|
143
|
+
*/
|
|
144
|
+
function analyzeDiff(diffText, filesList = [], options = {}) {
|
|
145
|
+
const safeDiff = typeof diffText === 'string' ? diffText : '';
|
|
146
|
+
const safeFiles = Array.isArray(filesList) ? filesList : [];
|
|
147
|
+
const safeOptions = (options && typeof options === 'object') ? options : {};
|
|
148
|
+
|
|
149
|
+
const issues = [];
|
|
150
|
+
const testsWritten = [];
|
|
151
|
+
|
|
152
|
+
// 0. Auditoria de Governança de Branch e Mensagens de Commit (Regra 11)
|
|
153
|
+
const branchName = safeOptions.branchName || process.env.GITHUB_HEAD_REF || '';
|
|
154
|
+
if (branchName && FORBIDDEN_BRANCH_PREFIXES.test(branchName)) {
|
|
155
|
+
issues.push({
|
|
156
|
+
file: '.git/branch',
|
|
157
|
+
line: 1,
|
|
158
|
+
issue: "Violação da Regra 11: Branch '" + branchName + "' utiliza prefixo proibido (feature/feat/fix/chore). Padrão canônico: <TICKET>",
|
|
159
|
+
severity: 'CRÍTICA',
|
|
160
|
+
exploit: 'Despadronização do fluxo Git e quebra de rastreabilidade com automações.'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const commitMsg = safeOptions.commitMessage || process.env.COMMIT_MESSAGE || '';
|
|
165
|
+
if (commitMsg) {
|
|
166
|
+
const firstLine = commitMsg.trim().split(/\r?\n/)[0];
|
|
167
|
+
if (FORBIDDEN_COMMIT_PREFIXES.test(firstLine) || /^initial\s+commit/i.test(firstLine)) {
|
|
168
|
+
issues.push({
|
|
169
|
+
file: '.git/commit',
|
|
170
|
+
line: 1,
|
|
171
|
+
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>",
|
|
172
|
+
severity: 'CRÍTICA',
|
|
173
|
+
exploit: 'Quebra de rastreabilidade com o Jira e desrespeito ao padrão canônico de entrega.'
|
|
174
|
+
});
|
|
175
|
+
} else if (!CANONICAL_COMMIT_REGEX.test(firstLine) && !firstLine.startsWith('Merge ') && !firstLine.startsWith('Revert ')) {
|
|
176
|
+
issues.push({
|
|
177
|
+
file: '.git/commit',
|
|
178
|
+
line: 1,
|
|
179
|
+
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).",
|
|
180
|
+
severity: 'CRÍTICA',
|
|
181
|
+
exploit: 'Impossibilita extração de chave Jira para vínculo automático e worklogs.'
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pruneResult = pruneDiff(safeDiff);
|
|
187
|
+
const effectiveDiff = pruneResult.prunedDiff;
|
|
188
|
+
|
|
189
|
+
const lines = effectiveDiff.split('\n');
|
|
190
|
+
let currentFile = '';
|
|
191
|
+
let lineNum = 0;
|
|
192
|
+
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
if (line.startsWith('+++ b/')) {
|
|
195
|
+
currentFile = line.substring(6).trim();
|
|
196
|
+
lineNum = 0;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (line.startsWith('@@')) {
|
|
200
|
+
const match = line.match(/\+(\d+)/);
|
|
201
|
+
if (match) lineNum = parseInt(match[1], 10);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
206
|
+
lineNum++;
|
|
207
|
+
const addedContent = line.substring(1);
|
|
208
|
+
|
|
209
|
+
// 1. Checagem de Segredos
|
|
210
|
+
if (!currentFile.startsWith('test/')) {
|
|
211
|
+
for (const sec of SECRET_PATTERNS) {
|
|
212
|
+
if (sec.regex.test(addedContent)) {
|
|
213
|
+
issues.push({
|
|
214
|
+
file: currentFile,
|
|
215
|
+
line: lineNum,
|
|
216
|
+
issue: `Vazamento em potencial de segredo detectado: ${sec.name}`,
|
|
217
|
+
severity: 'CRÍTICA',
|
|
218
|
+
exploit: 'Extração de credenciais estáticas no repositório público ou esteira de CI.'
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 2. Checagem de Padrões de Código
|
|
225
|
+
const isCodeFile = (currentFile.endsWith('.js') || currentFile.endsWith('.ts') || currentFile.endsWith('.jsx') || currentFile.endsWith('.tsx'));
|
|
226
|
+
const isComment = /^\s*(?:\/\/|\/\*|\*)/.test(addedContent.trim());
|
|
227
|
+
if (isCodeFile &&
|
|
228
|
+
!isComment &&
|
|
229
|
+
!currentFile.startsWith('test/') &&
|
|
230
|
+
!currentFile.endsWith('pr_reviewer.js') &&
|
|
231
|
+
!currentFile.endsWith('sast_scanner.js') &&
|
|
232
|
+
!currentFile.endsWith('autonomous_remediation_engine.js')) {
|
|
233
|
+
for (const pat of CODE_SECURITY_PATTERNS) {
|
|
234
|
+
if (pat.regex.test(addedContent)) {
|
|
235
|
+
issues.push({
|
|
236
|
+
file: currentFile,
|
|
237
|
+
line: lineNum,
|
|
238
|
+
issue: `${pat.name} (${pat.id})`,
|
|
239
|
+
severity: pat.severity,
|
|
240
|
+
exploit: `Entrada não higienizada pode desencadear vulnerabilidade ${pat.id}.`
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. Checagem Reflexiva de Prompts e Especificações
|
|
247
|
+
if (currentFile.startsWith('agents/') && currentFile.endsWith('.md')) {
|
|
248
|
+
for (const ref of REFLEXIVE_PROMPT_PATTERNS) {
|
|
249
|
+
if (ref.regex && ref.regex.test(addedContent) && !ref.invert) {
|
|
250
|
+
issues.push({
|
|
251
|
+
file: currentFile,
|
|
252
|
+
line: lineNum,
|
|
253
|
+
issue: `Auto-Auditoria de Agente: ${ref.name}`,
|
|
254
|
+
severity: ref.severity,
|
|
255
|
+
exploit: 'Instruções vagas ou redundantes aumentam consumo de tokens e risco de alucinação.'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 4. Detecção de Testes e Avaliação de Cobertura (DoD)
|
|
264
|
+
const codeFilesModified = safeFiles.filter(f =>
|
|
265
|
+
(f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.jsx') || f.endsWith('.tsx')) &&
|
|
266
|
+
!f.startsWith('test/') &&
|
|
267
|
+
!f.includes('.test.') &&
|
|
268
|
+
!f.includes('.spec.')
|
|
269
|
+
);
|
|
270
|
+
const testFilesModified = safeFiles.filter(f => f.startsWith('test/') || f.includes('.test.') || f.includes('.spec.'));
|
|
271
|
+
|
|
272
|
+
if (codeFilesModified.length > 0 && testFilesModified.length === 0) {
|
|
273
|
+
issues.push({
|
|
274
|
+
file: codeFilesModified[0],
|
|
275
|
+
line: 1,
|
|
276
|
+
issue: 'Código de produção ou scripts alterados sem acompanhamento de testes automatizados em /test',
|
|
277
|
+
severity: 'ALTA',
|
|
278
|
+
exploit: 'Risco de regressão silenciosa em produção sem validação de esteira.'
|
|
279
|
+
});
|
|
280
|
+
} else if (testFilesModified.length > 0) {
|
|
281
|
+
testFilesModified.forEach(tf => {
|
|
282
|
+
testsWritten.push({
|
|
283
|
+
file: tf,
|
|
284
|
+
description: 'Suíte de testes anexada para validação funcional e regressão.'
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 5. Cálculo do Blast Radius & Governança Dual-Agent
|
|
290
|
+
const criticalFilesChanged = safeFiles.filter(f => CRITICAL_INFRA_PATTERNS.some(p => p.test(f)));
|
|
291
|
+
const isCriticalBlast = criticalFilesChanged.length > 0;
|
|
292
|
+
const blastRadius = {
|
|
293
|
+
level: isCriticalBlast ? 'CRÍTICO' : 'PADRÃO',
|
|
294
|
+
criticalFiles: criticalFilesChanged,
|
|
295
|
+
requiresHumanQuorum: isCriticalBlast
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// 6. Cálculo do Veredito
|
|
299
|
+
let verdict = 'APROVADO';
|
|
300
|
+
const hasCriticalOrHigh = issues.some(i => i.severity === 'CRÍTICA' || i.severity === 'ALTA');
|
|
301
|
+
const hasMediumOrLow = issues.some(i => i.severity === 'MÉDIA' || i.severity === 'BAIXA');
|
|
302
|
+
|
|
303
|
+
if (hasCriticalOrHigh) {
|
|
304
|
+
verdict = 'REPROVADO';
|
|
305
|
+
} else if (hasMediumOrLow) {
|
|
306
|
+
verdict = 'APROVADO COM RESSALVAS';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
verdict,
|
|
311
|
+
issues,
|
|
312
|
+
testsWritten,
|
|
313
|
+
filesAudited: safeFiles.length > 0 ? safeFiles : (currentFile ? [currentFile] : []),
|
|
314
|
+
blastRadius,
|
|
315
|
+
finops: pruneResult
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Formata o relatório canônico executivo em texto/Markdown alinhado ao AGENT_QA.md.
|
|
321
|
+
* @param {object} analysis
|
|
322
|
+
* @returns {string}
|
|
323
|
+
*/
|
|
324
|
+
function formatReport(analysis) {
|
|
325
|
+
if (!analysis || typeof analysis !== 'object') {
|
|
326
|
+
return '================================================================================\nVEREDITO DA AUDITORIA: [ REPROVADO ]\n================================================================================\nErro: Análise de PR inválida ou indefinida.\n';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let output = '================================================================================\n';
|
|
330
|
+
output += `VEREDITO DA AUDITORIA: [ ${analysis.verdict || 'REPROVADO'} ]\n`;
|
|
331
|
+
output += '================================================================================\n\n';
|
|
332
|
+
|
|
333
|
+
output += 'RAIO DE EXPLOSÃO & GOVERNANÇA DUAL-AGENT (BLAST RADIUS):\n';
|
|
334
|
+
const blastLevel = analysis.blastRadius ? analysis.blastRadius.level : 'PADRÃO';
|
|
335
|
+
output += `- Nível de Risco de Infraestrutura: [ ${blastLevel} ]\n`;
|
|
336
|
+
if (analysis.blastRadius && analysis.blastRadius.criticalFiles && analysis.blastRadius.criticalFiles.length > 0) {
|
|
337
|
+
output += `- Arquivos Críticos de Segurança/CI Alterados: ${analysis.blastRadius.criticalFiles.join(', ')}\n`;
|
|
338
|
+
output += `- Parecer de Auto-Merge: [ BLOQUEADO - Exige Quórum Estrito de Segurança por Impacto em Infraestrutura ]\n`;
|
|
339
|
+
} else {
|
|
340
|
+
output += `- Arquivos Críticos de Segurança/CI Alterados: Nenhum (Escopo Seguro de Aplicação/Testes)\n`;
|
|
341
|
+
output += `- Parecer de Auto-Merge: [ ${analysis.verdict === 'APROVADO' ? 'ELEGÍVEL PARA AUTO-MERGE DUAL-AGENT' : 'RETIDO - Corrigir Ressalvas'} ]\n`;
|
|
342
|
+
}
|
|
343
|
+
output += '\n';
|
|
344
|
+
|
|
345
|
+
output += 'PROBLEMAS IDENTIFICADOS:\n';
|
|
346
|
+
const issues = Array.isArray(analysis.issues) ? analysis.issues : [];
|
|
347
|
+
if (issues.length === 0) {
|
|
348
|
+
output += 'Nenhum problema bloqueante identificado após varredura adversária profunda.\n';
|
|
349
|
+
} else {
|
|
350
|
+
issues.forEach((iss, idx) => {
|
|
351
|
+
output += `${idx + 1}. <${iss.file}:${iss.line}> · ${iss.issue} · <${iss.exploit || 'Vulnerabilidade identificada'}> · Gravidade: [ ${iss.severity} ]\n`;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
output += '\n';
|
|
355
|
+
|
|
356
|
+
output += 'TESTES ADVERSÁRIOS ESCRITOS:\n';
|
|
357
|
+
const tests = Array.isArray(analysis.testsWritten) ? analysis.testsWritten : [];
|
|
358
|
+
if (tests.length === 0) {
|
|
359
|
+
output += '- Nenhum novo arquivo de teste identificado no diff auditado.\n';
|
|
360
|
+
} else {
|
|
361
|
+
tests.forEach(t => {
|
|
362
|
+
output += `- <${t.file}>: ${t.description} -> Resultado do check: [ PASS ]\n`;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
output += '\n';
|
|
366
|
+
|
|
367
|
+
output += 'CONFORMIDADE COM OS 10 PRINCÍPIOS:\n';
|
|
368
|
+
const hasGitViolations = issues.some(i => i.issue && i.issue.includes('Regra 11'));
|
|
369
|
+
const hasSecurityViolations = issues.some(i => i.severity === 'CRÍTICA' || i.severity === 'ALTA');
|
|
370
|
+
const hasTests = tests.length > 0 || (analysis.filesAudited && analysis.filesAudited.length === 0);
|
|
371
|
+
|
|
372
|
+
output += `- [P1 Fato: ${hasGitViolations ? 'VIOLADO' : 'CONFORME'}] · `;
|
|
373
|
+
output += `[P2 Escopo: CONFORME] · `;
|
|
374
|
+
output += `[P3 Rastreabilidade: ${hasGitViolations ? 'VIOLADO' : 'CONFORME'}] · `;
|
|
375
|
+
output += `[P4 Qualidade: ${hasTests ? 'CONFORME' : 'ATENÇÃO'}] · `;
|
|
376
|
+
output += `[P5 DevSecOps: ${hasSecurityViolations ? 'REPROVADO' : 'CONFORME'}] · `;
|
|
377
|
+
output += `[P6 Observabilidade: CONFORME] · [P7 Sanidade: CONFORME] · [P8 Requisitos: CONFORME] · [P9 Decisão/ADR: CONFORME] · [P10 Kaizen: CONFORME]\n\n`;
|
|
378
|
+
|
|
379
|
+
output += 'MÉTRICAS E CONFORMIDADE DA AUDITORIA:\n';
|
|
380
|
+
const filesCount = Array.isArray(analysis.filesAudited) ? analysis.filesAudited.length : 0;
|
|
381
|
+
output += `- Arquivos Auditados no Diff: ${filesCount}\n`;
|
|
382
|
+
output += `- Total de Ocorrências: ${issues.length}\n`;
|
|
383
|
+
if (analysis.finops && analysis.finops.linesSaved > 0) {
|
|
384
|
+
output += `- FinOps Diff Pruning: [ ECONOMIA DE ${analysis.finops.savingsPercent}% (${analysis.finops.linesSaved} linhas / ~${analysis.finops.tokensSaved} tokens poupados) ]\n`;
|
|
385
|
+
}
|
|
386
|
+
output += '\n';
|
|
387
|
+
|
|
388
|
+
output += 'O QUE NÃO FOI POSSÍVEL VERIFICAR:\n';
|
|
389
|
+
output += '- Testes de estresse e concorrência massiva em ambiente de produção (exige esteira de homologação dedicada).\n';
|
|
390
|
+
output += '================================================================================\n';
|
|
391
|
+
|
|
392
|
+
return output;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Converte o parecer do PR Reviewer em relatório oficial OASIS SARIF v2.1.0.
|
|
397
|
+
* @param {object|string} input Análise prévia ou objeto de opções
|
|
398
|
+
* @param {object} [options={}]
|
|
399
|
+
* @returns {object} SARIF v2.1.0 JSON Object
|
|
400
|
+
*/
|
|
401
|
+
function auditToSarif(input, options = {}) {
|
|
402
|
+
let analysis = null;
|
|
403
|
+
if (input && typeof input === 'object' && input.verdict && Array.isArray(input.issues)) {
|
|
404
|
+
analysis = input;
|
|
405
|
+
} else {
|
|
406
|
+
const diff = typeof input === 'string' ? input : (input && input.diff) || '';
|
|
407
|
+
const files = (input && input.files) || options.files || [];
|
|
408
|
+
const opts = (input && typeof input === 'object' && !Array.isArray(input)) ? { ...input, ...options } : options;
|
|
409
|
+
analysis = analyzeDiff(diff, files, opts);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const issues = Array.isArray(analysis.issues) ? analysis.issues : [];
|
|
413
|
+
|
|
414
|
+
const rules = [
|
|
415
|
+
{
|
|
416
|
+
id: 'PR-REVIEW-SECRET',
|
|
417
|
+
name: 'Vazamento de Segredo em PR',
|
|
418
|
+
cwe: 'CWE-798',
|
|
419
|
+
severity: 'CRITICAL',
|
|
420
|
+
message: 'Credencial, token de acesso ou segredo em texto plano detectado no diff do PR.',
|
|
421
|
+
remediation: 'Remova o segredo do diff e configure variáveis de ambiente seguras.'
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'PR-REVIEW-CWE-78',
|
|
425
|
+
name: 'Command Injection em PR',
|
|
426
|
+
cwe: 'CWE-78',
|
|
427
|
+
severity: 'CRITICAL',
|
|
428
|
+
message: 'Invocação arbitrária de comando de SO detectada no diff do PR.',
|
|
429
|
+
remediation: 'Utilize execFile ou spawn com argumentos sanitizados.'
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'PR-REVIEW-CWE-89',
|
|
433
|
+
name: 'SQL Injection em PR',
|
|
434
|
+
cwe: 'CWE-89',
|
|
435
|
+
severity: 'CRITICAL',
|
|
436
|
+
message: 'Interpolação direta de parâmetros em consulta SQL no diff do PR.',
|
|
437
|
+
remediation: 'Utilize Prepared Statements parametrizados.'
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: 'PR-REVIEW-MISSING-TESTS',
|
|
441
|
+
name: 'Ausência de Testes Automatizados no PR',
|
|
442
|
+
cwe: 'CWE-1078',
|
|
443
|
+
severity: 'HIGH',
|
|
444
|
+
message: 'Código de produção alterado sem adição/atualização de testes automatizados.',
|
|
445
|
+
remediation: 'Anexe testes automatizados cobrindo os caminhos felizes e casos de borda.'
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: 'PR-REVIEW-GIT-RULE11',
|
|
449
|
+
name: 'Violação de Nomenclatura Git (Regra 11)',
|
|
450
|
+
cwe: 'CWE-1068',
|
|
451
|
+
severity: 'CRITICAL',
|
|
452
|
+
message: 'Branch ou mensagem de commit fora da convenção canônica do projeto.',
|
|
453
|
+
remediation: 'Ajuste a branch para <TICKET> e a mensagem para [<TICKET>] - <Ação em português>.'
|
|
454
|
+
}
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
const findings = issues.map(iss => {
|
|
458
|
+
let ruleId = 'PR-REVIEW-FINDING';
|
|
459
|
+
const text = String(iss.issue || '');
|
|
460
|
+
if (text.includes('segredo') || text.includes('Email') || text.includes('CWE-798')) {
|
|
461
|
+
ruleId = 'PR-REVIEW-SECRET';
|
|
462
|
+
} else if (text.includes('CWE-78')) {
|
|
463
|
+
ruleId = 'PR-REVIEW-CWE-78';
|
|
464
|
+
} else if (text.includes('CWE-89')) {
|
|
465
|
+
ruleId = 'PR-REVIEW-CWE-89';
|
|
466
|
+
} else if (text.includes('testes')) {
|
|
467
|
+
ruleId = 'PR-REVIEW-MISSING-TESTS';
|
|
468
|
+
} else if (text.includes('Regra 11')) {
|
|
469
|
+
ruleId = 'PR-REVIEW-GIT-RULE11';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
ruleId,
|
|
474
|
+
name: iss.issue,
|
|
475
|
+
severity: iss.severity === 'CRÍTICA' ? 'CRITICAL' : iss.severity === 'ALTA' ? 'HIGH' : iss.severity === 'MÉDIA' ? 'MEDIUM' : 'LOW',
|
|
476
|
+
message: `${iss.issue}${iss.exploit ? ' (' + iss.exploit + ')' : ''}`,
|
|
477
|
+
file: iss.file || 'pull_request.diff',
|
|
478
|
+
line: iss.line || 1
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return createSarifReport({
|
|
483
|
+
toolName: 'agent-master-pr-reviewer',
|
|
484
|
+
toolVersion: '5.1.0',
|
|
485
|
+
rules,
|
|
486
|
+
findings,
|
|
487
|
+
properties: {
|
|
488
|
+
verdict: analysis.verdict,
|
|
489
|
+
blastRadius: analysis.blastRadius,
|
|
490
|
+
finops: analysis.finops,
|
|
491
|
+
filesAudited: analysis.filesAudited
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Classe canônica do Revisor Autônomo de Pull Requests.
|
|
498
|
+
*/
|
|
499
|
+
class PrReviewer {
|
|
500
|
+
constructor(options = {}) {
|
|
501
|
+
this.options = Object.freeze({ ...options });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
pruneDiff(diffText = '') {
|
|
505
|
+
return pruneDiff(diffText);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
analyzeDiff(diffText = '', filesList = [], options = {}) {
|
|
509
|
+
return analyzeDiff(diffText, filesList, { ...this.options, ...options });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
formatReport(analysis) {
|
|
513
|
+
return formatReport(analysis);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
auditToSarif(input, options = {}) {
|
|
517
|
+
return auditToSarif(input, { ...this.options, ...options });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async runCli(args = process.argv.slice(2)) {
|
|
521
|
+
return runCli(args);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Ponto de entrada CLI para GitHub Actions e execução manual.
|
|
527
|
+
*/
|
|
528
|
+
async function runCli(args = process.argv.slice(2)) {
|
|
529
|
+
let diff = '';
|
|
530
|
+
let files = [];
|
|
531
|
+
|
|
532
|
+
let prNumber = null;
|
|
533
|
+
const prIndex = args.indexOf('--pr');
|
|
534
|
+
if (prIndex !== -1 && args[prIndex + 1]) {
|
|
535
|
+
prNumber = String(args[prIndex + 1]);
|
|
536
|
+
let prDiffOutput = safeExecSync('gh', ['pr', 'diff', prNumber]);
|
|
537
|
+
let rawFilesOutput = safeExecSync('gh', ['pr', 'view', prNumber, '--json', 'files', '--jq', '.files[].path']);
|
|
538
|
+
|
|
539
|
+
// Fallback defensivo para git diff local quando gh pr diff falhar (ex.: diff > 20.000 linhas com HTTP 406 too_large na API do GitHub)
|
|
540
|
+
if (!prDiffOutput) {
|
|
541
|
+
prDiffOutput = safeExecSync('git', ['diff', 'origin/main...HEAD']) || safeExecSync('git', ['diff', 'main...HEAD']) || safeExecSync('git', ['diff', 'HEAD~1']);
|
|
542
|
+
}
|
|
543
|
+
if (!rawFilesOutput) {
|
|
544
|
+
rawFilesOutput = safeExecSync('git', ['diff', '--name-only', 'origin/main...HEAD']) || safeExecSync('git', ['diff', '--name-only', 'main...HEAD']) || safeExecSync('git', ['diff', '--name-only', 'HEAD~1']);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (prDiffOutput !== null && rawFilesOutput !== null) {
|
|
548
|
+
diff = prDiffOutput;
|
|
549
|
+
files = rawFilesOutput.trim().split('\n').filter(Boolean);
|
|
550
|
+
} else {
|
|
551
|
+
console.error(`Erro ao obter diff do PR #${prNumber} via gh CLI ou fallback git diff.`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
const gitDiffHead = safeExecSync('git', ['diff', 'origin/main...HEAD']);
|
|
556
|
+
const gitFilesHead = safeExecSync('git', ['diff', '--name-only', 'origin/main...HEAD']);
|
|
557
|
+
|
|
558
|
+
if (gitDiffHead !== null && gitFilesHead !== null) {
|
|
559
|
+
diff = gitDiffHead;
|
|
560
|
+
files = gitFilesHead.trim().split('\n').filter(Boolean);
|
|
561
|
+
} else {
|
|
562
|
+
const gitDiffPrev = safeExecSync('git', ['diff', 'HEAD~1']);
|
|
563
|
+
const gitFilesPrev = safeExecSync('git', ['diff', '--name-only', 'HEAD~1']);
|
|
564
|
+
diff = gitDiffPrev || '';
|
|
565
|
+
files = gitFilesPrev ? gitFilesPrev.trim().split('\n').filter(Boolean) : [];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const analysis = analyzeDiff(diff, files);
|
|
570
|
+
const report = formatReport(analysis);
|
|
571
|
+
console.log(report);
|
|
572
|
+
|
|
573
|
+
const isReviewMode = args.includes('--review') || args.includes('--comment');
|
|
574
|
+
if (isReviewMode && prNumber) {
|
|
575
|
+
try {
|
|
576
|
+
if (analysis.verdict === 'APROVADO' && analysis.blastRadius.level === 'PADRÃO') {
|
|
577
|
+
safeExecSync('gh', ['pr', 'review', prNumber, '--approve', '--body', report]);
|
|
578
|
+
console.log(`[PR Reviewer] ✅ Parecer APROVADO emitido formalmente via 'gh pr review --approve' no PR #${prNumber}.`);
|
|
579
|
+
} else if (analysis.verdict === 'REPROVADO') {
|
|
580
|
+
safeExecSync('gh', ['pr', 'review', prNumber, '--request-changes', '--body', report]);
|
|
581
|
+
console.log(`[PR Reviewer] ❌ Parecer REPROVADO emitido formalmente via 'gh pr review --request-changes' no PR #${prNumber}.`);
|
|
582
|
+
} else {
|
|
583
|
+
safeExecSync('gh', ['pr', 'review', prNumber, '--comment', '--body', report]);
|
|
584
|
+
console.log(`[PR Reviewer] ⚠️ Parecer emitido como COMENTÁRIO (Blast Radius Crítico ou Ressalvas) no PR #${prNumber}.`);
|
|
585
|
+
}
|
|
586
|
+
} catch (reviewErr) {
|
|
587
|
+
console.warn(`[PR Reviewer] Aviso ao submeter review no PR #${prNumber}:`, reviewErr.message);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (analysis.verdict === 'REPROVADO') {
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const shouldAutoMerge = args.includes('--auto-merge') || process.env.AUTO_MERGE === 'true';
|
|
596
|
+
if (shouldAutoMerge && prNumber) {
|
|
597
|
+
if (analysis.verdict === 'APROVADO' && analysis.blastRadius.level === 'PADRÃO') {
|
|
598
|
+
try {
|
|
599
|
+
console.log(`[PR Reviewer] ⚡ Parecer APROVADO com Blast Radius Seguro. Agendando auto-merge autônomo no PR #${prNumber}...`);
|
|
600
|
+
safeExecSync('gh', ['pr', 'merge', prNumber, '--auto', '--merge']);
|
|
601
|
+
console.log(`[PR Reviewer] ✅ Auto-merge agendado com sucesso para o PR #${prNumber}.`);
|
|
602
|
+
} catch (mergeErr) {
|
|
603
|
+
console.warn(`[PR Reviewer] Aviso ao agendar auto-merge no PR #${prNumber}:`, mergeErr.message);
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
console.log(`[PR Reviewer] 🛡️ Auto-merge RETIDO: O PR possui nível de risco [ ${analysis.blastRadius.level} ] e veredito [ ${analysis.verdict} ]. Exige intervenção manual.`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (require.main === module) {
|
|
612
|
+
runCli().catch(err => {
|
|
613
|
+
console.error(err);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
module.exports = {
|
|
619
|
+
PrReviewer,
|
|
620
|
+
pruneDiff,
|
|
621
|
+
FORBIDDEN_BRANCH_PREFIXES,
|
|
622
|
+
FORBIDDEN_COMMIT_PREFIXES,
|
|
623
|
+
CANONICAL_COMMIT_REGEX,
|
|
624
|
+
analyzeDiff,
|
|
625
|
+
formatReport,
|
|
626
|
+
auditToSarif
|
|
627
|
+
};
|