@orkastery/cli 0.5.6 → 0.6.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/adapters/README.md +18 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +1 -0
- package/adapters/claude-code/README.md +37 -5
- package/adapters/claude-code/agents/ork-check.md +1 -0
- package/adapters/claude-code/agents/ork-go.md +1 -0
- package/adapters/claude-code/agents/ork-goal.md +1 -0
- package/adapters/claude-code/agents/ork-master.md +1 -0
- package/adapters/claude-code/agents/ork-plan.md +1 -0
- package/adapters/claude-code/agents/ork-ship.md +1 -0
- package/adapters/claude-code/hooks/hooks.json +2 -1
- package/adapters/claude-code/hooks/ork-guard.js +15 -1
- package/adapters/claude-code/hooks/ork-resposta.js +91 -0
- package/adapters/claude-code/hooks/ork-sensor.js +18 -2
- package/adapters/claude-code/hooks/quota-signal.js +14 -0
- package/adapters/claude-code/mods/ork-mensageiro/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-mensageiro/hooks/caixa.ts +215 -0
- package/adapters/claude-code/mods/ork-mensageiro/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-mensageiro/hooks/painel.tsx +165 -0
- package/adapters/claude-code/mods/ork-mensageiro/hooks/recepcao.ts +256 -0
- package/adapters/claude-code/mods/ork-mensageiro/hooks/register.tsx +481 -0
- package/adapters/claude-code/mods/ork-mensageiro/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-mensageiro/types/index.d.ts +85 -0
- package/adapters/claude-code/mods/ork-pista/hooks/ci.ts +9 -5
- package/adapters/claude-code/mods/ork-ritual/.claude-plugin/plugin.json +14 -0
- package/adapters/claude-code/mods/ork-ritual/hooks/hooks.json +3 -0
- package/adapters/claude-code/mods/ork-ritual/hooks/nota.ts +88 -0
- package/adapters/claude-code/mods/ork-ritual/hooks/register.tsx +494 -0
- package/adapters/claude-code/mods/ork-ritual/hooks/replay.tsx +271 -0
- package/adapters/claude-code/mods/ork-ritual/hooks/ritual.ts +218 -0
- package/adapters/claude-code/mods/ork-ritual/orkastery-mod.json +5 -0
- package/adapters/claude-code/mods/ork-ritual/types/index.d.ts +106 -0
- package/adapters/claude-code/mods/ork-sentinela/hooks/vigia.ts +1 -1
- package/adapters/codex/skills/ork/SKILL.md +2 -0
- package/adapters/hermes/README.md +64 -1
- package/adapters/hermes/bin/ork-conhecimento-colecoes.sh +14 -0
- package/adapters/hermes/bin/ork-conhecimento-listar.sh +15 -0
- package/adapters/hermes/bin/ork-conhecimento-mostrar.sh +16 -0
- package/adapters/hermes/bin/ork-conhecimento-verificar.sh +15 -0
- package/adapters/hermes/bin/ork-vigia-batida.py +127 -0
- package/adapters/hermes/hermes.plugin.json +4 -0
- package/adapters/hermes/hitl-ingress/__init__.py +101 -8
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +2 -2
- package/adapters/openclaw/README.md +81 -9
- package/adapters/openclaw/bin/ork-vigia-batida.js +86 -0
- package/adapters/openclaw/dist/hitl-ingress.js +106 -9
- package/adapters/openclaw/dist/index.js +86 -0
- package/adapters/openclaw/openclaw.plugin.json +34 -4
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +103 -9
- package/adapters/openclaw/src/index.ts +94 -0
- package/assets/adapters-en/claude-code/README.md +403 -0
- package/assets/adapters-en/claude-code/agents/ork-check.md +33 -0
- package/assets/adapters-en/claude-code/agents/ork-go.md +33 -0
- package/assets/adapters-en/claude-code/agents/ork-goal.md +34 -0
- package/assets/adapters-en/claude-code/agents/ork-master.md +35 -0
- package/assets/adapters-en/claude-code/agents/ork-plan.md +33 -0
- package/assets/adapters-en/claude-code/agents/ork-ship.md +33 -0
- package/assets/adapters-en/claude-code/commands/check.md +46 -0
- package/assets/adapters-en/claude-code/commands/go.md +46 -0
- package/assets/adapters-en/claude-code/commands/goal.md +55 -0
- package/assets/adapters-en/claude-code/commands/master.md +54 -0
- package/assets/adapters-en/claude-code/commands/onboarding.md +22 -0
- package/assets/adapters-en/claude-code/commands/ork.md +126 -0
- package/assets/adapters-en/claude-code/commands/plan.md +47 -0
- package/assets/adapters-en/claude-code/commands/ship.md +45 -0
- package/assets/adapters-en/codex/skills/ork/SKILL.md +85 -0
- package/assets/adapters-en/hermes/README.md +273 -0
- package/assets/adapters-en/hermes/skills/orkastery-devmaster/SKILL.md +169 -0
- package/assets/references-en/README.md +25 -0
- package/assets/references-en/code-review-axes.md +82 -0
- package/assets/references-en/definition-of-done.md +74 -0
- package/assets/references-en/performance-checklist.md +53 -0
- package/assets/references-en/security-checklist.md +101 -0
- package/assets/references-en/testing-patterns.md +51 -0
- package/assets/security-scan/defaults/.golangci.yml +2 -0
- package/assets/security-scan/defaults/eslint.config.mjs +11 -0
- package/assets/security-scan/defaults/gitleaks.toml +3 -0
- package/assets/security-scan/defaults/ruff.toml +1 -0
- package/assets/security-scan/tools-agil.json +87 -0
- package/assets/security-scan/tools.json +625 -0
- package/assets/skills-en/core/onboarding/SKILL.md +58 -0
- package/assets/skills-en/core/orkastery-bootstrap/SKILL.md +119 -0
- package/assets/skills-en/core/thread-state/SKILL.md +88 -0
- package/assets/skills-en/governance/decision-triage/SKILL.md +65 -0
- package/assets/skills-en/governance/narrative-guardian/SKILL.md +66 -0
- package/assets/skills-en/governance/roadmap-keeper/SKILL.md +60 -0
- package/assets/skills-en/governance/scope-check-capability-map/SKILL.md +71 -0
- package/assets/skills-en/observability/thread-tracing/SKILL.md +63 -0
- package/assets/skills-en/phases/check-quality/SKILL.md +103 -0
- package/assets/skills-en/phases/go-implementation/SKILL.md +78 -0
- package/assets/skills-en/phases/goal-definition/SKILL.md +69 -0
- package/assets/skills-en/phases/master-metrics/SKILL.md +72 -0
- package/assets/skills-en/phases/plan-specification/SKILL.md +67 -0
- package/assets/skills-en/phases/security-remediation/SKILL.md +70 -0
- package/assets/skills-en/phases/ship-release/SKILL.md +71 -0
- package/assets/skills-en/reviewers/code-reviewer/SKILL.md +61 -0
- package/assets/skills-en/reviewers/security-auditor/SKILL.md +93 -0
- package/assets/skills-en/reviewers/test-engineer/SKILL.md +66 -0
- package/assets/skills-en/reviewers/web-performance-auditor/SKILL.md +63 -0
- package/dist/adapters/claude-bg.js +118 -4
- package/dist/adapters/codex-controller-worker.js +12 -5
- package/dist/adapters/codex-events.js +16 -4
- package/dist/adapters/codex.js +3 -1
- package/dist/atualizar-branch.js +245 -0
- package/dist/auditoria.js +13 -11
- package/dist/auditrun.js +51 -46
- package/dist/autorizacao-duravel.js +327 -0
- package/dist/board.js +94 -40
- package/dist/branch-de-estado.js +16 -8
- package/dist/busca-semantica.js +7 -5
- package/dist/catalogo.js +2 -1
- package/dist/changelog-reconciliacao.js +290 -0
- package/dist/check-agil.js +500 -0
- package/dist/check-modo.js +41 -0
- package/dist/check-paralelo.js +183 -0
- package/dist/ci.js +248 -45
- package/dist/ciclos.js +8 -6
- package/dist/claim-lint.js +12 -8
- package/dist/claims-conferir.js +47 -39
- package/dist/claims.js +10 -9
- package/dist/company-brain-client.js +10 -3
- package/dist/company-brain-migration.js +11 -5
- package/dist/conducao-batida.js +759 -0
- package/dist/conducao-sentinela.js +25 -8
- package/dist/conducao.js +84 -29
- package/dist/conhecimento-consulta.js +270 -0
- package/dist/conhecimento.js +111 -87
- package/dist/conta-esgotada.js +127 -0
- package/dist/dado-privado.js +816 -0
- package/dist/decisao-autonoma.js +5 -4
- package/dist/demo.js +14 -12
- package/dist/dependencias-da-worktree.js +25 -0
- package/dist/divida.js +38 -36
- package/dist/docs.js +81 -74
- package/dist/doctor-forja.js +345 -0
- package/dist/doctor-pulse-cron.js +113 -1
- package/dist/doctor.js +164 -6
- package/dist/dsn-da-memoria.js +417 -0
- package/dist/entrega-por-mod.js +503 -0
- package/dist/entrega-pr.js +143 -43
- package/dist/esgotamentos.js +13 -11
- package/dist/estado-thread.js +21 -18
- package/dist/evalrunner.js +35 -32
- package/dist/fabrica-batida.js +34 -21
- package/dist/fabrica-estado.js +13 -10
- package/dist/fix.js +21 -19
- package/dist/forja/adaptador.js +6 -0
- package/dist/forja/ambiente-de-ci.js +27 -0
- package/dist/forja/api.js +256 -0
- package/dist/forja/cache.js +89 -0
- package/dist/forja/cli.js +60 -0
- package/dist/forja/contrato.js +9 -0
- package/dist/forja/endereco.js +43 -0
- package/dist/forja/estado.js +22 -0
- package/dist/forja/executor.js +73 -0
- package/dist/forja/github/ci.js +95 -0
- package/dist/forja/github/dono-da-rede.js +18 -0
- package/dist/forja/github/metadados.js +15 -0
- package/dist/forja/github/pedidos.js +200 -0
- package/dist/forja/gitlab/ci.js +141 -0
- package/dist/forja/gitlab/consultas.js +59 -0
- package/dist/forja/gitlab/dono-da-rede.js +43 -0
- package/dist/forja/gitlab/metadados.js +15 -0
- package/dist/forja/gitlab/pedidos-abertos.js +158 -0
- package/dist/forja/gitlab/pedidos.js +141 -0
- package/dist/forja/produtor.js +32 -0
- package/dist/forja/registro.js +31 -0
- package/dist/forja/resolver.js +121 -0
- package/dist/forja/texto.js +13 -0
- package/dist/forja/versao.js +71 -0
- package/dist/forja.js +123 -80
- package/dist/handoff.js +23 -13
- package/dist/hitl-contract.js +46 -43
- package/dist/hitl-curto.js +37 -25
- package/dist/hitl-gates.js +13 -11
- package/dist/hitl-local-receipt.js +51 -0
- package/dist/hitl-local.js +11 -2
- package/dist/hitl-lock.js +5 -11
- package/dist/hitl-public-receipt.js +11 -0
- package/dist/hitl-resumo.js +44 -1
- package/dist/hitl-sessions.js +62 -9
- package/dist/hitl-texto-livre.js +7 -4
- package/dist/hitl.js +34 -31
- package/dist/horario.js +16 -0
- package/dist/hosts.js +104 -134
- package/dist/index.js +848 -548
- package/dist/indice-vetorial.js +14 -12
- package/dist/init.js +11 -2
- package/dist/integracoes-locais.js +2 -0
- package/dist/intelligence-graph-cli.js +123 -69
- package/dist/intelligence-graph-query.js +37 -22
- package/dist/leases.js +19 -18
- package/dist/ledger-stats.js +17 -16
- package/dist/ledger.js +31 -1
- package/dist/licoes.js +12 -11
- package/dist/locale-accounts.js +220 -0
- package/dist/locale-adapter.js +291 -0
- package/dist/locale-ajuda.js +76 -4
- package/dist/locale-audit.js +199 -0
- package/dist/locale-claims.js +213 -0
- package/dist/locale-conducao.js +138 -0
- package/dist/locale-conhecimento.js +181 -0
- package/dist/locale-decisao.js +205 -0
- package/dist/locale-docs.js +230 -0
- package/dist/locale-estado.js +101 -0
- package/dist/locale-forja.js +290 -0
- package/dist/locale-gerados.js +5 -0
- package/dist/locale-grafo.js +129 -0
- package/dist/locale-master.js +83 -0
- package/dist/locale-network.js +524 -0
- package/dist/locale-operacao-restante.js +931 -0
- package/dist/locale-projetos.js +100 -0
- package/dist/locale-prompts.js +194 -0
- package/dist/locale-pulse.js +140 -2
- package/dist/locale-reservas.js +87 -0
- package/dist/locale-retry.js +145 -6
- package/dist/locale-security-scan.js +69 -0
- package/dist/locale-setup.js +192 -0
- package/dist/locale-ship.js +169 -66
- package/dist/locale-thread.js +149 -5
- package/dist/locale-verify.js +80 -4
- package/dist/locale-worktree.js +246 -0
- package/dist/locale.js +553 -9
- package/dist/maestro-runtime.js +22 -3
- package/dist/manifest.js +287 -11
- package/dist/maquina-ociosa.js +211 -0
- package/dist/maquina.js +10 -5
- package/dist/master-nota-local.js +200 -0
- package/dist/master-nota.js +10 -2
- package/dist/master.js +118 -41
- package/dist/mcp-conhecimento.js +52 -0
- package/dist/mcp-experiencia.js +26 -5
- package/dist/mcp-install.js +73 -8
- package/dist/mcp-noctum.js +101 -0
- package/dist/mcp-server.js +82 -14
- package/dist/mcp-ship-transporte.js +977 -0
- package/dist/mcp-ship.js +68 -24
- package/dist/mcp-transportes.js +17 -0
- package/dist/memoria.js +63 -58
- package/dist/modos-migracao.js +8 -6
- package/dist/modos.js +17 -18
- package/dist/network-roadmap.js +85 -49
- package/dist/noctum-aceite.js +789 -0
- package/dist/noctum-arco.js +540 -33
- package/dist/noctum-capacidade.js +3 -3
- package/dist/noctum-changelog.js +399 -0
- package/dist/noctum-cli.js +482 -0
- package/dist/noctum-coordenacao.js +1225 -0
- package/dist/noctum-entrega.js +1169 -0
- package/dist/noctum-envelope.js +252 -16
- package/dist/noctum-execucao.js +95 -23
- package/dist/noctum-fechamento.js +405 -0
- package/dist/noctum-fila.js +43 -9
- package/dist/noctum-guarda.js +256 -0
- package/dist/noctum-integracao.js +507 -0
- package/dist/noctum-maquinas.js +383 -0
- package/dist/noctum-medicao.js +450 -0
- package/dist/noctum-modo.js +362 -0
- package/dist/noctum-pergunta-canal.js +96 -0
- package/dist/noctum-pergunta.js +115 -0
- package/dist/noctum-provas.js +838 -0
- package/dist/noctum-reconciliacao.js +591 -0
- package/dist/noctum-registro.js +215 -0
- package/dist/noctum-relatorio.js +827 -0
- package/dist/noctum-resposta.js +207 -0
- package/dist/noctum-sessoes-em-voo.js +93 -0
- package/dist/noctum-supervisao.js +649 -0
- package/dist/noctum-terminal.js +459 -0
- package/dist/noctum-transporte.js +655 -0
- package/dist/noctum-vigia.js +639 -0
- package/dist/ocupacao.js +3 -1
- package/dist/onboarding.js +16 -5
- package/dist/orkmind.js +39 -23
- package/dist/orquestracao.js +31 -35
- package/dist/parado-no-condutor.js +146 -124
- package/dist/phase.js +53 -27
- package/dist/policies.js +33 -0
- package/dist/postura-local.js +4 -3
- package/dist/procedencia.js +9 -14
- package/dist/projeto-alvo.js +59 -20
- package/dist/prompts.js +136 -42
- package/dist/prova-minima.js +5 -6
- package/dist/pulse-cadencia.js +5 -4
- package/dist/pulse-consentimento.js +53 -1
- package/dist/pulse-delivery.js +143 -27
- package/dist/pulse-resposta.js +368 -4
- package/dist/pulse.js +136 -3
- package/dist/ratelimit.js +12 -4
- package/dist/recall.js +14 -13
- package/dist/rede-adesao.js +13 -8
- package/dist/rede-forja.js +108 -24
- package/dist/rede-status.js +63 -45
- package/dist/rede.js +86 -46
- package/dist/retomada-checkpoint.js +101 -0
- package/dist/retry.js +228 -73
- package/dist/roadmap-reservas.js +32 -32
- package/dist/runtime-ambiente.js +5 -0
- package/dist/runtime-context.js +117 -43
- package/dist/runtime-escada.js +423 -0
- package/dist/runtime-profiles.js +103 -46
- package/dist/saida-segura.js +3 -2
- package/dist/security-scan/agil.js +355 -0
- package/dist/security-scan/cli.js +76 -0
- package/dist/security-scan/compare.js +149 -0
- package/dist/security-scan/config.js +93 -0
- package/dist/security-scan/detect.js +97 -0
- package/dist/security-scan/executor.js +188 -0
- package/dist/security-scan/guard.js +180 -0
- package/dist/security-scan/imagem-de-lint.js +96 -0
- package/dist/security-scan/normalize/actionlint.js +12 -0
- package/dist/security-scan/normalize/eslint.js +12 -0
- package/dist/security-scan/normalize/gitleaks.js +13 -0
- package/dist/security-scan/normalize/golangci-lint.js +13 -0
- package/dist/security-scan/normalize/govulncheck.js +36 -0
- package/dist/security-scan/normalize/index.js +25 -0
- package/dist/security-scan/normalize/npm-audit.js +30 -0
- package/dist/security-scan/normalize/osv-scanner.js +32 -0
- package/dist/security-scan/normalize/oxlint.js +51 -0
- package/dist/security-scan/normalize/pip-audit.js +16 -0
- package/dist/security-scan/normalize/ruff.js +12 -0
- package/dist/security-scan/normalize/semgrep.js +20 -0
- package/dist/security-scan/normalize/trivy.js +33 -0
- package/dist/security-scan/normalize/util.js +204 -0
- package/dist/security-scan/normalize/zizmor.js +19 -0
- package/dist/security-scan/report.js +156 -0
- package/dist/security-scan/run.js +448 -0
- package/dist/security-scan/tools.js +62 -0
- package/dist/security-scan/trabalhador.js +74 -0
- package/dist/security-scan/types.js +15 -0
- package/dist/session-events.js +19 -2
- package/dist/session-watcher-claude.js +74 -19
- package/dist/session-watcher.js +156 -10
- package/dist/sessoes-contas.js +9 -0
- package/dist/sessoes-inventario.js +1 -1
- package/dist/sessoes.js +25 -23
- package/dist/setup.js +59 -58
- package/dist/ship-publicar.js +263 -92
- package/dist/ship.js +160 -17
- package/dist/superficie.js +14 -11
- package/dist/thread-close.js +9 -9
- package/dist/thread.js +121 -18
- package/dist/tokens.js +15 -11
- package/dist/trabalho-thread.js +297 -0
- package/dist/transporte-do-canal.js +189 -0
- package/dist/types.js +12 -1
- package/dist/util.js +5 -2
- package/dist/varredura-acoes.js +507 -0
- package/dist/verify.js +472 -29
- package/dist/worktree-local.js +12 -10
- package/dist/worktree.js +81 -85
- package/eval/casos/check-quality.json +77 -0
- package/eval/casos/security-auditor.json +148 -0
- package/eval/casos/security-remediation.json +161 -0
- package/eval/fixtures/security-scan/README.md +8 -0
- package/eval/fixtures/security-scan/go/.github/workflows/ci.yml +12 -0
- package/eval/fixtures/security-scan/go/Dockerfile +4 -0
- package/eval/fixtures/security-scan/go/README.md +4 -0
- package/eval/fixtures/security-scan/go/config.go +4 -0
- package/eval/fixtures/security-scan/go/go.mod +5 -0
- package/eval/fixtures/security-scan/go/go.sum +2 -0
- package/eval/fixtures/security-scan/go/main.go +31 -0
- package/eval/fixtures/security-scan/js/.github/workflows/ci.yml +12 -0
- package/eval/fixtures/security-scan/js/Dockerfile +3 -0
- package/eval/fixtures/security-scan/js/README.md +4 -0
- package/eval/fixtures/security-scan/js/package-lock.json +844 -0
- package/eval/fixtures/security-scan/js/package.json +10 -0
- package/eval/fixtures/security-scan/js/src/config.js +4 -0
- package/eval/fixtures/security-scan/js/src/server.js +17 -0
- package/eval/fixtures/security-scan/js/src/util.ts +4 -0
- package/eval/fixtures/security-scan/js/tsconfig.json +4 -0
- package/eval/fixtures/security-scan/python/.github/workflows/ci.yml +12 -0
- package/eval/fixtures/security-scan/python/Dockerfile +3 -0
- package/eval/fixtures/security-scan/python/README.md +4 -0
- package/eval/fixtures/security-scan/python/app/__init__.py +0 -0
- package/eval/fixtures/security-scan/python/app/config.py +2 -0
- package/eval/fixtures/security-scan/python/app/main.py +19 -0
- package/eval/fixtures/security-scan/python/requirements.txt +1 -0
- package/package.json +9 -4
- package/references/security-checklist.md +3 -3
- package/skills/README.md +1 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/check-quality/SKILL.md +16 -0
- package/skills/phases/go-implementation/SKILL.md +6 -0
- package/skills/phases/security-remediation/SKILL.md +70 -0
- package/skills/reviewers/security-auditor/SKILL.md +25 -0
|
@@ -0,0 +1,355 @@
|
|
|
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.PRAZO_MINIMO_DO_CLASSIC_MS = exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = exports.PRAZO_POR_REGRA_DO_SEMGREP_S = exports.SEMGREP_SEM_CACHE = void 0;
|
|
37
|
+
exports.especificacaoDoAgile = especificacaoDoAgile;
|
|
38
|
+
exports.estourouNoAgile = estourouNoAgile;
|
|
39
|
+
exports.motivoSemCredencial = motivoSemCredencial;
|
|
40
|
+
exports.resultadoDoTrabalhador = resultadoDoTrabalhador;
|
|
41
|
+
exports.catalogoDoAgile = catalogoDoAgile;
|
|
42
|
+
exports.runScanAgil = runScanAgil;
|
|
43
|
+
/**
|
|
44
|
+
* RM-072 (D9, D10 da ork-check5x): a varredura do Agile Check.
|
|
45
|
+
*
|
|
46
|
+
* A rodada e a mesma do Classic (`runScan`): a arvore do HEAD por worktree destacada, a base resolvida igual, as mesmas
|
|
47
|
+
* ferramentas do catalogo e a mesma classificacao contra a base. O que muda e o empacotamento: a arvore da base sai uma
|
|
48
|
+
* vez, antes, e cada ferramenta roda num trabalhador proprio (`trabalhador.ts`), ao mesmo tempo, chamando o mesmo
|
|
49
|
+
* `scanTool` do Classic; o relatorio sai na ordem do catalogo. As ferramentas do Trivy montam um diretorio persistente
|
|
50
|
+
* por ferramenta no `/tmp/trivy-cache` do catalogo, para o banco nao descer a cada rodada (cache de banco, nunca de
|
|
51
|
+
* resultado: o HEAD e sempre varrido de novo, e a base usa o cache por SHA que o `scanBase` ja tem, com a chave da
|
|
52
|
+
* especificacao do Agile). Cada peca do Agile que nao pode rodar cai para o equivalente do Classic e vai ao relatorio
|
|
53
|
+
* (`check.pecas`) e ao `ORK_CHECK_RELATORIO` do verify.
|
|
54
|
+
*/
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const os = __importStar(require("node:os"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const check_paralelo_1 = require("../check-paralelo");
|
|
59
|
+
const check_modo_1 = require("../check-modo");
|
|
60
|
+
const estado_thread_1 = require("../estado-thread");
|
|
61
|
+
const redacao_url_1 = require("../redacao-url");
|
|
62
|
+
const compare_1 = require("./compare");
|
|
63
|
+
const config_1 = require("./config");
|
|
64
|
+
const guard_1 = require("./guard");
|
|
65
|
+
const run_1 = require("./run");
|
|
66
|
+
const executor_1 = require("./executor");
|
|
67
|
+
const imagem_de_lint_1 = require("./imagem-de-lint");
|
|
68
|
+
const tools_1 = require("./tools");
|
|
69
|
+
const TRABALHADOR = path.join(__dirname, 'trabalhador.js');
|
|
70
|
+
/** Sem prazo da rodada, o trabalhador ganha o maior prazo do catalogo para a HEAD e para a base, com folga. */
|
|
71
|
+
const FOLGA_MS = 60_000;
|
|
72
|
+
const aspas = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
73
|
+
/**
|
|
74
|
+
* O Semgrep baixa as regras do registro (`p/typescript`, ...) a cada rodada, e nao tem cache delas. Regra lida de arquivo
|
|
75
|
+
* local ganha outro `check_id` (o prefixo do caminho), o que muda a impressao digital de todo achado: o cache de regras
|
|
76
|
+
* fica de fora, declarado como peca em fallback.
|
|
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
|
+
/**
|
|
81
|
+
* 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
|
+
* verify, junto das lanes de teste) a disputa de CPU fazia uma regra estourar (`partial-timeout` medido na primeira
|
|
83
|
+
* rodada real), e regra que estoura pode deixar de achar. O prazo maior nao troca regra nem arquivo varrido.
|
|
84
|
+
*/
|
|
85
|
+
exports.PRAZO_POR_REGRA_DO_SEMGREP_S = 30;
|
|
86
|
+
/** O Semgrep do Agile com o prazo por regra maior, antes do diretorio da stack (o ultimo argumento). */
|
|
87
|
+
function semgrepDoAgile(t, pecas) {
|
|
88
|
+
if (!t.argsByStack)
|
|
89
|
+
return t;
|
|
90
|
+
const comPrazo = (args) => args.length === 0 || args.includes('--timeout') ? args
|
|
91
|
+
: [...args.slice(0, -1), '--timeout', String(exports.PRAZO_POR_REGRA_DO_SEMGREP_S), args[args.length - 1]];
|
|
92
|
+
pecas.push({ peca: 'semgrep-prazo-por-regra', estado: 'agile',
|
|
93
|
+
motivo: `${exports.PRAZO_POR_REGRA_DO_SEMGREP_S} s por regra e arquivo (o padrao do semgrep e 5 s), para a disputa de CPU nao derrubar regra` });
|
|
94
|
+
return { ...t, argsByStack: Object.fromEntries(Object.entries(t.argsByStack).map(([stack, args]) => [stack, comPrazo(args ?? [])])) };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A especificacao da ferramenta no Agile: as do Trivy montam o cache persistente dela no `/tmp/trivy-cache` que o
|
|
98
|
+
* catalogo ja usa (sem o diretorio, a do catalogo, com a peca em fallback); o Semgrep ganha o prazo por regra maior.
|
|
99
|
+
*/
|
|
100
|
+
function especificacaoDoAgile(t, pecas) {
|
|
101
|
+
if (t.name === 'semgrep')
|
|
102
|
+
return semgrepDoAgile(t, pecas);
|
|
103
|
+
if (!t.name.startsWith('trivy-') || !t.args.includes('/tmp/trivy-cache'))
|
|
104
|
+
return t;
|
|
105
|
+
const host = path.join((0, run_1.cacheRoot)(), 'cache', t.name);
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(host, { recursive: true, mode: 0o700 });
|
|
108
|
+
pecas.push({ peca: `cache-${t.name}`, estado: 'agile' });
|
|
109
|
+
return { ...t, cacheMounts: [{ host, container: '/tmp/trivy-cache' }] };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
pecas.push({ peca: `cache-${t.name}`, estado: 'fallback', motivo: e.code ?? e.message,
|
|
113
|
+
fallback: 'cache efemero do catalogo' });
|
|
114
|
+
return t;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function relatar(pecas) {
|
|
118
|
+
const arquivo = process.env[check_modo_1.ENV_RELATORIO_DO_CHECK];
|
|
119
|
+
if (!arquivo)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
fs.appendFileSync(arquivo, pecas.map((p) => JSON.stringify({ tipo: 'peca', ...p })).join('\n') + '\n');
|
|
123
|
+
}
|
|
124
|
+
catch { /* o relatorio e evidencia do verify, nao condicao da varredura */ }
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* RM-072 (D11): o `eslint` com a imagem de lint pre-montada; sem ela (build que falhou, sem rede, catalogo mudou), o do
|
|
128
|
+
* catalogo, com a peca em fallback.
|
|
129
|
+
*/
|
|
130
|
+
function eslintDoAgile(t, prazoMs, pecas) {
|
|
131
|
+
if (t.name !== 'eslint' || !t.install?.length)
|
|
132
|
+
return t;
|
|
133
|
+
// A4 do CHECK: o build roda antes dos trabalhadores; com teto curto, um link lento nao consome o prazo da varredura toda.
|
|
134
|
+
const imagem = (0, imagem_de_lint_1.garantirImagemDeLint)(t, Math.min(prazoMs, exports.PRAZO_DO_BUILD_DA_IMAGEM_MS));
|
|
135
|
+
const comImagem = 'tag' in imagem ? (0, imagem_de_lint_1.eslintComImagem)(t, imagem.tag) : null;
|
|
136
|
+
if (comImagem) {
|
|
137
|
+
pecas.push({ peca: 'imagem-de-lint', estado: 'agile' });
|
|
138
|
+
return comImagem;
|
|
139
|
+
}
|
|
140
|
+
pecas.push({ peca: 'imagem-de-lint', estado: 'fallback', motivo: 'motivo' in imagem ? imagem.motivo : 'script do catalogo sem o npm install esperado',
|
|
141
|
+
fallback: 'eslint do catalogo, com npm install no container' });
|
|
142
|
+
return t;
|
|
143
|
+
}
|
|
144
|
+
/** O teto do build da imagem de lint (A4 do CHECK): estourou, o `eslint` do catalogo, com a peca em fallback. */
|
|
145
|
+
exports.PRAZO_DO_BUILD_DA_IMAGEM_MS = 180_000;
|
|
146
|
+
/** O prazo minimo que sobra para a ferramenta que caiu no Agile rodar pelo caminho do Classic (A5 do CHECK). */
|
|
147
|
+
exports.PRAZO_MINIMO_DO_CLASSIC_MS = 30_000;
|
|
148
|
+
/** Os motivos tipados de estouro que o `scanTool` grava: o prazo da HEAD e o da base. */
|
|
149
|
+
const MOTIVOS_DE_ESTOURO = new Set(['timeout', 'base-timeout']);
|
|
150
|
+
/**
|
|
151
|
+
* N2 do CHECK 2: a ferramenta estourou o prazo, e o caminho do Classic estouraria igual. Decide pelo motivo tipado da
|
|
152
|
+
* rodada ou pelo trabalhador cortado no prazo dele, nunca pelo texto do stderr: o erro de trava do cache do Trivy termina
|
|
153
|
+
* em `timeout` ("cache may be in use by another process: timeout") e e justamente a peca do Agile que o Classic cobre.
|
|
154
|
+
*/
|
|
155
|
+
function estourouNoAgile(motivo, trabalhador) {
|
|
156
|
+
if (trabalhador)
|
|
157
|
+
return trabalhador.duracaoMs >= trabalhador.prazoMs;
|
|
158
|
+
return MOTIVOS_DE_ESTOURO.has(motivo ?? '');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A URL que vai ate o fim do texto, do esquema em diante e sem espaco. Cortada, o login dela pode ter ficado pela metade,
|
|
162
|
+
* com `/`, `?`, `#` ou um segundo `@` que o redator so reconheceria inteiro; por isso ela sai toda, cortada ou nao. A URL
|
|
163
|
+
* legitima no fim tambem sai: o motivo e diagnostico, e a troca e a favor de nao vazar login.
|
|
164
|
+
*/
|
|
165
|
+
const URL_NO_FIM = /[a-z][a-z0-9+.-]*:\/\/\S*$/i;
|
|
166
|
+
/**
|
|
167
|
+
* S-k do CHECK 3 e N7 do CHECK 4: o texto que vai ao ledger sem credencial de URL. A URL que chega ate o fim sai inteira
|
|
168
|
+
* antes da redacao (o texto pode ter vindo cortado, como o motivo do Classic, cortado em 200 sem redigir); o resto e
|
|
169
|
+
* redigido antes do corte, e o que o corte deixar de URL no fim tambem sai. O resultado nunca passa do teto.
|
|
170
|
+
*/
|
|
171
|
+
function motivoSemCredencial(texto, teto) {
|
|
172
|
+
const semUrlNoFim = (t) => t.trimEnd().replace(URL_NO_FIM, '<url cortada>');
|
|
173
|
+
return semUrlNoFim((0, redacao_url_1.redigirCredenciaisUrl)(semUrlNoFim(texto)).slice(0, teto)).slice(0, teto);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* O resultado de um trabalhador da varredura e se ele estourou (N2 do CHECK 2, S-j do CHECK 3): o relatorio que ele
|
|
177
|
+
* gravou ou, sem ele, a falha do proprio trabalhador, com a ultima linha do stderr (nunca a de um detector de segredo) e o
|
|
178
|
+
* estouro pelo prazo dele.
|
|
179
|
+
*/
|
|
180
|
+
function resultadoDoTrabalhador(t, p, prazoMs) {
|
|
181
|
+
let lido = null;
|
|
182
|
+
try {
|
|
183
|
+
lido = JSON.parse(p.stdout);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
lido = null;
|
|
187
|
+
}
|
|
188
|
+
if (lido && lido.run)
|
|
189
|
+
return { r: lido, estourou: estourouNoAgile(lido.run.reason) };
|
|
190
|
+
const motivo = motivoSemCredencial(p.stderr.split('\n').filter(l => l.trim()).pop() ?? '', 160);
|
|
191
|
+
const r = { run: { name: t.name, category: t.category, version: t.version, executor: null, state: 'failed',
|
|
192
|
+
reason: `agile-worker-${p.code}${motivo && t.category !== 'secret' ? `: ${motivo}` : ''}`, counts: (0, run_1.emptyCounts)() }, head: [], base: [] };
|
|
193
|
+
return { r, estourou: estourouNoAgile(r.run.reason, { duracaoMs: p.duracaoMs, prazoMs }) };
|
|
194
|
+
}
|
|
195
|
+
/** RM-072 (D12): o binario nativo na versao do catalogo, quando a ferramenta o declara e nao instala pacote no container. */
|
|
196
|
+
function nativoDisponivel(t, probe) {
|
|
197
|
+
if (!t.native || t.install)
|
|
198
|
+
return false;
|
|
199
|
+
const saida = probe.nativeVersion(t.native.bin, t.native.versionArgs);
|
|
200
|
+
return saida !== null && new RegExp(t.native.versionPattern, 'm').test(saida);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* RM-072 (D13): o catalogo do Agile com as duas chaves: `osv_scanner` troca `trivy-vuln` e `npm-audit` pelo `osv-scanner`
|
|
204
|
+
* (no lugar do `trivy-vuln`), e `oxlint` acrescenta o primeiro passe antes do `eslint`, sem tirar o `eslint`. Desligadas,
|
|
205
|
+
* o catalogo do Classic.
|
|
206
|
+
*/
|
|
207
|
+
function catalogoDoAgile(catalogo, agile, pecas) {
|
|
208
|
+
if (!agile?.osv_scanner && !agile?.oxlint)
|
|
209
|
+
return catalogo;
|
|
210
|
+
const extras = (0, tools_1.loadAgileTools)();
|
|
211
|
+
const osv = extras.find(t => t.name === 'osv-scanner');
|
|
212
|
+
const ox = extras.find(t => t.name === 'oxlint');
|
|
213
|
+
const lista = [];
|
|
214
|
+
for (const t of catalogo) {
|
|
215
|
+
if (agile.osv_scanner && t.name === 'trivy-vuln') {
|
|
216
|
+
lista.push(osv);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (agile.osv_scanner && t.name === 'npm-audit')
|
|
220
|
+
continue;
|
|
221
|
+
if (agile.oxlint && t.name === 'eslint')
|
|
222
|
+
lista.push(ox);
|
|
223
|
+
lista.push(t);
|
|
224
|
+
}
|
|
225
|
+
if (agile.osv_scanner)
|
|
226
|
+
pecas.push({ peca: 'osv-scanner', estado: 'agile', motivo: 'no lugar de trivy-vuln e npm-audit (check.agile.osv_scanner)' });
|
|
227
|
+
if (agile.oxlint)
|
|
228
|
+
pecas.push({ peca: 'oxlint', estado: 'agile', motivo: 'primeiro passe antes do eslint, sem tirar regra dele (check.agile.oxlint)' });
|
|
229
|
+
return lista;
|
|
230
|
+
}
|
|
231
|
+
/** A rodada do Agile: as mesmas regras do `runScan`, com as ferramentas em paralelo e os caches de banco. */
|
|
232
|
+
function runScanAgil(opts) {
|
|
233
|
+
const start = path.resolve(opts.root);
|
|
234
|
+
const root = (0, run_1.git)(start, ['rev-parse', '--show-toplevel']) ?? start;
|
|
235
|
+
const generatedAt = (opts.now ?? (() => new Date()))().toISOString();
|
|
236
|
+
const head = (0, run_1.git)(root, ['rev-parse', 'HEAD']) ?? '';
|
|
237
|
+
let mainRoot = root;
|
|
238
|
+
try {
|
|
239
|
+
mainRoot = (0, estado_thread_1.raizDoEstado)(root);
|
|
240
|
+
}
|
|
241
|
+
catch { /* not a linked worktree git understands: use the root itself */ }
|
|
242
|
+
const pecas = [{ peca: 'varredura-paralela', estado: 'agile' }];
|
|
243
|
+
const report = { schema: 'ork.security-scan/v1', base: '', head, generatedAt, stacks: [], tools: [], excluded: [], findings: [],
|
|
244
|
+
check: { modo: 'agile', pecas } };
|
|
245
|
+
const catalog = catalogoDoAgile(opts.tools ?? (0, tools_1.loadTools)(), opts.agile, pecas);
|
|
246
|
+
if (!head) {
|
|
247
|
+
const config = (0, config_1.loadConfig)(root);
|
|
248
|
+
const pairs = (0, run_1.stackPairs)(root, config);
|
|
249
|
+
report.stacks = [...new Set(pairs.map(p => p.stack))];
|
|
250
|
+
report.tools = (0, run_1.failedAll)((0, tools_1.toolsFor)(report.stacks, catalog).filter(t => (0, run_1.unitsOf)(t, pairs).length > 0), config, 'base-unresolved');
|
|
251
|
+
relatar(pecas);
|
|
252
|
+
return report;
|
|
253
|
+
}
|
|
254
|
+
const guard = new guard_1.RoundGuard(mainRoot);
|
|
255
|
+
try {
|
|
256
|
+
let headTree;
|
|
257
|
+
try {
|
|
258
|
+
headTree = (0, run_1.checkout)(mainRoot, head, guard);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
const config = (0, config_1.loadConfig)(root);
|
|
262
|
+
const pairs = (0, run_1.stackPairs)(root, config);
|
|
263
|
+
report.stacks = [...new Set(pairs.map(p => p.stack))];
|
|
264
|
+
report.tools = (0, run_1.failedAll)((0, tools_1.toolsFor)(report.stacks, catalog).filter(t => (0, run_1.unitsOf)(t, pairs).length > 0), config, `head-${(e.message.split('\n')[0] ?? '').slice(0, 200)}`);
|
|
265
|
+
return report;
|
|
266
|
+
}
|
|
267
|
+
const config = (0, config_1.loadConfig)(headTree);
|
|
268
|
+
const dismissals = (0, config_1.loadDismissals)(headTree);
|
|
269
|
+
const pairs = (0, run_1.stackPairs)(headTree, config);
|
|
270
|
+
report.stacks = [...new Set(pairs.map(p => p.stack))];
|
|
271
|
+
const tools = (0, tools_1.toolsFor)(report.stacks, catalog).filter(t => (0, run_1.unitsOf)(t, pairs).length > 0);
|
|
272
|
+
const base = (0, run_1.resolveBase)(root, path.resolve(opts.stateRoot), opts.base);
|
|
273
|
+
if (!base) {
|
|
274
|
+
report.tools = (0, run_1.failedAll)(tools, config, 'base-unresolved');
|
|
275
|
+
return report;
|
|
276
|
+
}
|
|
277
|
+
report.base = base;
|
|
278
|
+
if (tools.some(t => t.name === 'semgrep'))
|
|
279
|
+
pecas.push(exports.SEMGREP_SEM_CACHE);
|
|
280
|
+
const count = (0, run_1.git)(root, ['rev-list', '--count', `${base}..${head}`]);
|
|
281
|
+
const deadline = opts.deadlineMs === undefined ? null : Date.now() + opts.deadlineMs;
|
|
282
|
+
// A arvore da base sai uma vez, aqui, e os trabalhadores a dividem (cada um varre a sua ferramenta nela).
|
|
283
|
+
let baseTree = base === head ? headTree : undefined;
|
|
284
|
+
if (!baseTree) {
|
|
285
|
+
try {
|
|
286
|
+
baseTree = (0, run_1.checkout)(mainRoot, base, guard);
|
|
287
|
+
}
|
|
288
|
+
catch { /* cada trabalhador tenta de novo e falha com base-worktree-failed */ }
|
|
289
|
+
}
|
|
290
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ork-security-scan-agil-')));
|
|
291
|
+
guard.add('outDirs', dir);
|
|
292
|
+
// RM-072 (D12): quem tem o binario nativo na versao do catalogo roda sem o Docker; a peca diz quem faltou.
|
|
293
|
+
const probe = opts.probe ?? (0, executor_1.defaultProbe)();
|
|
294
|
+
const comNativo = tools.filter(t => t.native && !t.install);
|
|
295
|
+
const nativos = new Set(comNativo.filter(t => nativoDisponivel(t, probe)).map(t => t.name));
|
|
296
|
+
if (comNativo.length > 0) {
|
|
297
|
+
const faltam = comNativo.filter(t => !nativos.has(t.name)).map(t => t.name);
|
|
298
|
+
pecas.push(nativos.size > 0
|
|
299
|
+
? { peca: 'binarios-nativos', estado: 'agile', motivo: `nativos: ${[...nativos].join(', ')}${faltam.length ? `; sem binario: ${faltam.join(', ')}` : ''}` }
|
|
300
|
+
: { peca: 'binarios-nativos', estado: 'fallback', motivo: `sem binario nativo na versao do catalogo: ${faltam.join(', ')}`, fallback: 'Docker, como no Classic' });
|
|
301
|
+
}
|
|
302
|
+
const montados = tools.map((t, i) => {
|
|
303
|
+
const restante = deadline === null ? t.timeoutMs : Math.min(t.timeoutMs, deadline - Date.now());
|
|
304
|
+
const trabalho = { tool: eslintDoAgile(especificacaoDoAgile(t, pecas), restante, pecas), units: (0, run_1.unitsOf)(t, pairs), config,
|
|
305
|
+
...(nativos.has(t.name) ? { nativoPrimeiro: true } : {}),
|
|
306
|
+
ctx: { headTree, mainRoot, base, head, rangeCommits: count === null ? null : Number(count), deadline, ...(baseTree ? { baseTree } : {}) } };
|
|
307
|
+
const arquivo = path.join(dir, `${i}.json`);
|
|
308
|
+
fs.writeFileSync(arquivo, JSON.stringify(trabalho), { mode: 0o600 });
|
|
309
|
+
return { nome: t.name, comando: `${aspas(process.execPath)} ${aspas(TRABALHADOR)} ${aspas(arquivo)}`, cwd: root };
|
|
310
|
+
});
|
|
311
|
+
// S-f do CHECK 2: o teto dos trabalhadores e contado depois de todos montados, com o build da imagem de lint ja feito.
|
|
312
|
+
const prazo = deadline === null ? Math.max(...tools.map(t => t.timeoutMs)) * 2 + FOLGA_MS : Math.max(1000, deadline - Date.now() + FOLGA_MS);
|
|
313
|
+
const trabalhos = montados.map((m) => ({ ...m, prazoMs: prazo }));
|
|
314
|
+
// RM-072: no grupo do verify, ao lado das lanes, a varredura roda poucas ferramentas por vez; sozinha, todas juntas.
|
|
315
|
+
const teto = Number(process.env[check_modo_1.ENV_VARREDURA_PARALELA_DO_CHECK]);
|
|
316
|
+
const maxParalelo = Number.isInteger(teto) && teto > 0 ? teto : undefined;
|
|
317
|
+
if (maxParalelo)
|
|
318
|
+
pecas[0] = { ...pecas[0], motivo: `ate ${maxParalelo} ferramenta(s) ao mesmo tempo, ao lado das lanes do verify` };
|
|
319
|
+
const processos = (0, check_paralelo_1.rodarEmParalelo)(trabalhos, process.env, maxParalelo ? { maxParalelo } : {});
|
|
320
|
+
const excluded = new Set();
|
|
321
|
+
tools.forEach((t, i) => {
|
|
322
|
+
const trabalhador = resultadoDoTrabalhador(t, processos[i], trabalhos[i].prazoMs);
|
|
323
|
+
let r = trabalhador.r;
|
|
324
|
+
// A5 do CHECK: a ferramenta que caiu por uma peca do Agile (o trabalhador que morreu, o nativo que falhou, o cache do
|
|
325
|
+
// Trivy disputado por outra rodada) roda uma vez pelo caminho do Classic, Docker primeiro e sem cache, dentro do prazo.
|
|
326
|
+
// Estouro de prazo nao repete: o Classic estouraria igual.
|
|
327
|
+
if (r.run.state === 'failed' && !trabalhador.estourou) {
|
|
328
|
+
// S-c do CHECK 2 e S-k do CHECK 3: o motivo vem do stderr da ferramenta e vai ao ledger sem credencial de URL.
|
|
329
|
+
const motivo = motivoSemCredencial(`no Agile: ${r.run.reason ?? 'falhou'}`, 200);
|
|
330
|
+
if (deadline === null || deadline - Date.now() >= exports.PRAZO_MINIMO_DO_CLASSIC_MS) {
|
|
331
|
+
r = (0, run_1.scanTool)(t, (0, run_1.unitsOf)(t, pairs), config, { headTree, mainRoot, base, head, rangeCommits: count === null ? null : Number(count),
|
|
332
|
+
deadline: deadline ?? Infinity, probe, guard, images: new Map(), ...(baseTree ? { baseTree } : {}) });
|
|
333
|
+
pecas.push({ peca: `classic-${t.name}`, estado: 'fallback', motivo, fallback: 'a ferramenta pelo caminho do Classic' });
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
pecas.push({ peca: `classic-${t.name}`, estado: 'fallback', motivo, fallback: 'nenhum: sem prazo para o caminho do Classic' });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (r.run.state === 'ran') {
|
|
340
|
+
const c = (0, compare_1.classify)(r.head, r.base, dismissals, config.exclude);
|
|
341
|
+
for (const f of c.findings)
|
|
342
|
+
r.run.counts[f.status][f.severity]++;
|
|
343
|
+
report.findings.push(...c.findings);
|
|
344
|
+
c.excluded.forEach(e => excluded.add(e));
|
|
345
|
+
}
|
|
346
|
+
report.tools.push(r.run);
|
|
347
|
+
});
|
|
348
|
+
report.excluded = [...excluded].sort();
|
|
349
|
+
return report;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
guard.close();
|
|
353
|
+
relatar(pecas);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executarSecurityScan = executarSecurityScan;
|
|
4
|
+
/**
|
|
5
|
+
* `ork security-scan [--base <ref>] [--json] [--check classic|agile]` (spec sections 5.4 and 5.5): runs the round, writes `report.json` and
|
|
6
|
+
* `report.md` under `<state of the main tree>/security-scan/<head-sha>/`, prints a short summary whose last line is the path of
|
|
7
|
+
* `report.md` (with `--json` it prints the ScanReport JSON instead, with no path line), and returns the exit code.
|
|
8
|
+
* Environment: ORK_SECURITY_SCAN_REQUIRED=1 makes an unavailable or failed loop-category tool exit 1;
|
|
9
|
+
* ORK_SECURITY_SCAN_DEADLINE_MS bounds the whole round. `ctx.estado` is the state root of the main tree (`raizDoEstado`). Invalid config or dismissals exit 1 without scanning.
|
|
10
|
+
* RM-072 (D9): the round is the Classic one (`runScan`) or the Agile one (`runScanAgil`, the same tools in parallel with
|
|
11
|
+
* the database caches); the mode comes from `--check`, then ORK_CHECK_MODO (set by `ork verify` for its round), then the
|
|
12
|
+
* manifest `check.mode`, then the default (agile).
|
|
13
|
+
*/
|
|
14
|
+
const locale_1 = require("../locale");
|
|
15
|
+
const check_modo_1 = require("../check-modo");
|
|
16
|
+
const agil_1 = require("./agil");
|
|
17
|
+
const report_1 = require("./report");
|
|
18
|
+
const run_1 = require("./run");
|
|
19
|
+
function executarSecurityScan(argv, ctx) {
|
|
20
|
+
const m = (0, locale_1.msg)().securityScan;
|
|
21
|
+
let base;
|
|
22
|
+
let json = false;
|
|
23
|
+
let check;
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === '--json')
|
|
27
|
+
json = true;
|
|
28
|
+
else if (a === '--check' || a.startsWith('--check=')) {
|
|
29
|
+
const valor = a === '--check' ? (i + 1 < argv.length && !argv[i + 1].startsWith('--') ? argv[++i] : null) : a.slice(8);
|
|
30
|
+
const modo = (0, check_modo_1.lerModo)(valor);
|
|
31
|
+
if (!modo) {
|
|
32
|
+
console.error(`${m.checkInvalido(valor)}\n${m.usage}`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
check = modo;
|
|
36
|
+
}
|
|
37
|
+
else if (a === '--base' && i + 1 < argv.length && !argv[i + 1].startsWith('--'))
|
|
38
|
+
base = argv[++i];
|
|
39
|
+
else if (a.startsWith('--base=') && a.length > 7)
|
|
40
|
+
base = a.slice(7);
|
|
41
|
+
else {
|
|
42
|
+
console.error(`${m.unknownFlag(a)}\n${m.usage}`);
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const required = process.env.ORK_SECURITY_SCAN_REQUIRED === '1';
|
|
47
|
+
const deadline = Number(process.env.ORK_SECURITY_SCAN_DEADLINE_MS);
|
|
48
|
+
let report;
|
|
49
|
+
try {
|
|
50
|
+
const modo = check ?? (0, check_modo_1.lerModo)(process.env[check_modo_1.ENV_MODO_DO_CHECK]) ?? ctx.modoDoManifesto ?? check_modo_1.MODO_PADRAO;
|
|
51
|
+
const opcoes = { root: ctx.raiz, stateRoot: ctx.estado, base, deadlineMs: Number.isFinite(deadline) && deadline > 0 ? deadline : undefined };
|
|
52
|
+
report = modo === 'agile' ? (0, agil_1.runScanAgil)({ ...opcoes, ...(ctx.agile ? { agile: ctx.agile } : {}) }) : (0, run_1.runScan)(opcoes);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const message = e.message;
|
|
56
|
+
if (!/^security-scan\.(config-invalid|dismissal-invalid)/.test(message))
|
|
57
|
+
throw e;
|
|
58
|
+
console.error(message);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
const files = (0, report_1.writeReport)(ctx.estado, report);
|
|
62
|
+
if (json)
|
|
63
|
+
ctx.escrever(JSON.stringify(report, null, 2));
|
|
64
|
+
else {
|
|
65
|
+
const t = (0, report_1.tally)(report);
|
|
66
|
+
ctx.escrever(m.summary(t.new, t.existing, t.dismissed, t.loop));
|
|
67
|
+
const notRun = report.tools.filter(r => r.state !== 'ran');
|
|
68
|
+
if (notRun.length) {
|
|
69
|
+
ctx.escrever(m.notRunHeading);
|
|
70
|
+
for (const r of notRun)
|
|
71
|
+
ctx.escrever(m.notRun(r.name, m.states[r.state] ?? r.state, r.reason ?? ''));
|
|
72
|
+
}
|
|
73
|
+
ctx.escrever(files.md);
|
|
74
|
+
}
|
|
75
|
+
return (0, report_1.exitCodeOf)(report, required);
|
|
76
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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.fingerprintOf = fingerprintOf;
|
|
37
|
+
exports.matchesGlob = matchesGlob;
|
|
38
|
+
exports.mergeByFingerprint = mergeByFingerprint;
|
|
39
|
+
exports.classify = classify;
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const types_1 = require("./types");
|
|
42
|
+
/**
|
|
43
|
+
* sha256 of `tool|rule|file|snippet`, whitespace collapsed. The line is not part of it (a shift must not make a finding
|
|
44
|
+
* new) and neither is the message. For secrets the snippet already is the sha256 of the value.
|
|
45
|
+
*/
|
|
46
|
+
function fingerprintOf(f) {
|
|
47
|
+
const text = [f.tool, f.rule, f.file, f.snippet].map(p => p.replace(/\s+/g, ' ').trim()).join('|');
|
|
48
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Glob over repository-relative posix paths, no dependency (`path.matchesGlob` needs Node 22, the package supports 20),
|
|
52
|
+
* with gitignore-like semantics: `**` crosses directories, `*` and `?` stay inside one segment, and a match on a
|
|
53
|
+
* directory also covers everything under it (`src/*` matches `src/a/b/c.js` through `src/a`; `vendor` excludes
|
|
54
|
+
* `vendor/x/y.js`).
|
|
55
|
+
*/
|
|
56
|
+
function matchesGlob(file, pattern) {
|
|
57
|
+
const pat = pattern.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
58
|
+
let re = '';
|
|
59
|
+
for (let i = 0; i < pat.length; i++) {
|
|
60
|
+
const c = pat[i];
|
|
61
|
+
if (c === '*' && pat[i + 1] === '*') {
|
|
62
|
+
if (pat[i + 2] === '/') {
|
|
63
|
+
re += '(?:.*/)?';
|
|
64
|
+
i += 2;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
re += '.*';
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (c === '*')
|
|
72
|
+
re += '[^/]*';
|
|
73
|
+
else if (c === '?')
|
|
74
|
+
re += '[^/]';
|
|
75
|
+
else
|
|
76
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
}
|
|
78
|
+
return new RegExp(`^${re}(?:/.*)?$`).test(file);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Union of several scans of the same tree state (Gitleaks HEAD: `dir` scan and `git` scan of base..HEAD) without double
|
|
82
|
+
* counting. Per fingerprint the merged count is the MAX of the per-list counts, not the sum, and the occurrences come
|
|
83
|
+
* from the list holding that max; on a tie the earliest list wins, so pass the `dir` scan first (its line numbers are
|
|
84
|
+
* those of the current tree). Order follows the first appearance of each fingerprint.
|
|
85
|
+
*/
|
|
86
|
+
function mergeByFingerprint(lists) {
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
for (const [i, list] of lists.entries()) {
|
|
89
|
+
for (const f of list) {
|
|
90
|
+
const fp = fingerprintOf(f);
|
|
91
|
+
const per = groups.get(fp) ?? lists.map(() => []);
|
|
92
|
+
per[i].push(f);
|
|
93
|
+
groups.set(fp, per);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const per of groups.values())
|
|
98
|
+
out.push(...per.reduce((best, cur) => cur.length > best.length ? cur : best));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Classifies the HEAD findings against the base ones. Dismissal (by fingerprint) wins, then presence in the base
|
|
103
|
+
* (`existing`), else `new`. Equal fingerprints are matched by count: occurrences beyond what the base had are `new`.
|
|
104
|
+
* `loop` is true only for `new` findings that `isLoop` accepts. Findings in an excluded path leave `findings`; their
|
|
105
|
+
* paths are listed (deduplicated, sorted) in `excluded`.
|
|
106
|
+
*/
|
|
107
|
+
function classify(head, base, dismissals, exclude) {
|
|
108
|
+
const excluded = new Set();
|
|
109
|
+
const isExcluded = (f) => exclude.some(p => matchesGlob(f.file, p));
|
|
110
|
+
const baseLines = new Map();
|
|
111
|
+
for (const b of base)
|
|
112
|
+
if (!isExcluded(b)) {
|
|
113
|
+
const fp = fingerprintOf(b);
|
|
114
|
+
baseLines.set(fp, [...(baseLines.get(fp) ?? []), b.line ?? 0]);
|
|
115
|
+
}
|
|
116
|
+
const headByFp = new Map();
|
|
117
|
+
head.forEach((h, i) => { if (!isExcluded(h)) {
|
|
118
|
+
const fp = fingerprintOf(h);
|
|
119
|
+
headByFp.set(fp, [...(headByFp.get(fp) ?? []), i]);
|
|
120
|
+
} });
|
|
121
|
+
// Same-fingerprint occurrences are paired with the base by nearest line, so the `new` one is the inserted one.
|
|
122
|
+
const paired = new Set();
|
|
123
|
+
for (const [fp, idxs] of headByFp) {
|
|
124
|
+
const lines = baseLines.get(fp) ?? [];
|
|
125
|
+
const pairs = idxs.flatMap(hi => lines.map((bl, bi) => ({ hi, bi, d: Math.abs((head[hi].line ?? 0) - bl) })));
|
|
126
|
+
const usedH = new Set();
|
|
127
|
+
const usedB = new Set();
|
|
128
|
+
for (const p of pairs.sort((x, y) => x.d - y.d || x.hi - y.hi || x.bi - y.bi)) {
|
|
129
|
+
if (usedH.has(p.hi) || usedB.has(p.bi))
|
|
130
|
+
continue;
|
|
131
|
+
usedH.add(p.hi);
|
|
132
|
+
usedB.add(p.bi);
|
|
133
|
+
paired.add(p.hi);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const dismissed = new Set(dismissals.map(d => d.fingerprint));
|
|
137
|
+
const findings = [];
|
|
138
|
+
for (const [i, h] of head.entries()) {
|
|
139
|
+
if (isExcluded(h)) {
|
|
140
|
+
excluded.add(h.file);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const fingerprint = fingerprintOf(h);
|
|
144
|
+
const status = dismissed.has(fingerprint) ? 'dismissed' : paired.has(i) ? 'existing' : 'new';
|
|
145
|
+
const { snippet: _snippet, ...rest } = h;
|
|
146
|
+
findings.push({ ...rest, fingerprint, status, loop: status === 'new' && (0, types_1.isLoop)(h.category, h.severity) });
|
|
147
|
+
}
|
|
148
|
+
return { findings, excluded: [...excluded].sort() };
|
|
149
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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.loadConfig = loadConfig;
|
|
37
|
+
exports.loadDismissals = loadDismissals;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const tools_1 = require("./tools");
|
|
41
|
+
function readJson(root, name) {
|
|
42
|
+
const file = path.join(root, '.ork-security', name);
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return undefined;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
throw Error(`security-scan.config-invalid: ${name}: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const strings = (v, name, field) => {
|
|
53
|
+
if (v === undefined)
|
|
54
|
+
return [];
|
|
55
|
+
if (!Array.isArray(v) || v.some(x => typeof x !== 'string'))
|
|
56
|
+
throw Error(`security-scan.config-invalid: ${name}: ${field} must be a list of strings`);
|
|
57
|
+
return v;
|
|
58
|
+
};
|
|
59
|
+
/** `.ork-security/config.json` under `root`; absent means empty defaults. */
|
|
60
|
+
function loadConfig(root) {
|
|
61
|
+
const raw = readJson(root, 'config.json');
|
|
62
|
+
if (raw === undefined)
|
|
63
|
+
return { exclude: [], tools: {}, stacks: { add: [], remove: [] } };
|
|
64
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
65
|
+
throw Error('security-scan.config-invalid: config.json: expected an object');
|
|
66
|
+
const c = raw;
|
|
67
|
+
// Swapping a tool version is not supported in this version: a declared version other than the catalog's is an error,
|
|
68
|
+
// never silently ignored (the scan would claim a version it did not run).
|
|
69
|
+
const catalog = new Map((0, tools_1.loadTools)().map(t => [t.name, t.version]));
|
|
70
|
+
for (const [name, t] of Object.entries(c.tools ?? {})) {
|
|
71
|
+
if (t?.version !== undefined && t.version !== catalog.get(name))
|
|
72
|
+
throw Error(`security-scan.config-invalid: tools.${name}.version`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
exclude: strings(c.exclude, 'config.json', 'exclude'),
|
|
76
|
+
tools: c.tools ?? {},
|
|
77
|
+
stacks: { add: strings(c.stacks?.add, 'config.json', 'stacks.add'), remove: strings(c.stacks?.remove, 'config.json', 'stacks.remove') },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** `.ork-security/dismissals.json` under `root` (a list); absent means none. An entry without `reason` or `decisionRef` throws. */
|
|
81
|
+
function loadDismissals(root) {
|
|
82
|
+
const raw = readJson(root, 'dismissals.json');
|
|
83
|
+
if (raw === undefined)
|
|
84
|
+
return [];
|
|
85
|
+
if (!Array.isArray(raw))
|
|
86
|
+
throw Error('security-scan.config-invalid: dismissals.json: expected a list');
|
|
87
|
+
return raw.map((d) => {
|
|
88
|
+
const filled = (v) => typeof v === 'string' && v.trim() !== '';
|
|
89
|
+
if (!filled(d?.fingerprint) || !filled(d.reason) || !filled(d.decisionRef))
|
|
90
|
+
throw Error(`security-scan.dismissal-invalid: ${d?.fingerprint ?? '(no fingerprint)'}`);
|
|
91
|
+
return d;
|
|
92
|
+
});
|
|
93
|
+
}
|