agent-master-devsecops 0.0.0-stage → 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 +204 -2
- package/agents/AGENT_DEVSECOPS.md +32 -0
- package/agents/AGENT_INFRA.md +31 -0
- package/agents/AGENT_ORCHESTRATOR.md +39 -0
- package/agents/AGENT_QA.md +58 -0
- package/agents/AGENT_SCRUM.md +23 -0
- package/agents/AGENT_SRE.md +22 -0
- package/agents/AGENT_UIUX.md +26 -0
- package/docs/ARCHITECTURE.md +152 -0
- package/docs/CALCULATIONS.md +17 -0
- package/docs/CHANGELOG.md +112 -0
- package/docs/ENGINEERING_RULES.md +56 -0
- package/docs/IDEAS.md +17 -0
- package/docs/LOCAL_BACKLOG.md +20 -0
- package/docs/STATE.md +111 -0
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
- package/package.json +57 -4
- 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/bin/bootstrap_cli.js +597 -0
- package/scripts/bin/domain_idea_miner.js +658 -0
- package/scripts/bin/smart_onboarding.js +905 -0
- package/scripts/bin/web_inspector.js +578 -0
- 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/core/circuit_breaker.js +546 -0
- 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/core/magic_prompt_generator.js +250 -0
- package/scripts/core/multi_agent_orchestrator.js +421 -0
- package/scripts/core/otel_tracer.js +335 -0
- package/scripts/core/perf_benchmark.js +212 -0
- package/scripts/core/sarif_engine.js +299 -0
- package/scripts/core/sre_remediation_engine.js +236 -0
- package/scripts/core/universal_packager.js +357 -0
- 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 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/integrations/atlassian_sync_action.js
|
|
4
|
+
*
|
|
5
|
+
* Sincronizador Automático Git ↔ Atlassian via Actions & Local (AGDEV-23 / AGDEV-35).
|
|
6
|
+
* Conecta automaticamente commits, branches, Pull Requests e páginas do Confluence
|
|
7
|
+
* aos cards do Jira utilizando a API oficial de Remote Links do Jira Cloud (REST API v3).
|
|
8
|
+
*
|
|
9
|
+
* Governança: Tríade Git + Jira + Confluence (Regra 11 e Regra 15).
|
|
10
|
+
* Conformidade: OASIS SARIF v2.1.0, Programação Defensiva e Clean Code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { execFileSync } = require('node:child_process');
|
|
16
|
+
const {
|
|
17
|
+
getAtlassianCredentials,
|
|
18
|
+
getGitMetadata,
|
|
19
|
+
getGitRepositoryUrl,
|
|
20
|
+
requestAtlassian,
|
|
21
|
+
AtlassianClient
|
|
22
|
+
} = require('./atlassian_client.js');
|
|
23
|
+
|
|
24
|
+
const { validateRule15 } = require('./credentials_guard.js');
|
|
25
|
+
const { createSarifReport } = require('../core/sarif_engine.js');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Obtém a credencial em base64 da Atlassian para compatibilidade retroativa.
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
function getAuth() {
|
|
32
|
+
const cfg = getAtlassianCredentials();
|
|
33
|
+
return cfg.authBase64;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extrai chaves de tickets Jira (ex: AGDEV-123, PROJ-456) a partir de um texto.
|
|
38
|
+
* Case-insensitive na leitura com normalização em maiúsculas e deduplicação.
|
|
39
|
+
* @param {string} text
|
|
40
|
+
* @returns {Array<string>}
|
|
41
|
+
*/
|
|
42
|
+
function extractIssueKeys(text) {
|
|
43
|
+
if (!text || typeof text !== 'string') return [];
|
|
44
|
+
const matches = text.match(/\b[A-Za-z]{2,10}-\d+\b/g) || [];
|
|
45
|
+
return [...new Set(matches.map(k => k.toUpperCase()))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extrai o identificador numérico de um Pull Request (#123, pull request #123 ou URL /pull/123).
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
function extractPrNumber(text) {
|
|
54
|
+
if (!text || typeof text !== 'string') return null;
|
|
55
|
+
const match = text.match(/#(\d+)\b/) ||
|
|
56
|
+
text.match(/pull request #(\d+)/i) ||
|
|
57
|
+
text.match(/\/pull\/(\d+)\b/i);
|
|
58
|
+
return match ? match[1] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Formata o payload canônico de Remote Link para a API v3 do Jira Cloud.
|
|
63
|
+
* @param {string} url
|
|
64
|
+
* @param {string} title
|
|
65
|
+
* @param {string} [type='commit']
|
|
66
|
+
* @param {string|null} [globalId=null]
|
|
67
|
+
* @param {string|null} [summary=null]
|
|
68
|
+
* @returns {object}
|
|
69
|
+
*/
|
|
70
|
+
function formatRemoteLinkPayload(url, title, type = 'commit', globalId = null, summary = null) {
|
|
71
|
+
const safeType = (typeof type === 'string') ? type.toLowerCase() : 'commit';
|
|
72
|
+
let iconUrl = 'https://github.githubassets.com/favicons/favicon.png';
|
|
73
|
+
let iconTitle = `GitHub ${safeType.toUpperCase()}`;
|
|
74
|
+
let relationship = 'causes';
|
|
75
|
+
|
|
76
|
+
if (safeType === 'pr') {
|
|
77
|
+
relationship = 'relates to';
|
|
78
|
+
} else if (safeType === 'branch') {
|
|
79
|
+
relationship = 'builds';
|
|
80
|
+
} else if (safeType === 'confluence' || safeType === 'wiki') {
|
|
81
|
+
relationship = undefined;
|
|
82
|
+
iconUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/confluence.png';
|
|
83
|
+
iconTitle = 'Confluence Page';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const payload = {
|
|
87
|
+
object: {
|
|
88
|
+
url: typeof url === 'string' ? url : '',
|
|
89
|
+
title: typeof title === 'string' ? title : '',
|
|
90
|
+
icon: {
|
|
91
|
+
url16x16: iconUrl,
|
|
92
|
+
title: iconTitle
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (relationship) {
|
|
98
|
+
payload.relationship = relationship;
|
|
99
|
+
}
|
|
100
|
+
if (globalId) {
|
|
101
|
+
payload.globalId = globalId;
|
|
102
|
+
}
|
|
103
|
+
if (summary) {
|
|
104
|
+
payload.object.summary = summary;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Vincula uma página do Confluence como Remote Link em uma issue do Jira.
|
|
112
|
+
* @param {string} issueKey
|
|
113
|
+
* @param {string} pageUrl
|
|
114
|
+
* @param {string} pageTitle
|
|
115
|
+
* @param {string|null} [summary=null]
|
|
116
|
+
* @returns {Promise<object>}
|
|
117
|
+
*/
|
|
118
|
+
async function linkConfluencePageToJira(issueKey, pageUrl, pageTitle, summary = null) {
|
|
119
|
+
const atlassian = getAtlassianCredentials();
|
|
120
|
+
const globalId = `system=confluence&pageUrl=${encodeURIComponent(pageUrl)}`;
|
|
121
|
+
return await linkUrlToJira(
|
|
122
|
+
issueKey,
|
|
123
|
+
pageUrl,
|
|
124
|
+
pageTitle,
|
|
125
|
+
'confluence',
|
|
126
|
+
atlassian.authBase64,
|
|
127
|
+
globalId,
|
|
128
|
+
summary
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Registra um Remote Link genérico (commit, branch, PR ou doc) em uma issue do Jira.
|
|
134
|
+
* @param {string} issueKey
|
|
135
|
+
* @param {string} url
|
|
136
|
+
* @param {string} title
|
|
137
|
+
* @param {string} [type='commit']
|
|
138
|
+
* @param {string|null} [authOverride=null]
|
|
139
|
+
* @param {string|null} [globalId=null]
|
|
140
|
+
* @param {string|null} [summary=null]
|
|
141
|
+
* @returns {Promise<object>}
|
|
142
|
+
*/
|
|
143
|
+
async function linkUrlToJira(issueKey, url, title, type = 'commit', authOverride = null, globalId = null, summary = null) {
|
|
144
|
+
const atlassian = getAtlassianCredentials();
|
|
145
|
+
const auth = (authOverride !== null && authOverride !== undefined) ? authOverride : atlassian.authBase64;
|
|
146
|
+
if (!auth) {
|
|
147
|
+
return { status: 'FAILED', message: 'Credenciais Atlassian não configuradas no ambiente (Regra 15).' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cleanDomain = atlassian.domain;
|
|
151
|
+
if (!cleanDomain) {
|
|
152
|
+
return { status: 'FAILED', message: 'Domínio Atlassian não configurado para este projeto (Regra 15).' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const payload = formatRemoteLinkPayload(url, title, type, globalId, summary);
|
|
156
|
+
const endpoint = `/rest/api/3/issue/${encodeURIComponent(issueKey)}/remotelink`;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const creds = {
|
|
160
|
+
domain: cleanDomain,
|
|
161
|
+
auth,
|
|
162
|
+
authBase64: auth
|
|
163
|
+
};
|
|
164
|
+
const res = await requestAtlassian(creds, 'POST', endpoint, payload);
|
|
165
|
+
|
|
166
|
+
if (res.status === 200 || res.status === 201) {
|
|
167
|
+
return { status: 'SUCCESS', issueKey, url, type, httpStatus: res.status };
|
|
168
|
+
} else {
|
|
169
|
+
const errText = typeof res.body === 'object' ? JSON.stringify(res.body) : String(res.body || '');
|
|
170
|
+
return { status: 'FAILED', issueKey, error: `HTTP ${res.status}: ${errText}` };
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { status: 'ERROR', issueKey, error: err.message };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sincroniza o contexto atual de Git (commit HEAD, branch atual e PR se detectado).
|
|
179
|
+
* @param {string} [targetDir=process.cwd()]
|
|
180
|
+
* @returns {Promise<object>}
|
|
181
|
+
*/
|
|
182
|
+
async function syncCurrentContext(targetDir = process.cwd()) {
|
|
183
|
+
const atlassian = getAtlassianCredentials(targetDir);
|
|
184
|
+
const gitMeta = getGitMetadata();
|
|
185
|
+
const repoUrl = gitMeta.repoUrl || 'https://github.com/ContalApps/agent-master-devsecops';
|
|
186
|
+
const repoSlug = repoUrl.replace('https://github.com/', '');
|
|
187
|
+
|
|
188
|
+
const sha = gitMeta.sha;
|
|
189
|
+
const branch = gitMeta.branch;
|
|
190
|
+
const message = gitMeta.message;
|
|
191
|
+
const prNumber = process.env.PR_NUMBER || extractPrNumber(process.env.PR_TITLE || '') || extractPrNumber(message);
|
|
192
|
+
|
|
193
|
+
const combined = `${branch} ${message} ${process.env.PR_TITLE || ''}`;
|
|
194
|
+
const keys = extractIssueKeys(combined);
|
|
195
|
+
|
|
196
|
+
if (keys.length === 0) {
|
|
197
|
+
return {
|
|
198
|
+
synced: false,
|
|
199
|
+
keys: [],
|
|
200
|
+
reason: 'Nenhuma chave Jira encontrada na branch ou mensagem de commit.',
|
|
201
|
+
results: []
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const results = [];
|
|
206
|
+
|
|
207
|
+
for (const key of keys) {
|
|
208
|
+
if (sha && repoUrl) {
|
|
209
|
+
const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
|
|
210
|
+
const commitRes = await linkUrlToJira(
|
|
211
|
+
key,
|
|
212
|
+
`${repoUrl}/commit/${sha}`,
|
|
213
|
+
`GitHub Commit: ${sha.slice(0, 7)} (${message})`,
|
|
214
|
+
'commit',
|
|
215
|
+
atlassian.authBase64,
|
|
216
|
+
commitGlobalId
|
|
217
|
+
);
|
|
218
|
+
results.push({ key, type: 'commit', ref: sha.slice(0, 7), status: commitRes.status, detail: commitRes });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (branch && branch !== 'main' && branch !== 'HEAD' && repoUrl) {
|
|
222
|
+
const branchGlobalId = `system=github&repo=${repoSlug}&type=branch&name=${branch}`;
|
|
223
|
+
const branchRes = await linkUrlToJira(
|
|
224
|
+
key,
|
|
225
|
+
`${repoUrl}/tree/${branch}`,
|
|
226
|
+
`GitHub Branch: ${branch}`,
|
|
227
|
+
'branch',
|
|
228
|
+
atlassian.authBase64,
|
|
229
|
+
branchGlobalId
|
|
230
|
+
);
|
|
231
|
+
results.push({ key, type: 'branch', ref: branch, status: branchRes.status, detail: branchRes });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (prNumber && repoUrl) {
|
|
235
|
+
const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
|
|
236
|
+
const prRes = await linkUrlToJira(
|
|
237
|
+
key,
|
|
238
|
+
`${repoUrl}/pull/${prNumber}`,
|
|
239
|
+
`GitHub Pull Request #${prNumber}`,
|
|
240
|
+
'pr',
|
|
241
|
+
atlassian.authBase64,
|
|
242
|
+
prGlobalId,
|
|
243
|
+
process.env.PR_TITLE || message
|
|
244
|
+
);
|
|
245
|
+
results.push({ key, type: 'pr', ref: `#${prNumber}`, status: prRes.status, detail: prRes });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
synced: true,
|
|
251
|
+
keys,
|
|
252
|
+
results
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Executa a sincronização retroativa (backfill) de commits recentes.
|
|
258
|
+
* @param {number} [limit=30]
|
|
259
|
+
* @returns {Promise<Array<object>>}
|
|
260
|
+
*/
|
|
261
|
+
async function backfillHistory(limit = 30) {
|
|
262
|
+
console.log(`[Atlassian Sync] Iniciando sincronização retroativa dos últimos ${limit} commits...`);
|
|
263
|
+
const atlassian = getAtlassianCredentials();
|
|
264
|
+
if (!atlassian.isConfigured) {
|
|
265
|
+
throw new Error('Credenciais Atlassian ausentes para sincronização retroativa.');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const repoUrl = getGitRepositoryUrl();
|
|
269
|
+
const repoSlug = repoUrl.replace('https://github.com/', '');
|
|
270
|
+
|
|
271
|
+
let rawLog = '';
|
|
272
|
+
try {
|
|
273
|
+
rawLog = execFileSync('git', ['log', `-${limit}`, '--format=%H|%s'], { encoding: 'utf8' });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error('[Atlassian Sync] Falha ao extrair histórico git:', err.message);
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const lines = rawLog.split('\n').filter(l => l.trim().length > 0);
|
|
280
|
+
const results = [];
|
|
281
|
+
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
const [sha, ...subjectParts] = line.split('|');
|
|
284
|
+
const subject = subjectParts.join('|');
|
|
285
|
+
const keys = extractIssueKeys(subject);
|
|
286
|
+
const prNumber = extractPrNumber(subject);
|
|
287
|
+
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
// 1. Link Commit
|
|
290
|
+
const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
|
|
291
|
+
const commitRes = await linkUrlToJira(
|
|
292
|
+
key,
|
|
293
|
+
`${repoUrl}/commit/${sha}`,
|
|
294
|
+
`GitHub Commit: ${sha.slice(0, 7)} (${subject})`,
|
|
295
|
+
'commit',
|
|
296
|
+
atlassian.authBase64,
|
|
297
|
+
commitGlobalId
|
|
298
|
+
);
|
|
299
|
+
results.push({ key, type: 'commit', ref: sha.slice(0, 7), status: commitRes.status });
|
|
300
|
+
|
|
301
|
+
// 2. Link PR se presente no commit title
|
|
302
|
+
if (prNumber) {
|
|
303
|
+
const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
|
|
304
|
+
const prRes = await linkUrlToJira(
|
|
305
|
+
key,
|
|
306
|
+
`${repoUrl}/pull/${prNumber}`,
|
|
307
|
+
`GitHub Pull Request #${prNumber}`,
|
|
308
|
+
'pr',
|
|
309
|
+
atlassian.authBase64,
|
|
310
|
+
prGlobalId,
|
|
311
|
+
subject
|
|
312
|
+
);
|
|
313
|
+
results.push({ key, type: 'pr', ref: `#${prNumber}`, status: prRes.status });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const successCount = results.filter(r => r.status === 'SUCCESS').length;
|
|
319
|
+
console.log(`[Atlassian Sync] Sincronização retroativa concluída: ${successCount}/${results.length} links processados com sucesso.`);
|
|
320
|
+
return results;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Formata um relatório executivo em Markdown com o resultado da sincronização de links.
|
|
325
|
+
* @param {object} [syncSummary={}]
|
|
326
|
+
* @returns {string}
|
|
327
|
+
*/
|
|
328
|
+
function formatReport(syncSummary = {}) {
|
|
329
|
+
let md = '# 🔗 Atlassian Sync Action — Remote Links Status Report\n\n';
|
|
330
|
+
|
|
331
|
+
const keys = syncSummary.keys || [];
|
|
332
|
+
const results = syncSummary.results || [];
|
|
333
|
+
|
|
334
|
+
md += `**Chaves Jira Vinculadas:** \`${keys.length > 0 ? keys.join(', ') : 'NENHUMA'}\`\n`;
|
|
335
|
+
md += `**Total de Links Processados:** \`${results.length}\`\n\n`;
|
|
336
|
+
|
|
337
|
+
if (results.length === 0) {
|
|
338
|
+
md += syncSummary.reason ? `*Observação:* ${syncSummary.reason}\n` : '*Nenhum link para sincronizar no contexto atual.*\n';
|
|
339
|
+
return md;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
md += '| Chave Jira | Tipo | Referência | Status |\n';
|
|
343
|
+
md += '| :--- | :--- | :--- | :--- |\n';
|
|
344
|
+
for (const r of results) {
|
|
345
|
+
const icon = r.status === 'SUCCESS' ? '✅' : '❌';
|
|
346
|
+
md += `| \`${r.key}\` | \`${r.type}\` | \`${r.ref}\` | ${icon} \`${r.status}\` |\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return md;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Audita o resultado da sincronização de links emitindo relatório OASIS SARIF v2.1.0.
|
|
354
|
+
* @param {object} [syncSummary={}]
|
|
355
|
+
* @returns {object} SARIF v2.1.0 JSON object
|
|
356
|
+
*/
|
|
357
|
+
function auditSyncToSarif(syncSummary = {}) {
|
|
358
|
+
const rules = [
|
|
359
|
+
{
|
|
360
|
+
id: 'RULE-SYNC-001',
|
|
361
|
+
name: 'JiraIssueKeyMissing',
|
|
362
|
+
severity: 'WARNING',
|
|
363
|
+
message: 'Nenhuma chave de issue Jira identificada no contexto atual do Git (Regra 11).',
|
|
364
|
+
remediation: 'Incluir a chave Jira no nome da branch ou na mensagem do commit: [<TICKET>] - <Ação em português>.',
|
|
365
|
+
cwe: 'CWE-1059'
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: 'RULE-SYNC-002',
|
|
369
|
+
name: 'RemoteLinkSyncFailed',
|
|
370
|
+
severity: 'ERROR',
|
|
371
|
+
message: 'Falha ao sincronizar Remote Link no card do Jira.',
|
|
372
|
+
remediation: 'Verifique credenciais Atlassian (Regra 15), permissões de projeto ou validade da chave Jira.',
|
|
373
|
+
cwe: 'CWE-287'
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: 'RULE-SYNC-003',
|
|
377
|
+
name: 'RemoteLinkSyncSuccess',
|
|
378
|
+
severity: 'NOTE',
|
|
379
|
+
message: 'Remote Link sincronizado com sucesso no card do Jira.',
|
|
380
|
+
remediation: 'Nenhuma ação necessária. Vínculo ativo e registrado.',
|
|
381
|
+
cwe: 'CWE-Unknown'
|
|
382
|
+
}
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
const findings = [];
|
|
386
|
+
const keys = syncSummary.keys || [];
|
|
387
|
+
const results = syncSummary.results || [];
|
|
388
|
+
|
|
389
|
+
if (keys.length === 0) {
|
|
390
|
+
findings.push({
|
|
391
|
+
ruleId: 'RULE-SYNC-001',
|
|
392
|
+
severity: 'WARNING',
|
|
393
|
+
message: syncSummary.reason || 'Nenhuma chave Jira identificada no contexto atual.',
|
|
394
|
+
filename: '.git',
|
|
395
|
+
line: 1,
|
|
396
|
+
column: 1
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const r of results) {
|
|
401
|
+
if (r.status !== 'SUCCESS') {
|
|
402
|
+
findings.push({
|
|
403
|
+
ruleId: 'RULE-SYNC-002',
|
|
404
|
+
severity: 'ERROR',
|
|
405
|
+
message: `Falha ao registrar remote link (${r.type}: ${r.ref}) para a chave ${r.key}.`,
|
|
406
|
+
filename: '.git',
|
|
407
|
+
line: 1,
|
|
408
|
+
column: 1
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return createSarifReport({
|
|
414
|
+
toolName: 'agent-master-atlassian-sync-action',
|
|
415
|
+
toolVersion: '5.1.0',
|
|
416
|
+
rules,
|
|
417
|
+
findings,
|
|
418
|
+
properties: {
|
|
419
|
+
category: 'Governance & Remote Links Sync',
|
|
420
|
+
keysCount: keys.length,
|
|
421
|
+
linksCount: results.length
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Classe Canônica AtlassianSyncAction.
|
|
428
|
+
* Fornece API orientada a objetos para orquestração da sincronização Git-Jira.
|
|
429
|
+
*/
|
|
430
|
+
class AtlassianSyncAction {
|
|
431
|
+
constructor(options = {}) {
|
|
432
|
+
this.options = Object.freeze({
|
|
433
|
+
targetDir: options.targetDir || process.cwd(),
|
|
434
|
+
timeout: options.timeout || 30000,
|
|
435
|
+
...options
|
|
436
|
+
});
|
|
437
|
+
this.atlassianClient = new AtlassianClient(this.options.targetDir, { timeout: this.options.timeout });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getAuth() {
|
|
441
|
+
return this.atlassianClient.getCredentials().authBase64;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
extractKeys(text) {
|
|
445
|
+
return extractIssueKeys(text);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
extractPr(text) {
|
|
449
|
+
return extractPrNumber(text);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
formatPayload(url, title, type, globalId, summary) {
|
|
453
|
+
return formatRemoteLinkPayload(url, title, type, globalId, summary);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async linkUrl(issueKey, url, title, type = 'commit', globalId = null, summary = null) {
|
|
457
|
+
return linkUrlToJira(issueKey, url, title, type, null, globalId, summary);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async linkConfluence(issueKey, pageUrl, pageTitle, summary = null) {
|
|
461
|
+
return linkConfluencePageToJira(issueKey, pageUrl, pageTitle, summary);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async backfill(limit = 30) {
|
|
465
|
+
return backfillHistory(limit);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async syncCurrentContext() {
|
|
469
|
+
return syncCurrentContext(this.options.targetDir);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
formatReport(syncResults = {}) {
|
|
473
|
+
return formatReport(syncResults);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
auditToSarif(syncResults = {}) {
|
|
477
|
+
return auditSyncToSarif(syncResults);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Métodos Estáticos Canônicos
|
|
481
|
+
static extractIssueKeys(text) {
|
|
482
|
+
return extractIssueKeys(text);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
static extractPrNumber(text) {
|
|
486
|
+
return extractPrNumber(text);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
static formatRemoteLinkPayload(url, title, type, globalId, summary) {
|
|
490
|
+
return formatRemoteLinkPayload(url, title, type, globalId, summary);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
static async linkUrlToJira(issueKey, url, title, type, authOverride, globalId, summary) {
|
|
494
|
+
return linkUrlToJira(issueKey, url, title, type, authOverride, globalId, summary);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
static async linkConfluencePageToJira(issueKey, pageUrl, pageTitle, summary) {
|
|
498
|
+
return linkConfluencePageToJira(issueKey, pageUrl, pageTitle, summary);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
static async backfillHistory(limit) {
|
|
502
|
+
return backfillHistory(limit);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
static async syncCurrentContext(targetDir) {
|
|
506
|
+
return syncCurrentContext(targetDir);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static formatReport(syncResults) {
|
|
510
|
+
return formatReport(syncResults);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
static auditToSarif(syncResults) {
|
|
514
|
+
return auditSyncToSarif(syncResults);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Ponto de entrada CLI para execução no GitHub Actions ou terminal local.
|
|
520
|
+
*/
|
|
521
|
+
async function runCli() {
|
|
522
|
+
const isCI = process.env.GITHUB_ACTIONS === 'true';
|
|
523
|
+
|
|
524
|
+
// Validação Compulsória de Requisitos (Regra 15)
|
|
525
|
+
// Em CI, a ausência de secrets é ERRO CRÍTICO para impedir perda silenciosa
|
|
526
|
+
const validation = validateRule15({ failFast: isCI, requireGitHub: false, requireAtlassian: true });
|
|
527
|
+
if (!validation.valid) {
|
|
528
|
+
console.error(`[Atlassian Sync] Falha de validação da Regra 15:\n ${validation.missing.join(', ')}`);
|
|
529
|
+
console.error(` Remediação: ${validation.remediation.join('\n ')}`);
|
|
530
|
+
if (isCI) {
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const args = process.argv.slice(2);
|
|
537
|
+
if (args.includes('--retroactive') || args.includes('-r')) {
|
|
538
|
+
const limitIdx = args.findIndex(a => a === '--retroactive' || a === '-r');
|
|
539
|
+
const limitVal = parseInt(args[limitIdx + 1], 10) || 30;
|
|
540
|
+
await backfillHistory(limitVal);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const syncOutcome = await syncCurrentContext();
|
|
545
|
+
if (!syncOutcome.synced) {
|
|
546
|
+
console.log(`[Atlassian Sync] ${syncOutcome.reason}`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.log(`[Atlassian Sync] Chaves Jira identificadas: ${syncOutcome.keys.join(', ')}`);
|
|
551
|
+
for (const r of syncOutcome.results) {
|
|
552
|
+
console.log(`[Atlassian Sync] ${r.type.toUpperCase()} ${r.ref} vinculado a ${r.key}: ${r.status}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (require.main === module) {
|
|
557
|
+
runCli().catch(err => {
|
|
558
|
+
console.error('[Atlassian Sync] Erro fatal na execução:', err);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
module.exports = {
|
|
564
|
+
AtlassianSyncAction,
|
|
565
|
+
getAuth,
|
|
566
|
+
extractIssueKeys,
|
|
567
|
+
extractPrNumber,
|
|
568
|
+
formatRemoteLinkPayload,
|
|
569
|
+
linkUrlToJira,
|
|
570
|
+
linkConfluencePageToJira,
|
|
571
|
+
backfillHistory,
|
|
572
|
+
syncCurrentContext,
|
|
573
|
+
formatReport,
|
|
574
|
+
auditSyncToSarif,
|
|
575
|
+
runCli
|
|
576
|
+
};
|