@orkastery/cli 0.6.3 → 0.6.4
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/adapters/openclaw/openclaw.plugin.json +1 -1
- package/adapters/openclaw/package.json +1 -1
- package/dist/agendador-cpu.js +46 -10
- package/dist/cenarios-sensores.js +5 -4
- package/dist/check-agil.js +2 -2
- package/dist/check-paralelo.js +322 -12
- package/dist/security-scan/agil.js +28 -12
- package/dist/security-scan/executor.js +1 -0
- package/dist/security-scan/incremental.js +289 -0
- package/dist/security-scan/trabalhador.js +53 -5
- package/dist/verify.js +12 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "orkastery",
|
|
3
3
|
"name": "Orkastery",
|
|
4
4
|
"description": "Conducao de looping threads em 6 fases pelo nucleo `ork`, exposta ao OpenClaw como tools `ork_*`. Zero regra de negocio no host: cada tool e uma chamada de CLI. Toda tool de projeto aceita projeto (nome do projeto pedido); o cwd do gateway nunca escolhe o projeto. O status do roadmap vem de ork_network_roadmap; as maquinas da pessoa, de ork_network_status (sem projeto).",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.4",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkastery/openclaw-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Plugin Orkastery para OpenClaw: 32 tools ork_* que traduzem intencao em chamada de CLI do nucleo `ork`. Zero regra de negocio no host.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": true,
|
package/dist/agendador-cpu.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.RESERVA_DO_GRUPO = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE = void 0;
|
|
3
|
+
exports.JANELA_DA_SONDA_MS = exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = exports.RESERVA_DO_GRUPO = exports.ENV_CONTAINERS_DA_LANE = exports.ENV_CPUS_DOCKER = exports.ENV_SINAL_DA_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_RESERVA = exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = exports.ENV_CPUS_DA_LANE = void 0;
|
|
4
|
+
exports.registrarContainerDaLane = registrarContainerDaLane;
|
|
4
5
|
exports.cargaDoProcLoadavg = cargaDoProcLoadavg;
|
|
5
6
|
exports.medirCpuDoGrupo = medirCpuDoGrupo;
|
|
6
7
|
exports.planejarCpuDoGrupo = planejarCpuDoGrupo;
|
|
@@ -16,12 +17,25 @@ exports.paredeAgileSimulada = paredeAgileSimulada;
|
|
|
16
17
|
const node_fs_1 = require("node:fs");
|
|
17
18
|
const steal_1 = require("./steal");
|
|
18
19
|
exports.ENV_CPUS_DA_LANE = 'ORK_CHECK_CPUS_DA_LANE';
|
|
20
|
+
exports.ENV_CPUS_DA_LANE_APOS_VARREDURA = 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA';
|
|
19
21
|
exports.ENV_CPUS_DA_LANE_APOS_RESERVA = 'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA';
|
|
20
22
|
exports.ENV_SINAL_DA_RESERVA = 'ORK_CHECK_RESERVA_DEVOLVIDA';
|
|
21
23
|
exports.ENV_CPUS_DOCKER = 'ORK_CHECK_CPUS_DOCKER';
|
|
24
|
+
/** Registro privado da rodada: quem sobe um container na lane anota o nome para a sonda de CPU. */
|
|
25
|
+
exports.ENV_CONTAINERS_DA_LANE = 'ORK_CHECK_CONTAINERS_DA_LANE';
|
|
22
26
|
exports.RESERVA_DO_GRUPO = 2;
|
|
23
27
|
exports.LIMIAR_STEAL_DO_AGENDADOR_PCT = 10;
|
|
24
28
|
exports.JANELA_DA_SONDA_MS = 250;
|
|
29
|
+
/** Registra o container antes do `docker run`; falha de evidencia nunca impede a ferramenta de rodar. */
|
|
30
|
+
function registrarContainerDaLane(nome, ambiente = process.env) {
|
|
31
|
+
const arquivo = ambiente[exports.ENV_CONTAINERS_DA_LANE];
|
|
32
|
+
if (!arquivo || !nome)
|
|
33
|
+
return;
|
|
34
|
+
try {
|
|
35
|
+
(0, node_fs_1.appendFileSync)(arquivo, JSON.stringify({ nome, em: new Date().toISOString() }) + '\n', { mode: 0o600 });
|
|
36
|
+
}
|
|
37
|
+
catch { /* a medicao fica incompleta e o agendador conserva a reserva */ }
|
|
38
|
+
}
|
|
25
39
|
const cpusValidas = (cpus) => Math.max(1, Math.floor(Number.isFinite(cpus) ? cpus : 1));
|
|
26
40
|
/** A carga de um minuto de `/proc/loadavg`; entrada invalida ou ausencia nao inventam saturacao. */
|
|
27
41
|
function cargaDoProcLoadavg(conteudo) {
|
|
@@ -68,14 +82,23 @@ function planejarCpuDoGrupo(cpusInformadas, medicao) {
|
|
|
68
82
|
const saturado = motivos.length > 0;
|
|
69
83
|
const fatia = saturado ? classic : Math.min(exports.RESERVA_DO_GRUPO, cpus - 1);
|
|
70
84
|
const ferramentas = Math.max(1, Math.floor(fatia));
|
|
85
|
+
const medida = medicao.cpusIntegracao;
|
|
86
|
+
const cpusIntegracao = saturado ? ferramentas : Math.max(1, Math.min(ferramentas, Number.isFinite(medida) && medida > 0 ? Math.ceil(medida) : ferramentas));
|
|
87
|
+
const medidaCompativel = medicao.fonteCpusIntegracao === 'medida' && medida !== null && medida !== undefined;
|
|
88
|
+
const integracaoEmParalelo = !saturado && medidaCompativel && cpus - fatia - cpusIntegracao >= 1;
|
|
71
89
|
return {
|
|
72
90
|
cpus,
|
|
73
91
|
ordem: saturado ? 'classic' : 'paralela',
|
|
74
92
|
saturado,
|
|
75
|
-
motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas
|
|
76
|
-
|
|
93
|
+
motivo: motivos.join('; ') || `carga e steal abaixo dos limiares; ${fatia} CPU(s) reservadas a varredura; ` +
|
|
94
|
+
(integracaoEmParalelo ? `integracao medida cabe em paralelo com ${cpusIntegracao} CPU(s)` : 'integracao permanece encadeada'),
|
|
95
|
+
cpusHermetica: saturado ? classic : Math.max(1, cpus - fatia - (integracaoEmParalelo ? cpusIntegracao : 0)),
|
|
96
|
+
cpusHermeticaAposVarredura: saturado ? classic : Math.max(1, cpus - cpusIntegracao),
|
|
77
97
|
cpusHermeticaAposReserva: saturado ? classic : cpus,
|
|
78
|
-
cpusIntegracao
|
|
98
|
+
cpusIntegracao,
|
|
99
|
+
fonteCpusIntegracao: saturado ? 'classic' : medidaCompativel
|
|
100
|
+
? 'medida' : 'conservadora',
|
|
101
|
+
integracaoEmParalelo,
|
|
79
102
|
cpusDaFatia: fatia,
|
|
80
103
|
ferramentasDaVarredura: ferramentas,
|
|
81
104
|
cpusPorContainer: Math.max(0.01, Math.floor((fatia / ferramentas) * 100) / 100),
|
|
@@ -91,10 +114,23 @@ function paredeAgileSimulada(plano, trabalho) {
|
|
|
91
114
|
return trabalho.hermetica / plano.cpusHermetica + trabalho.varredura / plano.cpusDaFatia
|
|
92
115
|
+ trabalho.integracao / plano.cpusIntegracao;
|
|
93
116
|
}
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
const paredeVarredura = trabalho.varredura / plano.cpusDaFatia;
|
|
118
|
+
const paredeIntegracao = trabalho.integracao / plano.cpusIntegracao;
|
|
119
|
+
if (plano.integracaoEmParalelo) {
|
|
120
|
+
let instante = 0, restante = trabalho.hermetica, capacidade = plano.cpusHermetica;
|
|
121
|
+
for (const evento of [{ em: paredeVarredura, libera: plano.cpusDaFatia },
|
|
122
|
+
{ em: paredeIntegracao, libera: plano.cpusIntegracao }].sort((a, b) => a.em - b.em)) {
|
|
123
|
+
restante = Math.max(0, restante - (evento.em - instante) * capacidade);
|
|
124
|
+
instante = evento.em;
|
|
125
|
+
capacidade += evento.libera;
|
|
126
|
+
}
|
|
127
|
+
return Math.max(paredeVarredura, paredeIntegracao, instante + restante / capacidade);
|
|
128
|
+
}
|
|
129
|
+
const feitoNaVarredura = Math.min(trabalho.hermetica, paredeVarredura * plano.cpusHermetica);
|
|
130
|
+
const restanteAposVarredura = trabalho.hermetica - feitoNaVarredura;
|
|
131
|
+
const feitoNaIntegracao = Math.min(restanteAposVarredura, paredeIntegracao * plano.cpusHermeticaAposVarredura);
|
|
132
|
+
const restanteFinal = restanteAposVarredura - feitoNaIntegracao;
|
|
133
|
+
const paredeDaHermetica = (feitoNaVarredura ? paredeVarredura : 0)
|
|
134
|
+
+ (feitoNaIntegracao ? paredeIntegracao : 0) + restanteFinal / plano.cpusHermeticaAposReserva;
|
|
135
|
+
return Math.max(paredeDaHermetica, paredeVarredura + paredeIntegracao);
|
|
100
136
|
}
|
|
@@ -47,10 +47,10 @@ const estado_thread_1 = require("./estado-thread");
|
|
|
47
47
|
const catalogo_1 = require("./catalogo");
|
|
48
48
|
const linha = (e) => JSON.stringify(e) + '\n';
|
|
49
49
|
function esperar(teste) {
|
|
50
|
-
const limite = Date.now() +
|
|
50
|
+
const limite = Date.now() + 15000;
|
|
51
51
|
while (!teste()) {
|
|
52
52
|
if (Date.now() >= limite)
|
|
53
|
-
throw new Error('subprocesso do cenário não terminou
|
|
53
|
+
throw new Error('subprocesso do cenário não terminou no prazo de transporte');
|
|
54
54
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -71,7 +71,8 @@ exports.CENARIO_SENSORES = {
|
|
|
71
71
|
const hook = path.join(catalogo, 'adapters/claude-code/hooks/ork-sensor.js');
|
|
72
72
|
const cli = (0, catalogo_1.cliDoCatalogo)(catalogo);
|
|
73
73
|
const start = Date.now();
|
|
74
|
-
|
|
74
|
+
// O teto de transporte absorve a fila do Classic; a fixture continua exigindo a latência funcional abaixo de 5 s.
|
|
75
|
+
const h = (0, node_child_process_1.spawnSync)(process.execPath, [hook], { cwd: wt, encoding: 'utf8', timeout: 15000,
|
|
75
76
|
input: JSON.stringify({ cwd: wt, session_id: 'fixture-claude', hook_event_name: 'PermissionRequest', tool_use_id: 'fixture-tool' }),
|
|
76
77
|
env: { ...process.env, ORK_SENSOR_CLI: cli } });
|
|
77
78
|
if (h.status !== 0 || h.stderr || h.stdout)
|
|
@@ -98,7 +99,7 @@ exports.CENARIO_SENSORES = {
|
|
|
98
99
|
}
|
|
99
100
|
// A identidade vem de um processo realmente executado e encerrado, sem recibo.
|
|
100
101
|
const runner = require.resolve('./adapters/codex-runner');
|
|
101
|
-
const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout:
|
|
102
|
+
const morto = (0, node_child_process_1.spawnSync)(process.execPath, ['-e', `console.log(JSON.stringify(require(process.argv[1]).identidadeProcesso(process.pid)))`, runner], { cwd: wt, encoding: 'utf8', timeout: 15000 });
|
|
102
103
|
if (morto.status !== 0)
|
|
103
104
|
throw new Error('processo de morte do cenário indisponível');
|
|
104
105
|
const filho = JSON.parse(morto.stdout);
|
package/dist/check-agil.js
CHANGED
|
@@ -627,8 +627,8 @@ function resultadosDasLanes(linhas) {
|
|
|
627
627
|
/** Hash do ambiente funcional; transporte do CHECK, npm e bookkeeping do shell nao mudam o teste. */
|
|
628
628
|
function assinaturaDoAmbiente(env) {
|
|
629
629
|
const ignorada = (nome) => nome.startsWith('npm_') || nome.startsWith('NPM_')
|
|
630
|
-
|| ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', '
|
|
631
|
-
'ORK_CHECK_CPUS_DOCKER',
|
|
630
|
+
|| ['ORK_CHECK_RELATORIO', 'ORK_CHECK_CPUS_DA_LANE', 'ORK_CHECK_CPUS_DA_LANE_APOS_VARREDURA',
|
|
631
|
+
'ORK_CHECK_CPUS_DA_LANE_APOS_RESERVA', 'ORK_CHECK_RESERVA_DEVOLVIDA', 'ORK_CHECK_CONTAINERS_DA_LANE', 'ORK_CHECK_CPUS_DOCKER',
|
|
632
632
|
'NODE_TEST_CONTEXT', 'NODE_COMPILE_CACHE', 'PWD', 'OLDPWD', 'SHLVL', '_', 'INIT_CWD'].includes(nome);
|
|
633
633
|
const pares = Object.entries(env).filter(([nome, valor]) => valor !== undefined && !ignorada(nome))
|
|
634
634
|
.sort(([a], [b]) => a.localeCompare(b));
|
package/dist/check-paralelo.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.argvDoItem = argvDoItem;
|
|
37
|
+
exports.chaveDoPerfilIntegracao = chaveDoPerfilIntegracao;
|
|
38
|
+
exports.picoCpuPorFonte = picoCpuPorFonte;
|
|
37
39
|
exports.esperaDoGrupo = esperaDoGrupo;
|
|
38
40
|
exports.rodarEmParalelo = rodarEmParalelo;
|
|
39
41
|
/**
|
|
@@ -48,12 +50,17 @@ exports.rodarEmParalelo = rodarEmParalelo;
|
|
|
48
50
|
* de integracao depois da varredura), e `maxParalelo` limita quantos rodam ao mesmo tempo.
|
|
49
51
|
*/
|
|
50
52
|
const node_child_process_1 = require("node:child_process");
|
|
53
|
+
const node_crypto_1 = require("node:crypto");
|
|
51
54
|
const fs = __importStar(require("node:fs"));
|
|
52
55
|
const os = __importStar(require("node:os"));
|
|
53
56
|
const path = __importStar(require("node:path"));
|
|
54
57
|
const agendador_cpu_1 = require("./agendador-cpu");
|
|
58
|
+
const integracoes_locais_1 = require("./integracoes-locais");
|
|
55
59
|
const TIMEOUT_BIN = '/usr/bin/timeout';
|
|
56
60
|
const NICE_BIN = '/usr/bin/nice';
|
|
61
|
+
const TIME_BIN = '/usr/bin/time';
|
|
62
|
+
const CONTRATO_PERFIL_INTEGRACAO = 'ork.check.cpu-integracao/v1';
|
|
63
|
+
const INTERVALO_CPU_MS = 100;
|
|
57
64
|
/** O argv de um item: `nice` (se pedido e existir), `timeout(1)` (se existir) e o `argv` do item ou `bash -c`. */
|
|
58
65
|
function argvDoItem(item, comGrupo = fs.existsSync(TIMEOUT_BIN), comNice = fs.existsSync(NICE_BIN)) {
|
|
59
66
|
const programa = item.argv?.length ? item.argv : ['bash', '-c', item.comando];
|
|
@@ -77,6 +84,243 @@ function vagas(max) {
|
|
|
77
84
|
livres++; },
|
|
78
85
|
};
|
|
79
86
|
}
|
|
87
|
+
function arquivoDoPerfilIntegracao() {
|
|
88
|
+
const base = process.env.XDG_CACHE_HOME && path.isAbsolute(process.env.XDG_CACHE_HOME)
|
|
89
|
+
? process.env.XDG_CACHE_HOME : path.join(os.homedir(), '.cache');
|
|
90
|
+
return path.join(base, 'orkastery', 'check', 'cpu-integracao.json');
|
|
91
|
+
}
|
|
92
|
+
/** Os arquivos da lane de integracao, pela mesma regra do `test-lanes.cjs`: a lista do ambiente ou a do nucleo. */
|
|
93
|
+
function arquivosDaIntegracao(env = process.env) {
|
|
94
|
+
const lista = env.ORK_LANES_INTEGRACAO !== undefined
|
|
95
|
+
? env.ORK_LANES_INTEGRACAO.split(',').map((n) => n.trim()).filter(Boolean) : [...integracoes_locais_1.TESTES_DE_INTEGRACAO_LOCAL];
|
|
96
|
+
return [...new Set(lista)].sort();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A medida so atravessa rodadas quando comando, capacidade e entradas que definem a lane continuam iguais: o script das
|
|
100
|
+
* lanes, a lista de integracao e o conteudo compilado de cada teste dela. Teste novo, mudado ou ausente troca a chave.
|
|
101
|
+
*/
|
|
102
|
+
function chaveDoPerfilIntegracao(itens, cpus, arquivos = arquivosDaIntegracao()) {
|
|
103
|
+
const integracao = itens.filter((i) => i.papelCpu === 'integracao');
|
|
104
|
+
if (!integracao.length)
|
|
105
|
+
return null;
|
|
106
|
+
const h = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify({ cpus, node: process.version,
|
|
107
|
+
comandos: integracao.map((i) => i.comando), arquivos }));
|
|
108
|
+
const cwd = integracao[0].cwd;
|
|
109
|
+
const relativos = ['core/dist/integracoes-locais.js', 'core/scripts/test-lanes.cjs',
|
|
110
|
+
...arquivos.map((nome) => path.join('core', 'dist-test', 'test', nome))];
|
|
111
|
+
for (const relativo of relativos) {
|
|
112
|
+
try {
|
|
113
|
+
h.update(relativo).update(fs.readFileSync(path.join(cwd, relativo)));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
h.update(`${relativo}:ausente`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return h.digest('hex');
|
|
120
|
+
}
|
|
121
|
+
function lerPerfilIntegracao(chave, cpus) {
|
|
122
|
+
if (!chave)
|
|
123
|
+
return null;
|
|
124
|
+
try {
|
|
125
|
+
const p = JSON.parse(fs.readFileSync(arquivoDoPerfilIntegracao(), 'utf8'));
|
|
126
|
+
return p?.contrato === CONTRATO_PERFIL_INTEGRACAO && p.chave === chave && p.cpus === cpus && p.completa
|
|
127
|
+
&& Number.isFinite(p.picoCpus) && p.picoCpus > 0 && p.picoCpus <= cpus ? p : null;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function gravarPerfilIntegracao(chave, cpus, medicao) {
|
|
134
|
+
if (!chave || !medicao.completa || medicao.anomalia || medicao.picoCpus === null
|
|
135
|
+
|| !(medicao.picoCpus > 0) || medicao.picoCpus > cpus)
|
|
136
|
+
return;
|
|
137
|
+
const arquivo = arquivoDoPerfilIntegracao();
|
|
138
|
+
try {
|
|
139
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true });
|
|
140
|
+
const temporario = `${arquivo}.${process.pid}.tmp`;
|
|
141
|
+
const perfil = { contrato: CONTRATO_PERFIL_INTEGRACAO, chave, cpus,
|
|
142
|
+
picoCpus: medicao.picoCpus, completa: true, medidoEm: new Date().toISOString() };
|
|
143
|
+
fs.writeFileSync(temporario, JSON.stringify(perfil) + '\n', { mode: 0o600 });
|
|
144
|
+
fs.renameSync(temporario, arquivo);
|
|
145
|
+
}
|
|
146
|
+
catch { /* cache de desempenho ausente conserva a reserva; nunca derruba o CHECK */ }
|
|
147
|
+
}
|
|
148
|
+
const CLK_TCK = (() => {
|
|
149
|
+
const r = (0, node_child_process_1.spawnSync)('getconf', ['CLK_TCK'], { encoding: 'utf8', timeout: 1000 });
|
|
150
|
+
const n = Number(r.stdout?.trim());
|
|
151
|
+
return Number.isFinite(n) && n > 0 ? n : 100;
|
|
152
|
+
})();
|
|
153
|
+
function processosCpu() {
|
|
154
|
+
const tabela = new Map();
|
|
155
|
+
let nomes = [];
|
|
156
|
+
try {
|
|
157
|
+
nomes = fs.readdirSync('/proc');
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return tabela;
|
|
161
|
+
}
|
|
162
|
+
for (const nome of nomes) {
|
|
163
|
+
if (!/^\d+$/.test(nome))
|
|
164
|
+
continue;
|
|
165
|
+
try {
|
|
166
|
+
const stat = fs.readFileSync(`/proc/${nome}/stat`, 'utf8');
|
|
167
|
+
const depois = stat.slice(stat.lastIndexOf(')') + 2).trim().split(/\s+/);
|
|
168
|
+
tabela.set(Number(nome), { ppid: Number(depois[1]), cpuMs: (Number(depois[11]) + Number(depois[12])) * 1000 / CLK_TCK,
|
|
169
|
+
inicio: depois[19] });
|
|
170
|
+
}
|
|
171
|
+
catch { /* acabou entre a lista e a leitura */ }
|
|
172
|
+
}
|
|
173
|
+
return tabela;
|
|
174
|
+
}
|
|
175
|
+
function arvoreDeProcessos(raiz, tabela) {
|
|
176
|
+
const filhos = new Map();
|
|
177
|
+
for (const [pid, p] of tabela) {
|
|
178
|
+
const lista = filhos.get(p.ppid) ?? [];
|
|
179
|
+
lista.push(pid);
|
|
180
|
+
filhos.set(p.ppid, lista);
|
|
181
|
+
}
|
|
182
|
+
const vistos = new Set(), pilha = [raiz];
|
|
183
|
+
while (pilha.length) {
|
|
184
|
+
const pid = pilha.pop();
|
|
185
|
+
if (vistos.has(pid))
|
|
186
|
+
continue;
|
|
187
|
+
vistos.add(pid);
|
|
188
|
+
pilha.push(...(filhos.get(pid) ?? []));
|
|
189
|
+
}
|
|
190
|
+
return [...vistos];
|
|
191
|
+
}
|
|
192
|
+
function nomesDosContainers(arquivo) {
|
|
193
|
+
try {
|
|
194
|
+
return [...new Set(fs.readFileSync(arquivo, 'utf8').split('\n').filter(Boolean).flatMap((linha) => {
|
|
195
|
+
try {
|
|
196
|
+
const r = JSON.parse(linha);
|
|
197
|
+
return typeof r.nome === 'string' ? [r.nome] : [];
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}))];
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function arquivoCpuDoContainer(nome) {
|
|
209
|
+
const r = (0, node_child_process_1.spawnSync)('docker', ['inspect', '--format', '{{.Id}} {{.State.Pid}}', nome], { encoding: 'utf8', timeout: 1000 });
|
|
210
|
+
const [id = '', pidTexto = ''] = r.stdout?.trim().split(/\s+/) ?? [];
|
|
211
|
+
const pid = Number(pidTexto);
|
|
212
|
+
if (r.status !== 0)
|
|
213
|
+
return { fonte: null, motivo: 'docker inspect nao encontrou o conteiner' };
|
|
214
|
+
if (!/^[a-f0-9]{12,64}$/.test(id) || !Number.isInteger(pid) || pid <= 0) {
|
|
215
|
+
return { fonte: null, motivo: 'PID ou identificador do conteiner indisponivel' };
|
|
216
|
+
}
|
|
217
|
+
let linhas = '';
|
|
218
|
+
try {
|
|
219
|
+
linhas = fs.readFileSync(`/proc/${pid}/cgroup`, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return { fonte: null, motivo: 'cgroup do PID do conteiner indisponivel' };
|
|
223
|
+
}
|
|
224
|
+
const unificado = /^0::(.+)$/m.exec(linhas)?.[1];
|
|
225
|
+
const candidatos = [
|
|
226
|
+
...(unificado ? [{ arquivo: path.join('/sys/fs/cgroup', unificado, 'cpu.stat'), divisor: 1000 }] : []),
|
|
227
|
+
{ arquivo: path.join('/sys/fs/cgroup/system.slice', `docker-${id}.scope`, 'cpu.stat'), divisor: 1000 },
|
|
228
|
+
{ arquivo: path.join('/sys/fs/cgroup/docker', id, 'cpu.stat'), divisor: 1000 },
|
|
229
|
+
];
|
|
230
|
+
const legado = /^(?:\d+):[^\n]*(?:cpuacct|cpu)[^\n]*:(.+)$/m.exec(linhas)?.[1];
|
|
231
|
+
if (legado)
|
|
232
|
+
candidatos.push({ arquivo: path.join('/sys/fs/cgroup/cpuacct', legado, 'cpuacct.usage'), divisor: 1_000_000 });
|
|
233
|
+
const fonte = candidatos.find((x) => fs.existsSync(x.arquivo)) ?? null;
|
|
234
|
+
return fonte ? { fonte, motivo: null }
|
|
235
|
+
: { fonte: null, motivo: 'cpu.stat ausente no cgroup v2, no escopo do systemd e no caminho legado' };
|
|
236
|
+
}
|
|
237
|
+
function lerCpuDoCgroup(fonte) {
|
|
238
|
+
try {
|
|
239
|
+
const texto = fs.readFileSync(fonte.arquivo, 'utf8');
|
|
240
|
+
const bruto = fonte.arquivo.endsWith('cpu.stat') ? Number(/^usage_usec\s+(\d+)$/m.exec(texto)?.[1]) : Number(texto.trim());
|
|
241
|
+
return Number.isFinite(bruto) && bruto >= 0 ? bruto / fonte.divisor : null;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Calcula o pico por fonte. A primeira leitura vira baseline; uma fonte que nasce depois dela contribui com o acumulado
|
|
249
|
+
* desde seu nascimento, sem despejar na janela o tempo anterior de processos ou conteineres que ja existiam.
|
|
250
|
+
*/
|
|
251
|
+
function picoCpuPorFonte(amostras, cpus) {
|
|
252
|
+
let pico = 0;
|
|
253
|
+
for (let i = 1; i < amostras.length; i++) {
|
|
254
|
+
const anterior = amostras[i - 1], atual = amostras[i];
|
|
255
|
+
if (!(atual.em > anterior.em))
|
|
256
|
+
continue;
|
|
257
|
+
let delta = 0;
|
|
258
|
+
for (const [fonte, acumulado] of Object.entries(atual.fontes)) {
|
|
259
|
+
const base = anterior.fontes[fonte];
|
|
260
|
+
delta += Math.max(0, acumulado - (base ?? 0));
|
|
261
|
+
}
|
|
262
|
+
pico = Math.max(pico, delta / (atual.em - anterior.em));
|
|
263
|
+
}
|
|
264
|
+
if (pico > cpus + 1e-9) {
|
|
265
|
+
return { picoCpus: null, anomalia: `pico medido de ${pico.toFixed(2)} CPU excede a capacidade de ${cpus} CPU` };
|
|
266
|
+
}
|
|
267
|
+
return { picoCpus: pico > 0 ? Math.ceil(pico * 100) / 100 : null, anomalia: null };
|
|
268
|
+
}
|
|
269
|
+
/** CPU acumulada e pico por janela da integracao; nomes registrados mas nunca observados tornam a medida incompleta. */
|
|
270
|
+
function sondarCpuDaIntegracao(pid, registro, cpus) {
|
|
271
|
+
const fontes = new Map();
|
|
272
|
+
const motivos = new Map();
|
|
273
|
+
const amostras = [];
|
|
274
|
+
const primeiraCpuDosContainers = new Map();
|
|
275
|
+
const ultimaCpuDosContainers = new Map();
|
|
276
|
+
const amostrar = () => {
|
|
277
|
+
const tabela = processosCpu();
|
|
278
|
+
const atuais = {};
|
|
279
|
+
for (const p of arvoreDeProcessos(pid, tabela)) {
|
|
280
|
+
const x = tabela.get(p);
|
|
281
|
+
if (x)
|
|
282
|
+
atuais[`processo:${p}:${x.inicio}`] = x.cpuMs;
|
|
283
|
+
}
|
|
284
|
+
for (const nome of nomesDosContainers(registro)) {
|
|
285
|
+
let fonte = fontes.get(nome);
|
|
286
|
+
if (!fonte) {
|
|
287
|
+
const busca = arquivoCpuDoContainer(nome);
|
|
288
|
+
if (busca.fonte) {
|
|
289
|
+
fonte = busca.fonte;
|
|
290
|
+
fontes.set(nome, fonte);
|
|
291
|
+
motivos.delete(nome);
|
|
292
|
+
}
|
|
293
|
+
else if (busca.motivo)
|
|
294
|
+
motivos.set(nome, busca.motivo);
|
|
295
|
+
}
|
|
296
|
+
const cpu = fonte ? lerCpuDoCgroup(fonte) : null;
|
|
297
|
+
if (cpu !== null) {
|
|
298
|
+
atuais[`conteiner:${nome}`] = cpu;
|
|
299
|
+
if (!primeiraCpuDosContainers.has(nome))
|
|
300
|
+
primeiraCpuDosContainers.set(nome, amostras.length === 0 ? cpu : 0);
|
|
301
|
+
ultimaCpuDosContainers.set(nome, cpu);
|
|
302
|
+
motivos.delete(nome);
|
|
303
|
+
}
|
|
304
|
+
else if (fonte)
|
|
305
|
+
motivos.set(nome, 'contador de CPU do cgroup ilegivel');
|
|
306
|
+
}
|
|
307
|
+
amostras.push({ em: Date.now(), fontes: atuais });
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
amostrar,
|
|
311
|
+
fechar: (arvoreMs, paredeMs) => {
|
|
312
|
+
amostrar();
|
|
313
|
+
const nomes = nomesDosContainers(registro);
|
|
314
|
+
const containersMs = nomes.reduce((total, nome) => total + Math.max(0, (ultimaCpuDosContainers.get(nome) ?? 0) - (primeiraCpuDosContainers.get(nome) ?? 0)), 0);
|
|
315
|
+
const ausentes = nomes.filter((nome) => !ultimaCpuDosContainers.has(nome));
|
|
316
|
+
const pico = picoCpuPorFonte(amostras, cpus);
|
|
317
|
+
return { arvoreMs, containersMs, totalMs: arvoreMs + containersMs, paredeMs,
|
|
318
|
+
picoCpus: pico.picoCpus, completa: ausentes.length === 0 && !pico.anomalia,
|
|
319
|
+
containers: nomes, ausentes, motivosDosAusentes: ausentes.map((container) => ({ container,
|
|
320
|
+
motivo: motivos.get(container) ?? 'conteiner terminou antes de uma amostra completa' })), anomalia: pico.anomalia };
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
80
324
|
/**
|
|
81
325
|
* O lado do auxiliar: lanca cada item quando os de `apos` terminam e ha vaga, espera todos e grava `resultado.json` ao
|
|
82
326
|
* lado das saidas.
|
|
@@ -84,7 +328,15 @@ function vagas(max) {
|
|
|
84
328
|
async function rodarEspecificacao(esp) {
|
|
85
329
|
const comGrupo = fs.existsSync(TIMEOUT_BIN);
|
|
86
330
|
const usaAgendador = esp.itens.some((i) => i.papelCpu === 'hermetica');
|
|
87
|
-
const
|
|
331
|
+
const cpus = esp.cpus ?? os.availableParallelism();
|
|
332
|
+
const chaveDoPerfil = usaAgendador ? chaveDoPerfilIntegracao(esp.itens, cpus) : null;
|
|
333
|
+
const perfil = esp.medicaoCpu?.cpusIntegracao === undefined ? lerPerfilIntegracao(chaveDoPerfil, cpus) : null;
|
|
334
|
+
const medicao = esp.medicaoCpu ?? await (0, agendador_cpu_1.medirCpuDoGrupo)();
|
|
335
|
+
if (perfil)
|
|
336
|
+
Object.assign(medicao, { cpusIntegracao: perfil.picoCpus, fonteCpusIntegracao: 'medida' });
|
|
337
|
+
else if (medicao.cpusIntegracao === undefined)
|
|
338
|
+
Object.assign(medicao, { cpusIntegracao: null, fonteCpusIntegracao: 'conservadora' });
|
|
339
|
+
const plano = usaAgendador ? (0, agendador_cpu_1.planejarCpuDoGrupo)(cpus, medicao) : undefined;
|
|
88
340
|
const sinalDaReserva = plano && !plano.saturado ? path.join(esp.dir, 'reserva-devolvida.json') : undefined;
|
|
89
341
|
const itens = plano ? aplicarPlano(esp.itens, plano, sinalDaReserva) : esp.itens;
|
|
90
342
|
const limite = vagas(esp.maxParalelo && esp.maxParalelo > 0 ? esp.maxParalelo : itens.length);
|
|
@@ -119,29 +371,86 @@ async function rodarEspecificacao(esp) {
|
|
|
119
371
|
if (!sinalDaReserva || !reservados.some((item) => item.papelCpu === 'varredura')
|
|
120
372
|
|| !reservados.some((item) => item.papelCpu === 'integracao'))
|
|
121
373
|
return;
|
|
122
|
-
|
|
123
|
-
|
|
374
|
+
const varreduras = reservados.filter((item) => item.papelCpu === 'varredura');
|
|
375
|
+
const integracoes = reservados.filter((item) => item.papelCpu === 'integracao');
|
|
376
|
+
const degrau = async (origem, pecas, vagas, antes) => {
|
|
377
|
+
await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
|
|
378
|
+
fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
|
|
379
|
+
em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
|
|
380
|
+
};
|
|
381
|
+
if (!plano.integracaoEmParalelo) {
|
|
382
|
+
await degrau('varredura', varreduras, plano.cpusHermeticaAposVarredura, plano.cpusHermetica);
|
|
383
|
+
await degrau('integracao', integracoes, plano.cpusHermeticaAposReserva, plano.cpusHermeticaAposVarredura);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// As promessas escrevem em série no event loop; o primeiro término recebe somente a reserva da própria peça.
|
|
387
|
+
let vagas = plano.cpusHermetica;
|
|
388
|
+
const aoTerminar = async (origem, pecas, libera) => {
|
|
389
|
+
await Promise.all(pecas.map((item) => vezDoItem(item, indice.get(item.nome))));
|
|
390
|
+
const antes = vagas;
|
|
391
|
+
vagas = Math.min(plano.cpus, vagas + libera);
|
|
392
|
+
fs.appendFileSync(sinalDaReserva, JSON.stringify({ contrato: 'ork.check.reserva-devolvida/v2', origem,
|
|
393
|
+
em: new Date().toISOString(), vagasGanhas: vagas - antes, vagas }) + '\n', { mode: 0o600 });
|
|
394
|
+
};
|
|
395
|
+
await Promise.all([aoTerminar('varredura', varreduras, plano.cpusDaFatia),
|
|
396
|
+
aoTerminar('integracao', integracoes, plano.cpusIntegracao)]);
|
|
397
|
+
}
|
|
124
398
|
};
|
|
125
399
|
const [, terminados] = await Promise.all([devolverReserva(), todos]);
|
|
400
|
+
const medidaDaIntegracao = terminados.find((t) => t.medicaoCpu)?.medicaoCpu;
|
|
401
|
+
if (plano && medidaDaIntegracao) {
|
|
402
|
+
plano.medicaoCpuIntegracao = medidaDaIntegracao;
|
|
403
|
+
gravarPerfilIntegracao(chaveDoPerfil, cpus, medidaDaIntegracao);
|
|
404
|
+
}
|
|
126
405
|
fs.writeFileSync(path.join(esp.dir, 'resultado.json'), JSON.stringify({ terminados, ...(plano ? { agendamentoCpu: plano } : {}) }), { mode: 0o600 });
|
|
127
406
|
function lancar(item, i) {
|
|
128
407
|
return new Promise((resolve) => {
|
|
129
408
|
const saida = fs.openSync(path.join(esp.dir, `${i}.out`), 'w', 0o600);
|
|
130
409
|
const erro = fs.openSync(path.join(esp.dir, `${i}.err`), 'w', 0o600);
|
|
131
410
|
const inicio = Date.now();
|
|
132
|
-
const
|
|
411
|
+
const medirIntegracao = item.papelCpu === 'integracao';
|
|
412
|
+
const registro = path.join(esp.dir, `${i}.containers.jsonl`);
|
|
413
|
+
const tempo = path.join(esp.dir, `${i}.tempo`);
|
|
414
|
+
if (medirIntegracao)
|
|
415
|
+
fs.writeFileSync(registro, '', { mode: 0o600 });
|
|
416
|
+
const base = argvDoItem(item, comGrupo);
|
|
417
|
+
const medido = medirIntegracao && fs.existsSync(TIME_BIN)
|
|
418
|
+
? [TIME_BIN, '-f', '%U %S', '-o', tempo, ...base] : base;
|
|
419
|
+
const [bin, ...args] = medido;
|
|
133
420
|
let fim = null;
|
|
421
|
+
let sonda = null;
|
|
422
|
+
let relogio = null;
|
|
134
423
|
const encerrar = (t) => {
|
|
135
424
|
if (fim)
|
|
136
425
|
return;
|
|
137
|
-
|
|
426
|
+
if (relogio)
|
|
427
|
+
clearInterval(relogio);
|
|
428
|
+
let arvoreMs = 0;
|
|
429
|
+
if (medirIntegracao) {
|
|
430
|
+
try {
|
|
431
|
+
const [user, sys] = fs.readFileSync(tempo, 'utf8').trim().split(/\s+/).map(Number);
|
|
432
|
+
arvoreMs = (user + sys) * 1000;
|
|
433
|
+
}
|
|
434
|
+
catch { /* medida incompleta abaixo */ }
|
|
435
|
+
}
|
|
436
|
+
const medicaoCpu = medirIntegracao && sonda ? sonda.fechar(arvoreMs, t.duracaoMs) : undefined;
|
|
437
|
+
if (medicaoCpu && !(arvoreMs > 0))
|
|
438
|
+
medicaoCpu.completa = false;
|
|
439
|
+
fim = { ...t, ...(medicaoCpu ? { medicaoCpu } : {}) };
|
|
138
440
|
fs.closeSync(saida);
|
|
139
441
|
fs.closeSync(erro);
|
|
140
|
-
resolve(
|
|
442
|
+
resolve(fim);
|
|
141
443
|
};
|
|
142
444
|
// Sem o `timeout(1)`, o proprio auxiliar mata pelo prazo, como o `spawnSync` do `exec` faria.
|
|
143
|
-
const
|
|
445
|
+
const ambiente = { ...process.env, ...item.ambiente,
|
|
446
|
+
...(medirIntegracao ? { [agendador_cpu_1.ENV_CONTAINERS_DA_LANE]: registro } : {}) };
|
|
447
|
+
const filho = (0, node_child_process_1.spawn)(bin, args, { cwd: item.cwd, env: ambiente, stdio: ['ignore', saida, erro],
|
|
144
448
|
...(comGrupo ? {} : { timeout: item.prazoMs, killSignal: 'SIGTERM' }) });
|
|
449
|
+
if (medirIntegracao) {
|
|
450
|
+
sonda = sondarCpuDaIntegracao(filho.pid, registro, cpus);
|
|
451
|
+
sonda.amostrar();
|
|
452
|
+
relogio = setInterval(sonda.amostrar, INTERVALO_CPU_MS);
|
|
453
|
+
}
|
|
145
454
|
filho.on('error', (e) => encerrar({ nome: item.nome, code: null, signal: null,
|
|
146
455
|
error: e.code ?? e.message, duracaoMs: Date.now() - inicio, comGrupo }));
|
|
147
456
|
filho.on('close', (code, signal) => encerrar({ nome: item.nome, code, signal,
|
|
@@ -170,24 +479,25 @@ function aplicarPlano(originais, plano, sinalDaReserva) {
|
|
|
170
479
|
const cpusContainer = String(plano.cpusPorContainer);
|
|
171
480
|
ambiente(hermetica, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusHermetica), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
|
|
172
481
|
...(sinalDaReserva && varredura && integracoes.length > 0 ? {
|
|
482
|
+
[agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_VARREDURA]: String(plano.cpusHermeticaAposVarredura),
|
|
173
483
|
[agendador_cpu_1.ENV_CPUS_DA_LANE_APOS_RESERVA]: String(plano.cpusHermeticaAposReserva), [agendador_cpu_1.ENV_SINAL_DA_RESERVA]: sinalDaReserva,
|
|
174
484
|
} : {}) });
|
|
175
485
|
ambiente(varredura, {
|
|
176
486
|
ORK_CHECK_VARREDURA_PARALELA: String(plano.ferramentasDaVarredura),
|
|
177
487
|
[agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer,
|
|
178
488
|
});
|
|
179
|
-
let
|
|
489
|
+
let anteriorIntegracao;
|
|
180
490
|
for (const item of integracoes) {
|
|
181
491
|
ambiente(item, { [agendador_cpu_1.ENV_CPUS_DA_LANE]: String(plano.cpusIntegracao), [agendador_cpu_1.ENV_CPUS_DOCKER]: cpusContainer });
|
|
182
|
-
depender(item, [
|
|
183
|
-
|
|
492
|
+
depender(item, [anteriorIntegracao ?? (plano.integracaoEmParalelo ? undefined : varredura?.nome)]);
|
|
493
|
+
anteriorIntegracao = item.nome;
|
|
184
494
|
}
|
|
185
495
|
if (plano.saturado)
|
|
186
496
|
depender(varredura, [hermetica?.nome]);
|
|
187
497
|
// Comandos sem papel conhecido nao disputam CPU com as tres pecas: fecham a cadeia, um por vez.
|
|
188
498
|
for (const item of outros) {
|
|
189
|
-
depender(item, [hermetica?.nome,
|
|
190
|
-
|
|
499
|
+
depender(item, [hermetica?.nome, varredura?.nome, anteriorIntegracao]);
|
|
500
|
+
anteriorIntegracao = item.nome;
|
|
191
501
|
}
|
|
192
502
|
return itens;
|
|
193
503
|
}
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S =
|
|
36
|
+
exports.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S = void 0;
|
|
37
37
|
exports.especificacaoDoAgile = especificacaoDoAgile;
|
|
38
38
|
exports.prazoDoBuildDaImagem = prazoDoBuildDaImagem;
|
|
39
39
|
exports.estourouNoAgile = estourouNoAgile;
|
|
@@ -48,9 +48,9 @@ exports.runScanAgil = runScanAgil;
|
|
|
48
48
|
* ferramentas do catalogo e a mesma classificacao contra a base. O que muda e o empacotamento: a arvore da base sai uma
|
|
49
49
|
* vez, antes, e cada ferramenta roda num trabalhador proprio (`trabalhador.ts`), ao mesmo tempo, chamando o mesmo
|
|
50
50
|
* `scanTool` do Classic; o relatorio sai na ordem do catalogo. As ferramentas do Trivy montam um diretorio persistente
|
|
51
|
-
* por ferramenta no `/tmp/trivy-cache` do catalogo
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* por ferramenta no `/tmp/trivy-cache` do catalogo. O resultado normalizado do Semgrep e das ferramentas de dependência
|
|
52
|
+
* fica no cache da máquina, por versão, regras e conteúdo; o Classic continua varrendo toda a árvore. Cada peça do Agile
|
|
53
|
+
* que não pode rodar cai para o equivalente do Classic e vai ao relatório
|
|
54
54
|
* (`check.pecas`) e ao `ORK_CHECK_RELATORIO` do verify.
|
|
55
55
|
*/
|
|
56
56
|
const fs = __importStar(require("node:fs"));
|
|
@@ -63,6 +63,7 @@ const redacao_url_1 = require("../redacao-url");
|
|
|
63
63
|
const compare_1 = require("./compare");
|
|
64
64
|
const config_1 = require("./config");
|
|
65
65
|
const guard_1 = require("./guard");
|
|
66
|
+
const incremental_1 = require("./incremental");
|
|
66
67
|
const run_1 = require("./run");
|
|
67
68
|
const executor_1 = require("./executor");
|
|
68
69
|
const imagem_de_lint_1 = require("./imagem-de-lint");
|
|
@@ -71,12 +72,9 @@ const TRABALHADOR = path.join(__dirname, 'trabalhador.js');
|
|
|
71
72
|
/** Sem prazo da rodada, o trabalhador ganha o maior prazo do catalogo para a HEAD e para a base, com folga. */
|
|
72
73
|
const FOLGA_MS = 60_000;
|
|
73
74
|
/**
|
|
74
|
-
* O
|
|
75
|
-
*
|
|
76
|
-
* fica de fora, declarado como peca em fallback.
|
|
75
|
+
* O cache guarda os achados normalizados junto da identidade das regras do registro. Assim uma rodada morna devolve o
|
|
76
|
+
* `check_id` original, em vez de reler uma regra por arquivo local e reescrever a impressão digital.
|
|
77
77
|
*/
|
|
78
|
-
exports.SEMGREP_SEM_CACHE = { peca: 'cache-semgrep', estado: 'fallback',
|
|
79
|
-
motivo: 'regra lida de arquivo local troca o check_id e a impressao digital', fallback: 'regras do registro a cada rodada, como no Classic' };
|
|
80
78
|
/**
|
|
81
79
|
* O prazo por regra e arquivo do Semgrep no Agile, em segundos. O padrao dele e 5 s; com as ferramentas juntas (e, no
|
|
82
80
|
* verify, junto das lanes de teste) a disputa de CPU fazia uma regra estourar (`partial-timeout` medido na primeira
|
|
@@ -284,8 +282,8 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
284
282
|
return report;
|
|
285
283
|
}
|
|
286
284
|
report.base = base;
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
pecas.push({ peca: 'varredura-incremental', estado: 'agile',
|
|
286
|
+
motivo: 'arquivo pela versao e regras da ferramenta; dependencias pelo lockfile; demais ferramentas rodam inteiras' });
|
|
289
287
|
const count = (0, run_1.git)(root, ['rev-list', '--count', `${base}..${head}`]);
|
|
290
288
|
const deadline = opts.deadlineMs === undefined ? null : Date.now() + opts.deadlineMs;
|
|
291
289
|
// A arvore da base sai uma vez, aqui, e os trabalhadores a dividem (cada um varre a sua ferramenta nela).
|
|
@@ -298,6 +296,19 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
298
296
|
}
|
|
299
297
|
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-security-scan-agil-')));
|
|
300
298
|
guard.add('outDirs', dir);
|
|
299
|
+
// A peça do cache já fazia parte do contrato ordenado do relatório antes das peças de executor. Resolver aqui mantém
|
|
300
|
+
// essa ordem pública; o mesmo resultado segue no trabalho que será entregue ao processo da ferramenta.
|
|
301
|
+
const regrasSemgrepPorIndice = new Map();
|
|
302
|
+
tools.forEach((t, i) => {
|
|
303
|
+
if (t.name !== 'semgrep')
|
|
304
|
+
return;
|
|
305
|
+
const regrasSemgrep = (0, incremental_1.resolverRegrasSemgrep)(t, (0, run_1.unitsOf)(t, pairs));
|
|
306
|
+
regrasSemgrepPorIndice.set(i, regrasSemgrep);
|
|
307
|
+
pecas.push(regrasSemgrep.reutilizavel
|
|
308
|
+
? { peca: 'cache-semgrep', estado: 'agile', motivo: 'hash do conteúdo das regras resolvidas; check_id preservado' }
|
|
309
|
+
: { peca: 'cache-semgrep', estado: 'fallback', motivo: regrasSemgrep.motivo,
|
|
310
|
+
fallback: 'varredura integral, como no Classic' });
|
|
311
|
+
});
|
|
301
312
|
// RM-072 (D12): quem tem o binario nativo na versao do catalogo roda sem o Docker; a peca diz quem faltou.
|
|
302
313
|
const probe = opts.probe ?? (0, executor_1.defaultProbe)();
|
|
303
314
|
const comNativo = tools.filter(t => t.native && !t.install);
|
|
@@ -310,7 +321,12 @@ function runScanAgil(opts, rodar = check_paralelo_1.rodarEmParalelo) {
|
|
|
310
321
|
}
|
|
311
322
|
const montados = tools.map((t, i) => {
|
|
312
323
|
const restante = deadline === null ? t.timeoutMs : Math.min(t.timeoutMs, deadline - Date.now());
|
|
313
|
-
const
|
|
324
|
+
const units = (0, run_1.unitsOf)(t, pairs);
|
|
325
|
+
const tool = eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas);
|
|
326
|
+
const regrasSemgrep = regrasSemgrepPorIndice.get(i);
|
|
327
|
+
const trabalho = { tool, units, config,
|
|
328
|
+
incremental: true,
|
|
329
|
+
...(regrasSemgrep ? { regrasSemgrep } : {}),
|
|
314
330
|
...(nativos.has(t.name) ? { nativoPrimeiro: true } : {}),
|
|
315
331
|
ctx: { headTree, mainRoot, base, head, rangeCommits: count === null ? null : Number(count), deadline, ...(baseTree ? { baseTree } : {}) } };
|
|
316
332
|
const arquivo = path.join(dir, `${i}.json`);
|
|
@@ -160,6 +160,7 @@ function runDocker(inv) {
|
|
|
160
160
|
...Object.entries(tool.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]),
|
|
161
161
|
'--entrypoint', tool.containerBin ?? inv.argv[0], tool.image,
|
|
162
162
|
...inv.argv.slice(1).map(a => expand(a, values))];
|
|
163
|
+
(0, agendador_cpu_1.registrarContainerDaLane)(name);
|
|
163
164
|
inv.guard?.add('containers', name);
|
|
164
165
|
const r = (0, node_child_process_1.spawnSync)('docker', args, { encoding: 'utf8', timeout: inv.timeoutMs, killSignal: 'SIGKILL', maxBuffer: MAX_BUFFER, env: process.env });
|
|
165
166
|
const timedOut = r.error?.code === 'ETIMEDOUT';
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.lerCacheIncremental = void 0;
|
|
37
|
+
exports.resolverRegrasSemgrep = resolverRegrasSemgrep;
|
|
38
|
+
exports.consultarCacheIncremental = consultarCacheIncremental;
|
|
39
|
+
exports.gravarCacheIncremental = gravarCacheIncremental;
|
|
40
|
+
/** Cache de resultado normalizado do Agile, por conteúdo e fora do repositório. */
|
|
41
|
+
const crypto = __importStar(require("node:crypto"));
|
|
42
|
+
const node_child_process_1 = require("node:child_process");
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const run_1 = require("./run");
|
|
46
|
+
const CONTRATO = 'ork.security-scan.incremental/v1';
|
|
47
|
+
const LOCKFILES = new Set(['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'poetry.lock',
|
|
48
|
+
'Pipfile.lock', 'go.mod', 'go.sum']);
|
|
49
|
+
const sha256 = (valor) => crypto.createHash('sha256').update(valor).digest('hex');
|
|
50
|
+
const SCRIPT_DO_REGISTRO = String.raw `
|
|
51
|
+
# ORK_SEMGREP_RESOLVE_V1
|
|
52
|
+
import sys, urllib.request
|
|
53
|
+
pedido = urllib.request.Request(sys.argv[1], headers={"Accept": "application/json", "User-Agent": "orkastery-security-scan"})
|
|
54
|
+
with urllib.request.urlopen(pedido, timeout=20) as resposta:
|
|
55
|
+
if resposta.status != 200:
|
|
56
|
+
raise RuntimeError("HTTP " + str(resposta.status))
|
|
57
|
+
sys.stdout.buffer.write(resposta.read())
|
|
58
|
+
`;
|
|
59
|
+
/** O mesmo endpoint público para o qual o resolvedor do Semgrep mapeia um id do registro. */
|
|
60
|
+
function urlDoRegistro(referencia) {
|
|
61
|
+
if (/^[prs]\/[A-Za-z0-9._/-]+$/.test(referencia))
|
|
62
|
+
return `https://semgrep.dev/c/${referencia}`;
|
|
63
|
+
if (/^https:\/\//.test(referencia))
|
|
64
|
+
return referencia;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function baixarDoRegistro(ferramenta, url) {
|
|
68
|
+
const uid = process.getuid?.() ?? 65534;
|
|
69
|
+
const gid = process.getgid?.() ?? 65534;
|
|
70
|
+
const nome = `ork-security-scan-semgrep-registry-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
71
|
+
const r = (0, node_child_process_1.spawnSync)('docker', ['run', '--rm', '--name', nome, '--user', `${uid === 0 ? 65534 : uid}:${uid === 0 ? 65534 : gid}`,
|
|
72
|
+
'--network', 'default', '--entrypoint', 'python3', ferramenta.image, '-c', SCRIPT_DO_REGISTRO, url], { encoding: 'utf8', timeout: 30_000, maxBuffer: 64 * 1024 * 1024, killSignal: 'SIGKILL' });
|
|
73
|
+
if (r.error || r.status !== 0)
|
|
74
|
+
(0, node_child_process_1.spawnSync)('docker', ['rm', '-f', nome], { stdio: 'ignore', timeout: 10_000 });
|
|
75
|
+
return !r.error && r.status === 0 && r.stdout.trim() ? r.stdout : null;
|
|
76
|
+
}
|
|
77
|
+
/** O conteúdo que o próprio Semgrep extrai da resposta do registro (`rules` direto ou envelope `rule_config`). */
|
|
78
|
+
function conteudoResolvido(resposta) {
|
|
79
|
+
try {
|
|
80
|
+
const json = JSON.parse(resposta);
|
|
81
|
+
const regras = Object.prototype.hasOwnProperty.call(json, 'rules') ? json : json.rule_config;
|
|
82
|
+
return typeof regras === 'string' ? regras : JSON.stringify(regras);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return resposta;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const MARCAS_ENTRE_ARQUIVOS = ['join', 'interfile', 'crossfile', 'multifile'];
|
|
89
|
+
const MARCA_NA_LINHA = /(?:^|[\s{,])(?:mode\s*:\s*)?(?:join|inter[-_ ]?file|cross[-_ ]?file|multi[-_ ]?file)(?:\s*:|\s*$)/i;
|
|
90
|
+
const CHAVE_DA_LINHA = /^(\s*)(-\s+)?([A-Za-z_][\w-]*)\s*:(?:\s+(.*))?$/;
|
|
91
|
+
const normalizar = (chave) => chave.toLowerCase().replace(/[-_]/g, '');
|
|
92
|
+
/**
|
|
93
|
+
* Se a marca cruza arquivos de fato. `join` cruza em qualquer motor. `interfile` em `options` só cruza no motor Pro do
|
|
94
|
+
* Semgrep; em `metadata` é rótulo do registro, que o motor ignora. Em qualquer outro lugar, cruza (conservador).
|
|
95
|
+
*/
|
|
96
|
+
function marcaCruzaArquivos(marca, pai, pro) {
|
|
97
|
+
if (marca === 'join')
|
|
98
|
+
return true;
|
|
99
|
+
if (pai === 'metadata')
|
|
100
|
+
return false;
|
|
101
|
+
if (pai === 'options')
|
|
102
|
+
return pro;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* O registro responde em YAML. Toda linha com marca cruza arquivos, menos a chave da própria marca, em bloco e com valor
|
|
107
|
+
* booleano, cujo pai pela indentação a torna inócua no motor da rodada. Linha de texto que parece chave nunca relaxa nada.
|
|
108
|
+
*/
|
|
109
|
+
function marcaNoYaml(conteudo, pro) {
|
|
110
|
+
const pais = [];
|
|
111
|
+
for (const linha of conteudo.split(/\r?\n/)) {
|
|
112
|
+
const m = CHAVE_DA_LINHA.exec(linha);
|
|
113
|
+
const coluna = m ? m[1].length + (m[2]?.length ?? 0) : 0;
|
|
114
|
+
if (m)
|
|
115
|
+
while (pais.length && pais[pais.length - 1].coluna >= coluna)
|
|
116
|
+
pais.pop();
|
|
117
|
+
if (MARCA_NA_LINHA.test(linha)) {
|
|
118
|
+
const chave = m ? normalizar(m[3]) : null;
|
|
119
|
+
const booleano = !!m && /^(?:true|false|yes|no|on|off)?\s*(?:#.*)?$/i.test(m[4] ?? '');
|
|
120
|
+
if (!chave || !MARCAS_ENTRE_ARQUIVOS.includes(chave) || !booleano
|
|
121
|
+
|| marcaCruzaArquivos(chave, pais[pais.length - 1]?.chave ?? null, pro))
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (m)
|
|
125
|
+
pais.push({ coluna, chave: normalizar(m[3]) });
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
function regraEntreArquivos(conteudo, pro) {
|
|
130
|
+
let valor;
|
|
131
|
+
try {
|
|
132
|
+
valor = JSON.parse(conteudo);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return marcaNoYaml(conteudo, pro);
|
|
136
|
+
}
|
|
137
|
+
if (typeof valor === 'string')
|
|
138
|
+
return marcaNoYaml(valor, pro);
|
|
139
|
+
const visitar = (atual, pai) => {
|
|
140
|
+
if (Array.isArray(atual))
|
|
141
|
+
return atual.some((item) => visitar(item, pai));
|
|
142
|
+
if (!atual || typeof atual !== 'object')
|
|
143
|
+
return false;
|
|
144
|
+
return Object.entries(atual).some(([chave, filho]) => {
|
|
145
|
+
const normal = normalizar(chave);
|
|
146
|
+
if (normal === 'mode' && typeof filho === 'string' && /^(join|interfile|crossfile|multifile)$/i.test(filho))
|
|
147
|
+
return true;
|
|
148
|
+
if (MARCAS_ENTRE_ARQUIVOS.includes(normal) && filho !== false && filho !== null
|
|
149
|
+
&& marcaCruzaArquivos(normal, pai, pro))
|
|
150
|
+
return true;
|
|
151
|
+
return visitar(filho, normal);
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
return visitar(valor, null);
|
|
155
|
+
}
|
|
156
|
+
/** O Semgrep da rodada usa o motor Pro, o único que cruza arquivos nas regras `interfile`: `--pro*`, `semgrep ci` ou token. */
|
|
157
|
+
function motorPro(ferramenta, unidades) {
|
|
158
|
+
const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
|
|
159
|
+
const todos = [ferramenta.args, ...[...stacks].map((s) => ferramenta.argsByStack?.[s] ?? [])];
|
|
160
|
+
return todos.some((args) => args.some((a) => a.startsWith('--pro')) || args.includes('ci'))
|
|
161
|
+
|| Object.keys(ferramenta.env ?? {}).some((k) => /^SEMGREP_APP_TOKEN$/i.test(k));
|
|
162
|
+
}
|
|
163
|
+
function referenciasDasRegras(ferramenta, unidades) {
|
|
164
|
+
const stacks = new Set(unidades.map((u) => u.stack).filter((s) => !!s));
|
|
165
|
+
const referencias = new Set();
|
|
166
|
+
for (const stack of stacks) {
|
|
167
|
+
const args = ferramenta.argsByStack?.[stack] ?? [];
|
|
168
|
+
for (let i = 0; i < args.length - 1; i++)
|
|
169
|
+
if (args[i] === '--config' || args[i] === '-f')
|
|
170
|
+
referencias.add(args[++i]);
|
|
171
|
+
}
|
|
172
|
+
return [...referencias].sort();
|
|
173
|
+
}
|
|
174
|
+
function resolverRegrasSemgrep(ferramenta, unidades, baixar = baixarDoRegistro) {
|
|
175
|
+
const referencias = referenciasDasRegras(ferramenta, unidades);
|
|
176
|
+
if (referencias.length === 0)
|
|
177
|
+
return { reutilizavel: false, hash: null, motivo: 'regras-sem-referencia', referencias };
|
|
178
|
+
const resolvidas = [];
|
|
179
|
+
const pro = motorPro(ferramenta, unidades);
|
|
180
|
+
for (const referencia of referencias) {
|
|
181
|
+
const url = urlDoRegistro(referencia);
|
|
182
|
+
if (!url)
|
|
183
|
+
return { reutilizavel: false, hash: null, motivo: 'referencia-de-regra-nao-resolvivel', referencias };
|
|
184
|
+
const resposta = baixar(ferramenta, url);
|
|
185
|
+
if (!resposta)
|
|
186
|
+
return { reutilizavel: false, hash: null, motivo: 'registro-indisponivel', referencias };
|
|
187
|
+
const conteudo = conteudoResolvido(resposta);
|
|
188
|
+
if (regraEntreArquivos(conteudo, pro))
|
|
189
|
+
return { reutilizavel: false, hash: null, motivo: 'regra-entre-arquivos', referencias };
|
|
190
|
+
resolvidas.push([referencia, conteudo]);
|
|
191
|
+
}
|
|
192
|
+
return { reutilizavel: true, hash: sha256(JSON.stringify(resolvidas)), referencias };
|
|
193
|
+
}
|
|
194
|
+
/** Só habilita o que tem garantia explícita: Semgrep por arquivo e dependências por lockfile. */
|
|
195
|
+
function escopoDaFerramenta(t, trabalho) {
|
|
196
|
+
if (t.name === 'semgrep') {
|
|
197
|
+
// A API direta antecede o trabalhador e continua útil para conferir o retrato por arquivo isoladamente. No caminho
|
|
198
|
+
// real do Agile, `incremental: true` torna obrigatória a identidade das regras resolvidas: ausência, registro
|
|
199
|
+
// indisponível ou regra entre arquivos nunca cai nesta compatibilidade.
|
|
200
|
+
if (trabalho.incremental !== true)
|
|
201
|
+
return 'arquivo';
|
|
202
|
+
return trabalho.regrasSemgrep?.reutilizavel && trabalho.regrasSemgrep.hash ? 'arquivo' : null;
|
|
203
|
+
}
|
|
204
|
+
if (t.category === 'dependency')
|
|
205
|
+
return 'lockfile';
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function arquivos(root, escopo) {
|
|
209
|
+
const saida = {};
|
|
210
|
+
const visitar = (dir) => {
|
|
211
|
+
let nomes;
|
|
212
|
+
try {
|
|
213
|
+
nomes = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const nome of nomes) {
|
|
219
|
+
if (nome.name === '.git')
|
|
220
|
+
continue;
|
|
221
|
+
const absoluto = path.join(dir, nome.name);
|
|
222
|
+
if (nome.isDirectory()) {
|
|
223
|
+
visitar(absoluto);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!nome.isFile())
|
|
227
|
+
continue;
|
|
228
|
+
const relativo = path.relative(root, absoluto).split(path.sep).join('/');
|
|
229
|
+
const lockfile = LOCKFILES.has(nome.name) || /^requirements(?:\.[^.]+)?\.txt$/.test(nome.name);
|
|
230
|
+
if (escopo === 'lockfile' && !lockfile)
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
saida[relativo] = sha256(fs.readFileSync(absoluto));
|
|
234
|
+
}
|
|
235
|
+
catch { /* arquivo mudou durante a leitura: nao entra */ }
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
visitar(root);
|
|
239
|
+
return saida;
|
|
240
|
+
}
|
|
241
|
+
function identidade(trabalho, ferramenta) {
|
|
242
|
+
return sha256(JSON.stringify({ ferramenta, unidades: trabalho.units, configuracao: trabalho.config, base: trabalho.ctx.base,
|
|
243
|
+
regrasSemgrep: trabalho.regrasSemgrep?.hash ?? null }));
|
|
244
|
+
}
|
|
245
|
+
function localDaEntrada(chave) {
|
|
246
|
+
return path.join((0, run_1.cacheRoot)(), 'incremental', `${chave}.json`);
|
|
247
|
+
}
|
|
248
|
+
function consultarCacheIncremental(trabalho, ferramenta) {
|
|
249
|
+
const escopo = escopoDaFerramenta(ferramenta, trabalho);
|
|
250
|
+
if (!escopo)
|
|
251
|
+
return { escopo: null, atual: null, anterior: null, alterados: [], reutilizado: null };
|
|
252
|
+
const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
|
|
253
|
+
if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0) {
|
|
254
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
255
|
+
}
|
|
256
|
+
const chave = identidade(trabalho, ferramenta);
|
|
257
|
+
try {
|
|
258
|
+
const entrada = JSON.parse(fs.readFileSync(localDaEntrada(chave), 'utf8'));
|
|
259
|
+
if (entrada.contrato !== CONTRATO || entrada.chave !== chave || entrada.resultado?.run?.state !== 'ran') {
|
|
260
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
261
|
+
}
|
|
262
|
+
const nomes = new Set([...Object.keys(entrada.retrato.arquivos), ...Object.keys(retrato.arquivos)]);
|
|
263
|
+
const alterados = [...nomes].filter((nome) => entrada.retrato.arquivos[nome] !== retrato.arquivos[nome]).sort();
|
|
264
|
+
return { escopo, atual: retrato, anterior: entrada.resultado, alterados,
|
|
265
|
+
reutilizado: alterados.length === 0 ? entrada.resultado : null };
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { escopo, atual: retrato, anterior: null, alterados: [], reutilizado: null };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const lerCacheIncremental = (trabalho, ferramenta) => consultarCacheIncremental(trabalho, ferramenta).reutilizado;
|
|
272
|
+
exports.lerCacheIncremental = lerCacheIncremental;
|
|
273
|
+
function gravarCacheIncremental(trabalho, ferramenta, resultado) {
|
|
274
|
+
const escopo = escopoDaFerramenta(ferramenta, trabalho);
|
|
275
|
+
if (!escopo || resultado.run.state !== 'ran')
|
|
276
|
+
return;
|
|
277
|
+
const retrato = { escopo, arquivos: arquivos(trabalho.ctx.headTree, escopo) };
|
|
278
|
+
if (escopo === 'lockfile' && Object.keys(retrato.arquivos).length === 0)
|
|
279
|
+
return;
|
|
280
|
+
const chave = identidade(trabalho, ferramenta), arquivo = localDaEntrada(chave);
|
|
281
|
+
try {
|
|
282
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true, mode: 0o700 });
|
|
283
|
+
const temporario = `${arquivo}.${process.pid}.tmp`;
|
|
284
|
+
const entrada = { contrato: CONTRATO, chave, retrato, resultado };
|
|
285
|
+
fs.writeFileSync(temporario, JSON.stringify(entrada), { mode: 0o600 });
|
|
286
|
+
fs.renameSync(temporario, arquivo);
|
|
287
|
+
}
|
|
288
|
+
catch { /* cache ausente custa CPU, mas nunca muda o veredito */ }
|
|
289
|
+
}
|
|
@@ -41,9 +41,11 @@ exports.comCacheNoHost = comCacheNoHost;
|
|
|
41
41
|
* `runScanAgil`, um trabalhador por ferramenta, ao mesmo tempo.
|
|
42
42
|
*/
|
|
43
43
|
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
44
45
|
const executor_1 = require("./executor");
|
|
45
46
|
const guard_1 = require("./guard");
|
|
46
47
|
const run_1 = require("./run");
|
|
48
|
+
const incremental_1 = require("./incremental");
|
|
47
49
|
/**
|
|
48
50
|
* S6 do CHECK: o binario nativo usa o cache por usuario que o Docker monta (o caminho do host no lugar do caminho do
|
|
49
51
|
* container), e nao o `/tmp` compartilhado do catalogo, onde outro usuario da maquina deixaria um banco do Trivy.
|
|
@@ -51,10 +53,49 @@ const run_1 = require("./run");
|
|
|
51
53
|
function comCacheNoHost(t) {
|
|
52
54
|
if (!t.cacheMounts?.length)
|
|
53
55
|
return t;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const trocar = (args) => {
|
|
57
|
+
let resposta = args;
|
|
58
|
+
for (const m of t.cacheMounts ?? [])
|
|
59
|
+
resposta = resposta.map((a) => a.split(m.container).join(m.host));
|
|
60
|
+
return resposta;
|
|
61
|
+
};
|
|
62
|
+
return { ...t, args: trocar(t.args), ...(t.argsByStack
|
|
63
|
+
? { argsByStack: Object.fromEntries(Object.entries(t.argsByStack).map(([stack, args]) => [stack, trocar(args ?? [])])) }
|
|
64
|
+
: {}) };
|
|
65
|
+
}
|
|
66
|
+
/** Semgrep é a garantia por arquivo: roda apenas conteúdo mudado e conserva os achados dos demais arquivos. */
|
|
67
|
+
function semgrepIncremental(trabalho, ferramenta, consulta, executar) {
|
|
68
|
+
if (ferramenta.name !== 'semgrep' || !consulta.anterior || consulta.escopo !== 'arquivo' || !ferramenta.argsByStack)
|
|
69
|
+
return null;
|
|
70
|
+
if (new Set(trabalho.units.map((u) => u.stack)).size !== trabalho.units.length)
|
|
71
|
+
return null;
|
|
72
|
+
const existentes = consulta.alterados.filter((arquivo) => {
|
|
73
|
+
try {
|
|
74
|
+
return fs.statSync(path.join(trabalho.ctx.headTree, arquivo)).isFile();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const argsByStack = {};
|
|
81
|
+
const unidades = trabalho.units.filter((unidade) => {
|
|
82
|
+
const prefixo = unidade.dir === '.' ? '' : `${unidade.dir.replace(/\\/g, '/')}/`;
|
|
83
|
+
const alvos = existentes.filter((arquivo) => !prefixo || arquivo.startsWith(prefixo));
|
|
84
|
+
const base = unidade.stack ? ferramenta.argsByStack?.[unidade.stack] : undefined;
|
|
85
|
+
if (!base?.length || alvos.length === 0 || !unidade.stack)
|
|
86
|
+
return false;
|
|
87
|
+
argsByStack[unidade.stack] = [...base.slice(0, -1), ...alvos.map((arquivo) => `{src}/${arquivo}`)];
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
const mudaram = new Set(consulta.alterados);
|
|
91
|
+
const preservados = consulta.anterior.head.filter((achado) => !mudaram.has(achado.file));
|
|
92
|
+
if (unidades.length === 0)
|
|
93
|
+
return { ...consulta.anterior, head: preservados };
|
|
94
|
+
const parcial = executar({ ...ferramenta, argsByStack }, unidades, true);
|
|
95
|
+
if (parcial.run.state !== 'ran')
|
|
96
|
+
return parcial;
|
|
97
|
+
const novos = parcial.head.filter((achado) => mudaram.has(achado.file));
|
|
98
|
+
return { ...parcial, head: [...preservados, ...novos], base: consulta.anterior.base };
|
|
58
99
|
}
|
|
59
100
|
if (require.main === module) {
|
|
60
101
|
const trabalho = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
@@ -65,7 +106,14 @@ if (require.main === module) {
|
|
|
65
106
|
// chooseExecutor escolher o nativo); sem ele, o Docker, como no Classic.
|
|
66
107
|
const sonda = (0, executor_1.defaultProbe)();
|
|
67
108
|
const probe = trabalho.nativoPrimeiro ? { ...sonda, dockerCli: () => false, dockerDaemon: () => false } : sonda;
|
|
68
|
-
const
|
|
109
|
+
const ferramenta = trabalho.nativoPrimeiro ? comCacheNoHost(trabalho.tool) : trabalho.tool;
|
|
110
|
+
const executar = (tool, units, somenteHead = false) => (0, run_1.scanTool)(tool, units, trabalho.config, { ...resto, ...(somenteHead ? { base: resto.head, baseTree: resto.headTree, rangeCommits: 0 } : {}),
|
|
111
|
+
deadline: deadline ?? Infinity, probe, guard, images: new Map() });
|
|
112
|
+
const consulta = trabalho.incremental ? (0, incremental_1.consultarCacheIncremental)(trabalho, ferramenta) : null;
|
|
113
|
+
const r = consulta?.reutilizado ?? (consulta ? semgrepIncremental(trabalho, ferramenta, consulta, executar) : null)
|
|
114
|
+
?? executar(ferramenta, trabalho.units);
|
|
115
|
+
if (trabalho.incremental && !consulta?.reutilizado)
|
|
116
|
+
(0, incremental_1.gravarCacheIncremental)(trabalho, ferramenta, r);
|
|
69
117
|
process.stdout.write(JSON.stringify(r));
|
|
70
118
|
}
|
|
71
119
|
finally {
|
package/dist/verify.js
CHANGED
|
@@ -448,10 +448,21 @@ function executorDoGrupo(manifesto, ambienteDaRodada, instabilidade, leitor, con
|
|
|
448
448
|
const processos = (0, check_paralelo_1.rodarEmParalelo)(paraOAuxiliar, ambienteDoVerify(), medicaoCpu ? { medicaoCpu } : {});
|
|
449
449
|
if (processos.agendamentoCpu) {
|
|
450
450
|
const p = processos.agendamentoCpu;
|
|
451
|
+
const medida = p.medicaoCpuIntegracao;
|
|
452
|
+
const evidencia = medida
|
|
453
|
+
? `; CPU da integracao: arvore ${Math.round(medida.arvoreMs)} ms + conteineres ${Math.round(medida.containersMs)} ms` +
|
|
454
|
+
` = ${Math.round(medida.totalMs)} ms, pico ${medida.picoCpus ?? 'indisponivel'} CPU, ` +
|
|
455
|
+
(medida.completa ? 'medida completa' : medida.anomalia
|
|
456
|
+
? `medida anomala (${medida.anomalia})`
|
|
457
|
+
: `medida incompleta (${medida.motivosDosAusentes.map((x) => `${x.container}: ${x.motivo}`).join('; ')})`)
|
|
458
|
+
: '';
|
|
451
459
|
executar.pecas = [{ peca: 'agendador-cpu', estado: p.saturado ? 'fallback' : 'agile',
|
|
452
460
|
motivo: p.saturado
|
|
453
461
|
? `${p.motivo}; hermetica com ${p.cpusHermetica}/${p.cpus} CPUs, varredura e integracao depois dela`
|
|
454
|
-
: `${p.cpusHermetica}/${p.cpus} CPUs na hermetica
|
|
462
|
+
: `${p.cpusHermetica}/${p.cpus} CPUs na hermetica; depois da varredura ${p.cpusHermeticaAposVarredura}` +
|
|
463
|
+
` e depois da integracao ${p.cpusHermeticaAposReserva}; reserva da integracao ${p.fonteCpusIntegracao}` +
|
|
464
|
+
` (${p.cpusIntegracao} CPU), ${p.integracaoEmParalelo ? 'em paralelo com a varredura' : 'encadeada depois da varredura'}` +
|
|
465
|
+
`; conteineres com ${p.cpusPorContainer} CPU${evidencia}`,
|
|
455
466
|
...(p.saturado ? { fallback: 'ordem do Classic para hermetica, varredura e integracao' } : {}) }];
|
|
456
467
|
}
|
|
457
468
|
const stealPct = janela.fechar();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkastery/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "A software factory of AI agents that proves its work: parallel threads, verified results, and short decisions only when they matter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/orkastery/orkastery#readme",
|