@orkastery/cli 0.2.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/LICENSE +21 -0
- package/README.md +87 -0
- package/adapters/README.md +22 -0
- package/adapters/claude-code/.claude-plugin/marketplace.json +15 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +31 -0
- package/adapters/claude-code/README.md +102 -0
- package/adapters/claude-code/agents/ork-check.md +32 -0
- package/adapters/claude-code/agents/ork-go.md +32 -0
- package/adapters/claude-code/agents/ork-goal.md +33 -0
- package/adapters/claude-code/agents/ork-master.md +32 -0
- package/adapters/claude-code/agents/ork-plan.md +32 -0
- package/adapters/claude-code/agents/ork-ship.md +32 -0
- package/adapters/claude-code/commands/check.md +45 -0
- package/adapters/claude-code/commands/go.md +46 -0
- package/adapters/claude-code/commands/goal.md +54 -0
- package/adapters/claude-code/commands/master.md +42 -0
- package/adapters/claude-code/commands/ork.md +37 -0
- package/adapters/claude-code/commands/plan.md +47 -0
- package/adapters/claude-code/commands/ship.md +45 -0
- package/adapters/claude-code/hooks/hooks.json +17 -0
- package/adapters/claude-code/hooks/ork-guard.js +130 -0
- package/adapters/hermes/README.md +45 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +25 -0
- package/adapters/hermes/hermes.plugin.json +21 -0
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +116 -0
- package/adapters/openclaw/README.md +63 -0
- package/adapters/openclaw/bin/ork-abrir-thread.sh +24 -0
- package/adapters/openclaw/openclaw.plugin.json +138 -0
- package/dist/adapters/claude-bg.js +308 -0
- package/dist/auditoria.js +848 -0
- package/dist/auditrun.js +976 -0
- package/dist/board.js +314 -0
- package/dist/canarios.js +271 -0
- package/dist/catalogo.js +124 -0
- package/dist/ciclos.js +156 -0
- package/dist/claims.js +226 -0
- package/dist/divida.js +374 -0
- package/dist/doctor.js +246 -0
- package/dist/evalrunner.js +458 -0
- package/dist/fix.js +450 -0
- package/dist/gates.js +92 -0
- package/dist/handoff.js +521 -0
- package/dist/hosts.js +331 -0
- package/dist/index.js +1910 -0
- package/dist/init.js +231 -0
- package/dist/leases.js +508 -0
- package/dist/ledger.js +138 -0
- package/dist/manifest.js +255 -0
- package/dist/master.js +453 -0
- package/dist/memoria.js +773 -0
- package/dist/modos.js +220 -0
- package/dist/orkmind.js +487 -0
- package/dist/orquestracao.js +559 -0
- package/dist/phase.js +572 -0
- package/dist/policies.js +176 -0
- package/dist/prompts.js +406 -0
- package/dist/ratelimit.js +179 -0
- package/dist/recall.js +252 -0
- package/dist/retry.js +917 -0
- package/dist/sandbox.js +94 -0
- package/dist/sessoes.js +104 -0
- package/dist/ship.js +551 -0
- package/dist/slug.js +100 -0
- package/dist/superficie.js +1347 -0
- package/dist/thread.js +333 -0
- package/dist/tokens.js +228 -0
- package/dist/types.js +20 -0
- package/dist/util.js +156 -0
- package/dist/verify.js +262 -0
- package/dist/worktree.js +576 -0
- package/dist/yaml.js +112 -0
- package/eval/README.md +85 -0
- package/eval/casos/check-quality.json +103 -0
- package/eval/casos/code-reviewer.json +102 -0
- package/eval/casos/decision-triage.json +103 -0
- package/eval/casos/go-implementation.json +103 -0
- package/eval/casos/goal-definition.json +103 -0
- package/eval/casos/master-metrics.json +103 -0
- package/eval/casos/narrative-guardian.json +163 -0
- package/eval/casos/orkastery-bootstrap.json +102 -0
- package/eval/casos/plan-specification.json +102 -0
- package/eval/casos/roadmap-keeper.json +103 -0
- package/eval/casos/scope-check-capability-map.json +83 -0
- package/eval/casos/security-auditor.json +102 -0
- package/eval/casos/ship-release.json +122 -0
- package/eval/casos/test-engineer.json +122 -0
- package/eval/casos/thread-state.json +82 -0
- package/eval/casos/thread-tracing.json +83 -0
- package/eval/casos/web-performance-auditor.json +102 -0
- package/eval/fixtures/b0-slug-e-modos/caso.json +56 -0
- package/eval/fixtures/b2-master-log/caso.json +91 -0
- package/eval/fixtures/fx-concurrency/caso.json +15 -0
- package/eval/fixtures/fx-hallucination/caso.json +12 -0
- package/eval/fixtures/fx-happy/caso.json +16 -0
- package/eval/fixtures/fx-schema-drift/caso.json +15 -0
- package/eval/fixtures/fx-stale-base/caso.json +12 -0
- package/eval/fixtures/fx-wiki-destroy/caso.json +16 -0
- package/eval/fixtures/superficie-de-rede/README.md +22 -0
- package/eval/fixtures/superficie-de-rede/api-express.js +30 -0
- package/eval/fixtures/superficie-de-rede/api_fastapi.py +17 -0
- package/eval/fixtures/superficie-de-rede/api_gin.go +18 -0
- package/package.json +56 -0
- package/references/README.md +25 -0
- package/references/code-review-axes.md +82 -0
- package/references/definition-of-done.md +74 -0
- package/references/performance-checklist.md +53 -0
- package/references/security-checklist.md +101 -0
- package/references/testing-patterns.md +51 -0
- package/skills/README.md +43 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +89 -0
- package/skills/core/thread-state/SKILL.md +86 -0
- package/skills/governance/decision-triage/SKILL.md +65 -0
- package/skills/governance/narrative-guardian/SKILL.md +66 -0
- package/skills/governance/roadmap-keeper/SKILL.md +60 -0
- package/skills/governance/scope-check-capability-map/SKILL.md +61 -0
- package/skills/observability/thread-tracing/SKILL.md +63 -0
- package/skills/phases/check-quality/SKILL.md +87 -0
- package/skills/phases/go-implementation/SKILL.md +70 -0
- package/skills/phases/goal-definition/SKILL.md +69 -0
- package/skills/phases/master-metrics/SKILL.md +65 -0
- package/skills/phases/plan-specification/SKILL.md +67 -0
- package/skills/phases/ship-release/SKILL.md +68 -0
- package/skills/reviewers/code-reviewer/SKILL.md +61 -0
- package/skills/reviewers/security-auditor/SKILL.md +68 -0
- package/skills/reviewers/test-engineer/SKILL.md +66 -0
- package/skills/reviewers/web-performance-auditor/SKILL.md +63 -0
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Varredura DETERMINISTICA da superficie de ataque de rede (extensao do pack
|
|
4
|
+
* `security-privacy`, bloco B5).
|
|
5
|
+
*
|
|
6
|
+
* O pack cobria seguranca de codigo (segredo, entrada sem validacao, CVE) e LGPD. O que
|
|
7
|
+
* ele NAO cobria era a superficie que o produto publica na rede: as rotas e os endpoints
|
|
8
|
+
* expostos. Estas regras fecham essa lacuna, no MESMO contrato de achado do B5 (evidencia
|
|
9
|
+
* arquivo:linha, impacto, fix sugerido, passo irreversivel, estimativa e claim de
|
|
10
|
+
* reexecucao), gravadas no MESMO board de divida.
|
|
11
|
+
*
|
|
12
|
+
* Tres decisoes estruturais moram aqui:
|
|
13
|
+
*
|
|
14
|
+
* 1. A deteccao e deterministica e roda sem LLM: ela localiza as definicoes de rota por
|
|
15
|
+
* framework (express, fastify, hono, koa, NestJS, Next.js, FastAPI, Flask, Django,
|
|
16
|
+
* gin, echo) e confere a presenca de guarda de autenticacao, limite de taxa, esquema
|
|
17
|
+
* de validacao e restricao de rede NO BLOCO da rota e no arquivo.
|
|
18
|
+
* 2. O comando de reexecucao de cada achado sai da MESMA lista de marcadores que a
|
|
19
|
+
* deteccao usou (`grep -qEi` sobre a mesma alternancia). Detector e claim nunca
|
|
20
|
+
* divergem, porque nao existem duas listas.
|
|
21
|
+
* 3. Onde a leitura nao da confianca (middleware que o `ork` nao reconhece, framework
|
|
22
|
+
* nao identificado, marcador presente no arquivo mas fora do bloco da rota), o achado
|
|
23
|
+
* sai com confianca `baixa`, severidade `menor` e o titulo diz REQUER CONFIRMACAO
|
|
24
|
+
* HUMANA. Falso positivo disfarcado de certeza e o defeito que este modulo evita.
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.SUFIXO_DE_CONFIRMACAO = exports.PROPOSTA_POR_REGRA = exports.LINHAS_DEPOIS_DO_BLOCO = exports.LINHAS_ANTES_DO_BLOCO = exports.LIMITE_PADRAO_POR_REGRA = exports.LIMITE_DE_BYTES_POR_ARQUIVO = exports.LIMITE_DE_ARQUIVOS = exports.EXCLUSOES_DA_VARREDURA = exports.EXTENSOES_DE_ROTA = exports.FRAMEWORKS = exports.MARCADORES_DE_ENTRADA = exports.MARCADORES_DE_CREDENCIAIS_NO_CORS = exports.MARCADORES_DE_ORIGEM_GLOBAL = exports.ANCORAS_DE_CORS = exports.CAMINHOS_DE_DADO_SENSIVEL = exports.CAMINHOS_PUBLICOS_POR_NATUREZA = exports.CAMINHOS_SENSIVEIS_A_FORCA_BRUTA = exports.MARCADORES_DE_SAUDE_DETALHADA = exports.CAMINHOS_DE_SAUDE = exports.CAMINHOS_ADMINISTRATIVOS = exports.MARCADORES_DE_RESTRICAO_DE_REDE = exports.MARCADORES_NEUTROS = exports.MARCADORES_DE_VALIDACAO = exports.MARCADORES_DE_RATE_LIMIT = exports.MARCADORES_DE_AUTH = exports.DESCRICAO_DA_CONFIANCA = exports.REGRAS_DE_SUPERFICIE = void 0;
|
|
61
|
+
exports.ehLinhaDeDados = ehLinhaDeDados;
|
|
62
|
+
exports.padraoDe = padraoDe;
|
|
63
|
+
exports.alternanciaDe = alternanciaDe;
|
|
64
|
+
exports.arquivosVersionados = arquivosVersionados;
|
|
65
|
+
exports.arquivosDaVarredura = arquivosDaVarredura;
|
|
66
|
+
exports.linguagemDoArquivo = linguagemDoArquivo;
|
|
67
|
+
exports.frameworkDoArquivo = frameworkDoArquivo;
|
|
68
|
+
exports.podeRemoverCaminho = podeRemoverCaminho;
|
|
69
|
+
exports.caminhoParaSed = caminhoParaSed;
|
|
70
|
+
exports.semComentarios = semComentarios;
|
|
71
|
+
exports.semOCaminho = semOCaminho;
|
|
72
|
+
exports.escaparParaEre = escaparParaEre;
|
|
73
|
+
exports.analisarArgumentos = analisarArgumentos;
|
|
74
|
+
exports.caminhoDoNext = caminhoDoNext;
|
|
75
|
+
exports.rotasDoArquivo = rotasDoArquivo;
|
|
76
|
+
exports.contextoDoArquivo = contextoDoArquivo;
|
|
77
|
+
exports.comandoDaLinha = comandoDaLinha;
|
|
78
|
+
exports.filtroDaEvidencia = filtroDaEvidencia;
|
|
79
|
+
exports.comandoDeAusencia = comandoDeAusencia;
|
|
80
|
+
exports.comandoDePresenca = comandoDePresenca;
|
|
81
|
+
exports.confiancaDoAchado = confiancaDoAchado;
|
|
82
|
+
exports.severidadeDoAchado = severidadeDoAchado;
|
|
83
|
+
exports.achadosDaRota = achadosDaRota;
|
|
84
|
+
exports.achadosDeCors = achadosDeCors;
|
|
85
|
+
exports.varrerSuperficie = varrerSuperficie;
|
|
86
|
+
exports.entradaBrutaDaSuperficie = entradaBrutaDaSuperficie;
|
|
87
|
+
exports.tabelaDaVarredura = tabelaDaVarredura;
|
|
88
|
+
exports.textoDaVarredura = textoDaVarredura;
|
|
89
|
+
exports.blocoDoFormato = blocoDoFormato;
|
|
90
|
+
const fs = __importStar(require("node:fs"));
|
|
91
|
+
const path = __importStar(require("node:path"));
|
|
92
|
+
const util_1 = require("./util");
|
|
93
|
+
/** Ordem canonica das regras novas, usada no catalogo e nas saidas. */
|
|
94
|
+
exports.REGRAS_DE_SUPERFICIE = ['SP8', 'SP9', 'SP10', 'SP11', 'SP12'];
|
|
95
|
+
/** O que cada nivel de confianca significa, dito por extenso em toda saida. */
|
|
96
|
+
exports.DESCRICAO_DA_CONFIANCA = {
|
|
97
|
+
alta: 'framework reconhecido e nenhum marcador do controle no arquivo inteiro',
|
|
98
|
+
media: 'framework inferido pela forma da chamada, ou marcador presente no arquivo e ausente no bloco da rota',
|
|
99
|
+
baixa: 'ha middleware que o ork nao classifica: REQUER CONFIRMACAO HUMANA antes de virar trabalho',
|
|
100
|
+
};
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Marcadores: fonte UNICA do detector e do comando de reexecucao
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* Cada lista abaixo e um fragmento de ERE (o dialeto do `grep -E`), escrito para servir aos
|
|
106
|
+
* DOIS lados: o detector monta `new RegExp(lista.join('|'), 'i')` e a claim do achado monta
|
|
107
|
+
* `grep -qEi '<a mesma alternancia>'`. Nenhuma lista pode conter aspas simples, porque a
|
|
108
|
+
* alternancia viaja dentro de aspas simples no comando de shell.
|
|
109
|
+
*/
|
|
110
|
+
exports.MARCADORES_DE_AUTH = [
|
|
111
|
+
'auth',
|
|
112
|
+
'guard',
|
|
113
|
+
'jwt',
|
|
114
|
+
'token',
|
|
115
|
+
'session',
|
|
116
|
+
'sessao',
|
|
117
|
+
'permiss',
|
|
118
|
+
'login',
|
|
119
|
+
'current[_-]?user',
|
|
120
|
+
'usuario[_-]?atual',
|
|
121
|
+
'rbac',
|
|
122
|
+
'acl',
|
|
123
|
+
'api[_-]?key',
|
|
124
|
+
'bearer',
|
|
125
|
+
'passport',
|
|
126
|
+
'clerk',
|
|
127
|
+
'principal',
|
|
128
|
+
'protect',
|
|
129
|
+
'require[_-]?user',
|
|
130
|
+
'is[_-]?admin',
|
|
131
|
+
'ensure[_-]?logged',
|
|
132
|
+
];
|
|
133
|
+
/** Middleware de limite de taxa, cota ou rpm. */
|
|
134
|
+
exports.MARCADORES_DE_RATE_LIMIT = [
|
|
135
|
+
'rate[_-]?limit',
|
|
136
|
+
'ratelimit',
|
|
137
|
+
'limiter',
|
|
138
|
+
'throttl',
|
|
139
|
+
'slowapi',
|
|
140
|
+
'slow[_-]?down',
|
|
141
|
+
'token[_-]?bucket',
|
|
142
|
+
'leaky[_-]?bucket',
|
|
143
|
+
'quota',
|
|
144
|
+
'rpm',
|
|
145
|
+
'requests[_-]?per',
|
|
146
|
+
'brute[_-]?force',
|
|
147
|
+
'cooldown',
|
|
148
|
+
'tollbooth',
|
|
149
|
+
];
|
|
150
|
+
/** Esquema/validacao de payload declarado na borda. */
|
|
151
|
+
exports.MARCADORES_DE_VALIDACAO = [
|
|
152
|
+
'schema',
|
|
153
|
+
'zod',
|
|
154
|
+
'joi',
|
|
155
|
+
'yup',
|
|
156
|
+
'valibot',
|
|
157
|
+
'ajv',
|
|
158
|
+
'celebrate',
|
|
159
|
+
'validat',
|
|
160
|
+
'validac',
|
|
161
|
+
'pydantic',
|
|
162
|
+
'base[_-]?model',
|
|
163
|
+
'serializer',
|
|
164
|
+
'dto',
|
|
165
|
+
'safe[_-]?parse',
|
|
166
|
+
'should[_-]?bind',
|
|
167
|
+
'binding:',
|
|
168
|
+
'marshmallow',
|
|
169
|
+
'superstruct',
|
|
170
|
+
'typebox',
|
|
171
|
+
// Parametro anotado com um tipo de entrada declarado (`pedido: PedidoIn`, `body: UserDto`):
|
|
172
|
+
// e assim que FastAPI, NestJS e afins declaram o esquema da borda. O sufixo e o que separa
|
|
173
|
+
// o modelo de entrada do objeto cru da requisicao (`request: Request` NAO casa aqui).
|
|
174
|
+
':[ ]*[a-z0-9_]*(in|out|schema|model|payload|body|dto|input|create|update|form)[ ]*[,)=]',
|
|
175
|
+
];
|
|
176
|
+
/**
|
|
177
|
+
* Middleware conhecido que NAO e guarda: o `ork` sabe o que ele faz, e por isso ele nao
|
|
178
|
+
* derruba a confianca do achado. Sem esta lista, um `app.use(cors(...))` deixaria toda
|
|
179
|
+
* rota do arquivo com confianca `baixa`, e a varredura inteira viraria "talvez".
|
|
180
|
+
*/
|
|
181
|
+
exports.MARCADORES_NEUTROS = [
|
|
182
|
+
'cors',
|
|
183
|
+
'helmet',
|
|
184
|
+
'morgan',
|
|
185
|
+
'logger',
|
|
186
|
+
'logging',
|
|
187
|
+
'compress',
|
|
188
|
+
'body[_-]?parser',
|
|
189
|
+
'urlencoded',
|
|
190
|
+
'cookie[_-]?parser',
|
|
191
|
+
'express\\.(json|static|urlencoded|raw|text)',
|
|
192
|
+
'static',
|
|
193
|
+
'favicon',
|
|
194
|
+
'timeout',
|
|
195
|
+
'request[_-]?id',
|
|
196
|
+
'trace',
|
|
197
|
+
'otel',
|
|
198
|
+
'sentry',
|
|
199
|
+
'etag',
|
|
200
|
+
'gzip',
|
|
201
|
+
'multer',
|
|
202
|
+
'upload',
|
|
203
|
+
];
|
|
204
|
+
/** Restricao de REDE (o outro jeito legitimo de proteger endpoint de operacao). */
|
|
205
|
+
exports.MARCADORES_DE_RESTRICAO_DE_REDE = [
|
|
206
|
+
'allow[_-]?list',
|
|
207
|
+
'white[_-]?list',
|
|
208
|
+
'ip[_-]?filter',
|
|
209
|
+
'ip[_-]?allow',
|
|
210
|
+
'127\\.0\\.0\\.1',
|
|
211
|
+
'localhost',
|
|
212
|
+
'private[_-]?network',
|
|
213
|
+
'vpn',
|
|
214
|
+
'basic[_-]?auth',
|
|
215
|
+
'mtls',
|
|
216
|
+
'mutual[_-]?tls',
|
|
217
|
+
'firewall',
|
|
218
|
+
'cidr',
|
|
219
|
+
'trusted[_-]?prox',
|
|
220
|
+
];
|
|
221
|
+
/** Caminho de endpoint privado de administracao/operacao. */
|
|
222
|
+
exports.CAMINHOS_ADMINISTRATIVOS = [
|
|
223
|
+
'admin',
|
|
224
|
+
'console',
|
|
225
|
+
'debug',
|
|
226
|
+
'swagger',
|
|
227
|
+
'openapi',
|
|
228
|
+
'api[_-]?docs',
|
|
229
|
+
'apidocs',
|
|
230
|
+
'redoc',
|
|
231
|
+
'graphiql',
|
|
232
|
+
'playground',
|
|
233
|
+
'metrics',
|
|
234
|
+
'actuator',
|
|
235
|
+
'pprof',
|
|
236
|
+
'profiler',
|
|
237
|
+
'internal',
|
|
238
|
+
'cluster',
|
|
239
|
+
'nodes',
|
|
240
|
+
'dump',
|
|
241
|
+
'phpmyadmin',
|
|
242
|
+
'adminer',
|
|
243
|
+
'kibana',
|
|
244
|
+
'prometheus',
|
|
245
|
+
'flower',
|
|
246
|
+
];
|
|
247
|
+
/** Caminho de saude/estado, que so vira achado quando o corpo devolve DETALHE de infra. */
|
|
248
|
+
exports.CAMINHOS_DE_SAUDE = ['health', 'healthz', 'readyz', 'livez', 'status', 'ping'];
|
|
249
|
+
/** O que torna um health check um vazamento de metadados de infraestrutura. */
|
|
250
|
+
exports.MARCADORES_DE_SAUDE_DETALHADA = [
|
|
251
|
+
'version',
|
|
252
|
+
'versao',
|
|
253
|
+
'env',
|
|
254
|
+
'hostname',
|
|
255
|
+
'database',
|
|
256
|
+
'uptime',
|
|
257
|
+
'memory',
|
|
258
|
+
'memoria',
|
|
259
|
+
'dependenc',
|
|
260
|
+
'commit',
|
|
261
|
+
'build',
|
|
262
|
+
'migration',
|
|
263
|
+
'disk',
|
|
264
|
+
];
|
|
265
|
+
/** Caminhos historicamente sujeitos a brute-force, enumeracao e DDoS de aplicacao. */
|
|
266
|
+
exports.CAMINHOS_SENSIVEIS_A_FORCA_BRUTA = [
|
|
267
|
+
'login',
|
|
268
|
+
'signin',
|
|
269
|
+
'sign[_-]in',
|
|
270
|
+
'log[_-]in',
|
|
271
|
+
'auth',
|
|
272
|
+
'token',
|
|
273
|
+
'session',
|
|
274
|
+
'password',
|
|
275
|
+
'senha',
|
|
276
|
+
'reset',
|
|
277
|
+
'forgot',
|
|
278
|
+
'esqueci',
|
|
279
|
+
'otp',
|
|
280
|
+
'mfa',
|
|
281
|
+
'2fa',
|
|
282
|
+
'verify',
|
|
283
|
+
'confirm',
|
|
284
|
+
'register',
|
|
285
|
+
'signup',
|
|
286
|
+
'sign[_-]up',
|
|
287
|
+
'cadastro',
|
|
288
|
+
'invite',
|
|
289
|
+
'convite',
|
|
290
|
+
'contact',
|
|
291
|
+
'contato',
|
|
292
|
+
'subscribe',
|
|
293
|
+
'newsletter',
|
|
294
|
+
'search',
|
|
295
|
+
'busca',
|
|
296
|
+
'export',
|
|
297
|
+
'upload',
|
|
298
|
+
'download',
|
|
299
|
+
'checkout',
|
|
300
|
+
'payment',
|
|
301
|
+
'pagamento',
|
|
302
|
+
'pix',
|
|
303
|
+
'coupon',
|
|
304
|
+
'cupom',
|
|
305
|
+
];
|
|
306
|
+
/**
|
|
307
|
+
* Caminhos publicos POR NATUREZA: rota sem auth aqui e projeto, nao defeito.
|
|
308
|
+
*
|
|
309
|
+
* Sem esta lista, o SP8 acusaria a tela de login de nao exigir login, que e o falso
|
|
310
|
+
* positivo mais obvio possivel numa varredura de superficie. Elas continuam sujeitas ao
|
|
311
|
+
* SP10 (justamente as que mais precisam de limite de taxa) e ao SP12.
|
|
312
|
+
*/
|
|
313
|
+
exports.CAMINHOS_PUBLICOS_POR_NATUREZA = [
|
|
314
|
+
'login',
|
|
315
|
+
'signin',
|
|
316
|
+
'sign[_-]in',
|
|
317
|
+
'log[_-]in',
|
|
318
|
+
'logout',
|
|
319
|
+
'signup',
|
|
320
|
+
'sign[_-]up',
|
|
321
|
+
'register',
|
|
322
|
+
'cadastro',
|
|
323
|
+
'oauth',
|
|
324
|
+
'callback',
|
|
325
|
+
'webhook',
|
|
326
|
+
'health',
|
|
327
|
+
'healthz',
|
|
328
|
+
'readyz',
|
|
329
|
+
'livez',
|
|
330
|
+
'ping',
|
|
331
|
+
'public',
|
|
332
|
+
'static',
|
|
333
|
+
'assets',
|
|
334
|
+
'favicon',
|
|
335
|
+
'robots',
|
|
336
|
+
'sitemap',
|
|
337
|
+
'forgot',
|
|
338
|
+
'reset',
|
|
339
|
+
'verify',
|
|
340
|
+
'confirm',
|
|
341
|
+
'contact',
|
|
342
|
+
'contato',
|
|
343
|
+
'newsletter',
|
|
344
|
+
'subscribe',
|
|
345
|
+
'landing',
|
|
346
|
+
'home',
|
|
347
|
+
'index',
|
|
348
|
+
];
|
|
349
|
+
/** Caminho que sinaliza dado sensivel trafegando na rota (usado pelo SP11). */
|
|
350
|
+
exports.CAMINHOS_DE_DADO_SENSIVEL = [
|
|
351
|
+
'user',
|
|
352
|
+
'usuario',
|
|
353
|
+
'account',
|
|
354
|
+
'conta',
|
|
355
|
+
'profile',
|
|
356
|
+
'perfil',
|
|
357
|
+
'customer',
|
|
358
|
+
'cliente',
|
|
359
|
+
'payment',
|
|
360
|
+
'pagamento',
|
|
361
|
+
'card',
|
|
362
|
+
'cartao',
|
|
363
|
+
'cpf',
|
|
364
|
+
'cnpj',
|
|
365
|
+
'email',
|
|
366
|
+
'phone',
|
|
367
|
+
'telefone',
|
|
368
|
+
'address',
|
|
369
|
+
'endereco',
|
|
370
|
+
'order',
|
|
371
|
+
'pedido',
|
|
372
|
+
'admin',
|
|
373
|
+
'token',
|
|
374
|
+
'session',
|
|
375
|
+
'auth',
|
|
376
|
+
'password',
|
|
377
|
+
'senha',
|
|
378
|
+
'billing',
|
|
379
|
+
'invoice',
|
|
380
|
+
'fatura',
|
|
381
|
+
'document',
|
|
382
|
+
'medical',
|
|
383
|
+
'saude',
|
|
384
|
+
'patient',
|
|
385
|
+
'paciente',
|
|
386
|
+
];
|
|
387
|
+
/**
|
|
388
|
+
* Ancora de CONFIGURACAO de CORS em qualquer uma das stacks reconhecidas.
|
|
389
|
+
*
|
|
390
|
+
* As ancoras sao sintaticas (`cors(`, `allow_origins=`, o nome do header) e nao a palavra
|
|
391
|
+
* solta: `cors` mencionado numa lista de constantes nao e configuracao de CORS, e foi
|
|
392
|
+
* exatamente assim que a primeira versao desta varredura acusou o proprio `superficie.ts`.
|
|
393
|
+
* `[ ]*` no lugar de `\s*` de proposito: a alternancia precisa valer ao mesmo tempo como
|
|
394
|
+
* RegExp do JavaScript (o detector) e como ERE do `grep -E` POSIX (a claim). `\s` nao existe
|
|
395
|
+
* no ERE portatil, e `[[:space:]]` nao existe no JavaScript: so a classe literal serve aos dois.
|
|
396
|
+
*/
|
|
397
|
+
exports.ANCORAS_DE_CORS = [
|
|
398
|
+
'cors[ ]*\\(',
|
|
399
|
+
'corsmiddleware',
|
|
400
|
+
'cors_allow_all_origins',
|
|
401
|
+
'allow_origins[ ]*=',
|
|
402
|
+
'allowedorigins[ ]*[:=]',
|
|
403
|
+
'allowallorigins[ ]*[:=]',
|
|
404
|
+
'access-control-allow-origin',
|
|
405
|
+
];
|
|
406
|
+
/**
|
|
407
|
+
* A linha e so um item de lista de texto (`'access-control-allow-origin',`)?
|
|
408
|
+
*
|
|
409
|
+
* Tabela de constantes nao e configuracao: esta guarda e o que mantem a varredura fora do
|
|
410
|
+
* proprio catalogo de marcadores dela.
|
|
411
|
+
*/
|
|
412
|
+
function ehLinhaDeDados(linha) {
|
|
413
|
+
return /^\s*['"`][^'"`]*['"`]\s*,?\s*$/.test(linha);
|
|
414
|
+
}
|
|
415
|
+
/** Origem global: o `*` do CORS em cada dialeto. */
|
|
416
|
+
exports.MARCADORES_DE_ORIGEM_GLOBAL = [
|
|
417
|
+
'origin[^,;)]{0,20}\\*',
|
|
418
|
+
'origin:[ ]*true',
|
|
419
|
+
'allow_origins[^]]{0,20}\\*',
|
|
420
|
+
'allowallorigins[^,;}]{0,20}true',
|
|
421
|
+
'cors_allow_all_origins[^,;}]{0,20}true',
|
|
422
|
+
'access-control-allow-origin[^,;)]{0,20}\\*',
|
|
423
|
+
];
|
|
424
|
+
/** Credenciais junto com a origem global: a combinacao que o navegador so aceita por engano. */
|
|
425
|
+
exports.MARCADORES_DE_CREDENCIAIS_NO_CORS = [
|
|
426
|
+
'credentials[^,;)]{0,20}true',
|
|
427
|
+
'allow_credentials[^,;)]{0,20}true',
|
|
428
|
+
'allowcredentials[^,;}]{0,20}true',
|
|
429
|
+
'access-control-allow-credentials',
|
|
430
|
+
'withcredentials[^,;)]{0,20}true',
|
|
431
|
+
];
|
|
432
|
+
/** Acesso a entrada da requisicao no corpo da rota (usado pelo SP12). */
|
|
433
|
+
exports.MARCADORES_DE_ENTRADA = [
|
|
434
|
+
'req\\.(body|query|params)',
|
|
435
|
+
'request\\.(body|query|args|json|form)',
|
|
436
|
+
'c\\.req\\.',
|
|
437
|
+
'ctx\\.(query|request|params)',
|
|
438
|
+
'\\.getquery',
|
|
439
|
+
'\\.param\\(',
|
|
440
|
+
'\\.query\\(',
|
|
441
|
+
'shouldbind',
|
|
442
|
+
'body\\(',
|
|
443
|
+
'formdata',
|
|
444
|
+
];
|
|
445
|
+
/** Monta o regex do detector a partir da MESMA lista que vai para o `grep -E` da claim. */
|
|
446
|
+
function padraoDe(fontes) {
|
|
447
|
+
return new RegExp(fontes.join('|'), 'i');
|
|
448
|
+
}
|
|
449
|
+
/** A alternancia como o `grep -E` a recebe (fonte unica com o detector). */
|
|
450
|
+
function alternanciaDe(fontes) {
|
|
451
|
+
return fontes.join('|');
|
|
452
|
+
}
|
|
453
|
+
/** Os frameworks cujas definicoes de rota esta varredura sabe ler. */
|
|
454
|
+
exports.FRAMEWORKS = [
|
|
455
|
+
{ id: 'express', nome: 'Express', linguagem: 'js', assinaturas: ['from [\'"]express', 'require\\([\'"]express'] },
|
|
456
|
+
{ id: 'fastify', nome: 'Fastify', linguagem: 'js', assinaturas: ['from [\'"]fastify', 'require\\([\'"]fastify'] },
|
|
457
|
+
{ id: 'hono', nome: 'Hono', linguagem: 'js', assinaturas: ['from [\'"]hono', 'require\\([\'"]hono'] },
|
|
458
|
+
{ id: 'koa', nome: 'Koa', linguagem: 'js', assinaturas: ['from [\'"]koa', 'require\\([\'"]koa'] },
|
|
459
|
+
{ id: 'nestjs', nome: 'NestJS', linguagem: 'js', assinaturas: ['@nestjs/common', '@nestjs/core'] },
|
|
460
|
+
{ id: 'next', nome: 'Next.js (route handler)', linguagem: 'js', assinaturas: ['next/server', 'next/navigation'] },
|
|
461
|
+
{ id: 'fastapi', nome: 'FastAPI', linguagem: 'py', assinaturas: ['from fastapi', 'import fastapi'] },
|
|
462
|
+
{ id: 'flask', nome: 'Flask', linguagem: 'py', assinaturas: ['from flask', 'import flask'] },
|
|
463
|
+
{ id: 'django', nome: 'Django', linguagem: 'py', assinaturas: ['from django', 'django\\.urls'] },
|
|
464
|
+
{ id: 'gin', nome: 'gin', linguagem: 'go', assinaturas: ['gin-gonic/gin'] },
|
|
465
|
+
{ id: 'echo', nome: 'echo', linguagem: 'go', assinaturas: ['labstack/echo'] },
|
|
466
|
+
];
|
|
467
|
+
/** Extensoes que a varredura le. */
|
|
468
|
+
exports.EXTENSOES_DE_ROTA = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go'];
|
|
469
|
+
/** Diretorios e arquivos que nao sao superficie do produto. */
|
|
470
|
+
exports.EXCLUSOES_DA_VARREDURA = [
|
|
471
|
+
/(^|\/)(node_modules|dist|dist-test|build|out|vendor|coverage|\.next|\.venv|venv|__pycache__|\.git)\//,
|
|
472
|
+
/(^|\/)\.orkastery\//,
|
|
473
|
+
/(\.|_)(test|spec)\.[jt]sx?$/,
|
|
474
|
+
/(^|\/)__tests__\//,
|
|
475
|
+
/(^|\/)tests?\//,
|
|
476
|
+
/_test\.go$/,
|
|
477
|
+
/(^|\/)test_[^/]*\.py$/,
|
|
478
|
+
];
|
|
479
|
+
/** Teto de arquivos lidos numa varredura (governanca de custo, igual ao escopo da rodada). */
|
|
480
|
+
exports.LIMITE_DE_ARQUIVOS = 2000;
|
|
481
|
+
/** Teto de bytes por arquivo lido. */
|
|
482
|
+
exports.LIMITE_DE_BYTES_POR_ARQUIVO = 400 * 1024;
|
|
483
|
+
/** Quantos achados uma regra pode produzir numa varredura antes de truncar (dito em voz alta). */
|
|
484
|
+
exports.LIMITE_PADRAO_POR_REGRA = 10;
|
|
485
|
+
/** Linhas antes e depois da definicao que compoem o BLOCO da rota. */
|
|
486
|
+
exports.LINHAS_ANTES_DO_BLOCO = 3;
|
|
487
|
+
exports.LINHAS_DEPOIS_DO_BLOCO = 12;
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Leitura dos arquivos candidatos
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
/**
|
|
492
|
+
* Caminhos do repositorio, ou a arvore lida na mao quando nao ha git.
|
|
493
|
+
*
|
|
494
|
+
* `--others --exclude-standard` entra de proposito: codigo ainda nao commitado tambem esta
|
|
495
|
+
* publicado na superficie de quem roda o servidor, e uma varredura que so olhasse o que ja
|
|
496
|
+
* esta no indice diria "superficie limpa" sobre a rota que acabou de ser escrita.
|
|
497
|
+
*/
|
|
498
|
+
function arquivosVersionados(raiz) {
|
|
499
|
+
const r = (0, util_1.exec)('git', ['ls-files', '--cached', '--others', '--exclude-standard'], raiz);
|
|
500
|
+
if (r.ok && r.stdout.trim() !== '') {
|
|
501
|
+
return r.stdout
|
|
502
|
+
.split('\n')
|
|
503
|
+
.map((l) => l.trim())
|
|
504
|
+
.filter((l) => l !== '');
|
|
505
|
+
}
|
|
506
|
+
const saida = [];
|
|
507
|
+
const andar = (dir) => {
|
|
508
|
+
if (saida.length >= exports.LIMITE_DE_ARQUIVOS)
|
|
509
|
+
return;
|
|
510
|
+
let entradas;
|
|
511
|
+
try {
|
|
512
|
+
entradas = fs.readdirSync(dir, { withFileTypes: true });
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
for (const e of entradas) {
|
|
518
|
+
const completo = path.join(dir, e.name);
|
|
519
|
+
const relativo = path.relative(raiz, completo).split(path.sep).join('/');
|
|
520
|
+
if (exports.EXCLUSOES_DA_VARREDURA.some((x) => x.test(relativo + (e.isDirectory() ? '/' : ''))))
|
|
521
|
+
continue;
|
|
522
|
+
if (e.isDirectory())
|
|
523
|
+
andar(completo);
|
|
524
|
+
else
|
|
525
|
+
saida.push(relativo);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
andar(raiz);
|
|
529
|
+
return saida.sort();
|
|
530
|
+
}
|
|
531
|
+
/** Os arquivos que a varredura vai ler, ja filtrados por extensao e por exclusao. */
|
|
532
|
+
function arquivosDaVarredura(raiz, opcoes = {}) {
|
|
533
|
+
const escopo = opcoes.escopo;
|
|
534
|
+
const brutos = escopo && escopo.incremental && escopo.arquivos.length > 0 ? escopo.arquivos : arquivosVersionados(raiz);
|
|
535
|
+
const prefixo = opcoes.dir ? opcoes.dir.replace(/^\.\/+/, '').replace(/\/+$/, '') + '/' : '';
|
|
536
|
+
return brutos
|
|
537
|
+
.map((a) => a.split(path.sep).join('/'))
|
|
538
|
+
.filter((a) => exports.EXTENSOES_DE_ROTA.includes(path.extname(a)))
|
|
539
|
+
.filter((a) => !exports.EXCLUSOES_DA_VARREDURA.some((x) => x.test(a)))
|
|
540
|
+
.filter((a) => (prefixo === '' ? true : a.startsWith(prefixo)))
|
|
541
|
+
.filter((a) => {
|
|
542
|
+
const completo = path.join(raiz, a);
|
|
543
|
+
try {
|
|
544
|
+
const st = fs.statSync(completo);
|
|
545
|
+
return st.isFile() && st.size <= exports.LIMITE_DE_BYTES_POR_ARQUIVO;
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
.slice(0, exports.LIMITE_DE_ARQUIVOS);
|
|
552
|
+
}
|
|
553
|
+
/** Linguagem do arquivo pela extensao. */
|
|
554
|
+
function linguagemDoArquivo(arquivo) {
|
|
555
|
+
const ext = path.extname(arquivo);
|
|
556
|
+
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext))
|
|
557
|
+
return 'js';
|
|
558
|
+
if (ext === '.py')
|
|
559
|
+
return 'py';
|
|
560
|
+
if (ext === '.go')
|
|
561
|
+
return 'go';
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
/** O framework declarado pelo proprio arquivo, ou null quando nada o identifica. */
|
|
565
|
+
function frameworkDoArquivo(arquivo, conteudo) {
|
|
566
|
+
const lingua = linguagemDoArquivo(arquivo);
|
|
567
|
+
for (const f of exports.FRAMEWORKS) {
|
|
568
|
+
if (lingua !== null && f.linguagem !== lingua)
|
|
569
|
+
continue;
|
|
570
|
+
if (f.assinaturas.some((a) => new RegExp(a, 'i').test(conteudo)))
|
|
571
|
+
return f;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
/** Nomes de receptor que denunciam um roteador, quando o framework nao se identificou. */
|
|
576
|
+
const RECEPTORES_DE_ROTA = /^(app|router|server|api|apirouter|fastify|hono|koa|instance|routes|r|e|mux|srv|http|admin|v\d+|[a-z_$][\w$]*(router|app|server|api))$/i;
|
|
577
|
+
const METODOS_JS = 'get|post|put|patch|delete|del|options|head|all|use';
|
|
578
|
+
const METODOS_PY = 'get|post|put|patch|delete|options|head|route';
|
|
579
|
+
const METODOS_GO = 'GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Group';
|
|
580
|
+
/**
|
|
581
|
+
* O caminho da rota pode ser removido do texto antes da procura por marcadores?
|
|
582
|
+
*
|
|
583
|
+
* Sem esta remocao, `/internal/metrics` casaria com o marcador de restricao de rede e
|
|
584
|
+
* `/auth/token` casaria com o marcador de autenticacao: a rota se protegeria sozinha pelo
|
|
585
|
+
* proprio nome, e as rotas mais sensiveis seriam justamente as invisiveis. Caminho com
|
|
586
|
+
* aspas simples, `@` ou `|` fica de fora porque ele nao viaja em seguranca dentro do
|
|
587
|
+
* comando de shell que a claim reexecuta.
|
|
588
|
+
*/
|
|
589
|
+
function podeRemoverCaminho(caminho) {
|
|
590
|
+
return caminho.length >= 3 && !/['@|]/.test(caminho);
|
|
591
|
+
}
|
|
592
|
+
/** O caminho escapado para o `sed` (BRE), com `@` de delimitador. */
|
|
593
|
+
function caminhoParaSed(caminho) {
|
|
594
|
+
return caminho.replace(/[\\.[\]^$*]/g, (c) => '\\' + c);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* O texto sem comentario de linha.
|
|
598
|
+
*
|
|
599
|
+
* MEDIDO nesta extensao: um comentario em portugues com a palavra "guarda" casa com o
|
|
600
|
+
* marcador `guard`, e "sem oAuth" casa com `auth`. Sem este recorte, a rota mais mal
|
|
601
|
+
* protegida do arquivo (a que tem um TODO em cima dizendo que falta auth) seria justamente
|
|
602
|
+
* a que a varredura daria por protegida. O comando da claim faz o MESMO recorte com `sed`.
|
|
603
|
+
*/
|
|
604
|
+
function semComentarios(texto) {
|
|
605
|
+
return texto
|
|
606
|
+
.split('\n')
|
|
607
|
+
.map((l) => l.replace(/\/\/.*$/, '').replace(/#.*$/, ''))
|
|
608
|
+
.join('\n');
|
|
609
|
+
}
|
|
610
|
+
/** O texto sem o caminho da rota (o MESMO recorte que o comando da claim faz com `sed`). */
|
|
611
|
+
function semOCaminho(texto, caminho) {
|
|
612
|
+
return podeRemoverCaminho(caminho) ? texto.split(caminho).join(' ') : texto;
|
|
613
|
+
}
|
|
614
|
+
/** Escapa o texto para caber dentro de um ERE do `grep -E`. */
|
|
615
|
+
function escaparParaEre(texto) {
|
|
616
|
+
return texto.replace(/[\\.[\]{}()*+?^$|\/]/g, (c) => '\\' + c);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Os argumentos entre o caminho e o handler.
|
|
620
|
+
*
|
|
621
|
+
* O ultimo token identificador e o handler e sai da conta; o que sobra sao middlewares
|
|
622
|
+
* declarados na propria rota. Middleware que nao casa com nenhuma lista de marcadores vira
|
|
623
|
+
* `desconhecido`, e um desconhecido derruba a confianca do achado para `baixa`: pode ser
|
|
624
|
+
* exatamente a guarda que o `ork` nao conhece.
|
|
625
|
+
*/
|
|
626
|
+
function analisarArgumentos(resto) {
|
|
627
|
+
const corpo = resto.replace(/\)\s*;?\s*$/, '');
|
|
628
|
+
const tokens = [];
|
|
629
|
+
let atual = '';
|
|
630
|
+
let profundidade = 0;
|
|
631
|
+
for (const c of corpo) {
|
|
632
|
+
if (c === '(' || c === '[' || c === '{')
|
|
633
|
+
profundidade++;
|
|
634
|
+
if (c === ')' || c === ']' || c === '}')
|
|
635
|
+
profundidade--;
|
|
636
|
+
if (c === ',' && profundidade <= 0) {
|
|
637
|
+
tokens.push(atual.trim());
|
|
638
|
+
atual = '';
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
atual += c;
|
|
642
|
+
}
|
|
643
|
+
tokens.push(atual.trim());
|
|
644
|
+
const identificadores = tokens
|
|
645
|
+
.map((t) => t.trim())
|
|
646
|
+
.filter((t) => t !== '')
|
|
647
|
+
.filter((t) => /^[A-Za-z_$][\w$.]*(\(.*\))?$/.test(t));
|
|
648
|
+
// O ULTIMO ARGUMENTO da chamada, quando e um identificador simples, e o handler da rota e
|
|
649
|
+
// nao um middleware. Quando o ultimo argumento e uma funcao inline, todo identificador que
|
|
650
|
+
// veio antes dela e middleware, e nenhum deles pode ser descartado como handler.
|
|
651
|
+
const ultimo = tokens[tokens.length - 1]?.trim() ?? '';
|
|
652
|
+
const ultimoEhHandlerNomeado = identificadores.length > 0 &&
|
|
653
|
+
identificadores[identificadores.length - 1] === ultimo &&
|
|
654
|
+
/^[A-Za-z_$][\w$.]*$/.test(ultimo);
|
|
655
|
+
const semHandler = ultimoEhHandlerNomeado ? identificadores.slice(0, -1) : identificadores;
|
|
656
|
+
const conhecidos = padraoDe([
|
|
657
|
+
...exports.MARCADORES_DE_AUTH,
|
|
658
|
+
...exports.MARCADORES_DE_RATE_LIMIT,
|
|
659
|
+
...exports.MARCADORES_DE_VALIDACAO,
|
|
660
|
+
...exports.MARCADORES_DE_RESTRICAO_DE_REDE,
|
|
661
|
+
...exports.MARCADORES_NEUTROS,
|
|
662
|
+
]);
|
|
663
|
+
return {
|
|
664
|
+
middlewares: semHandler,
|
|
665
|
+
desconhecidos: semHandler.filter((m) => !conhecidos.test(m)),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/** O caminho de rota que um arquivo do Next.js (app router ou pages/api) publica. */
|
|
669
|
+
function caminhoDoNext(arquivo) {
|
|
670
|
+
const normalizado = arquivo.split(path.sep).join('/');
|
|
671
|
+
const app = /(^|\/)(?:src\/)?app\/(.*)\/route\.[jt]sx?$/.exec(normalizado);
|
|
672
|
+
if (app) {
|
|
673
|
+
const segmentos = app[2]
|
|
674
|
+
.split('/')
|
|
675
|
+
.filter((s) => s !== '' && !/^\(.*\)$/.test(s))
|
|
676
|
+
.map((s) => s.replace(/^\[\.\.\.(.+)\]$/, '*$1').replace(/^\[(.+)\]$/, ':$1'));
|
|
677
|
+
return '/' + segmentos.join('/');
|
|
678
|
+
}
|
|
679
|
+
const pages = /(^|\/)(?:src\/)?pages\/api\/(.*)\.[jt]sx?$/.exec(normalizado);
|
|
680
|
+
if (pages) {
|
|
681
|
+
const semIndice = pages[2].replace(/\/?index$/, '');
|
|
682
|
+
const segmentos = semIndice
|
|
683
|
+
.split('/')
|
|
684
|
+
.filter((s) => s !== '')
|
|
685
|
+
.map((s) => s.replace(/^\[\.\.\.(.+)\]$/, '*$1').replace(/^\[(.+)\]$/, ':$1'));
|
|
686
|
+
return '/api/' + segmentos.join('/');
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
/** Bloco lido em volta de uma linha, com as fronteiras que a claim vai reexecutar. */
|
|
691
|
+
function blocoEmVolta(linhas, linha) {
|
|
692
|
+
const inicio = Math.max(1, linha - exports.LINHAS_ANTES_DO_BLOCO);
|
|
693
|
+
const fim = Math.min(linhas.length, linha + exports.LINHAS_DEPOIS_DO_BLOCO);
|
|
694
|
+
return { inicio, fim, texto: linhas.slice(inicio - 1, fim).join('\n') };
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* As definicoes de rota de um arquivo.
|
|
698
|
+
*
|
|
699
|
+
* A deteccao e por FORMA da definicao, framework a framework, e nunca por "parece uma
|
|
700
|
+
* rota": quando o arquivo nao declara framework nenhum, so passa a chamada cujo receptor
|
|
701
|
+
* tem cara de roteador E cujo primeiro argumento e um caminho comecando em `/`. E o que
|
|
702
|
+
* mantem `axios.get(url)` fora da lista.
|
|
703
|
+
*/
|
|
704
|
+
function rotasDoArquivo(arquivo, conteudo) {
|
|
705
|
+
const lingua = linguagemDoArquivo(arquivo);
|
|
706
|
+
if (lingua === null)
|
|
707
|
+
return [];
|
|
708
|
+
const framework = frameworkDoArquivo(arquivo, conteudo);
|
|
709
|
+
const linhas = conteudo.split('\n');
|
|
710
|
+
const rotas = [];
|
|
711
|
+
const registrar = (linha, metodo, caminho, resto, montagem = false) => {
|
|
712
|
+
const bloco = blocoEmVolta(linhas, linha);
|
|
713
|
+
const { middlewares, desconhecidos } = analisarArgumentos(resto);
|
|
714
|
+
rotas.push({
|
|
715
|
+
arquivo,
|
|
716
|
+
linha,
|
|
717
|
+
metodo: metodo.toUpperCase(),
|
|
718
|
+
caminho,
|
|
719
|
+
framework: framework?.id ?? 'generico',
|
|
720
|
+
linguagem: lingua,
|
|
721
|
+
texto: (linhas[linha - 1] ?? '').trim(),
|
|
722
|
+
inicioDoBloco: bloco.inicio,
|
|
723
|
+
fimDoBloco: bloco.fim,
|
|
724
|
+
bloco: bloco.texto,
|
|
725
|
+
middlewares,
|
|
726
|
+
desconhecidos,
|
|
727
|
+
montagem,
|
|
728
|
+
});
|
|
729
|
+
};
|
|
730
|
+
const prefixoDeControlador = (ate) => {
|
|
731
|
+
for (let i = ate - 1; i >= 0; i--) {
|
|
732
|
+
const c = /@Controller\s*\(\s*['"`]([^'"`]*)['"`]/.exec(linhas[i] ?? '');
|
|
733
|
+
if (c)
|
|
734
|
+
return '/' + c[1].replace(/^\/+/, '');
|
|
735
|
+
if (/^\s*@Controller\s*\(\s*\)/.test(linhas[i] ?? ''))
|
|
736
|
+
return '';
|
|
737
|
+
}
|
|
738
|
+
return '';
|
|
739
|
+
};
|
|
740
|
+
const caminhoNext = caminhoDoNext(arquivo);
|
|
741
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
742
|
+
const linha = linhas[i];
|
|
743
|
+
const n = i + 1;
|
|
744
|
+
if (lingua === 'js') {
|
|
745
|
+
// NestJS: @Get('x') dentro de um @Controller('admin').
|
|
746
|
+
const nest = /^\s*@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/.exec(linha);
|
|
747
|
+
if (nest) {
|
|
748
|
+
const sufixo = (nest[2] ?? '').replace(/^\/+/, '');
|
|
749
|
+
const prefixo = prefixoDeControlador(i);
|
|
750
|
+
registrar(n, nest[1], `${prefixo}${sufixo === '' ? '' : '/' + sufixo}` || '/', '');
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
// Next.js: o metodo exportado e a rota, e o caminho vem do proprio arquivo.
|
|
754
|
+
const next = /^\s*export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(/.exec(linha);
|
|
755
|
+
if (next && caminhoNext !== null) {
|
|
756
|
+
registrar(n, next[1], caminhoNext, '');
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// express / fastify / hono / koa-router: receptor.metodo('/caminho', ...).
|
|
760
|
+
const js = new RegExp(`^\\s*(?:(?:const|let|var)\\s+[\\w$]+\\s*=\\s*)?([A-Za-z_$][\\w$]*)\\s*\\.\\s*(${METODOS_JS})\\s*\\(\\s*(['"\`])([^'"\`]*)\\3\\s*(.*)$`).exec(linha);
|
|
761
|
+
if (js) {
|
|
762
|
+
const receptor = js[1];
|
|
763
|
+
const caminho = js[4];
|
|
764
|
+
if (!caminho.startsWith('/'))
|
|
765
|
+
continue;
|
|
766
|
+
if (framework === null && !RECEPTORES_DE_ROTA.test(receptor))
|
|
767
|
+
continue;
|
|
768
|
+
const metodo = js[2].toLowerCase();
|
|
769
|
+
registrar(n, metodo === 'del' ? 'delete' : metodo, caminho, js[5], metodo === 'use');
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
// fastify.route({ method: 'GET', url: '/x' }).
|
|
773
|
+
const fastifyRoute = /^\s*[A-Za-z_$][\w$]*\s*\.\s*route\s*\(\s*\{/.exec(linha);
|
|
774
|
+
if (fastifyRoute) {
|
|
775
|
+
const bloco = blocoEmVolta(linhas, n).texto;
|
|
776
|
+
const metodo = /method\s*:\s*['"`]([A-Za-z]+)['"`]/.exec(bloco);
|
|
777
|
+
const url = /url\s*:\s*['"`]([^'"`]*)['"`]/.exec(bloco);
|
|
778
|
+
if (metodo && url)
|
|
779
|
+
registrar(n, metodo[1], url[1], '');
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (lingua === 'py') {
|
|
785
|
+
// FastAPI / Flask: @app.get("/x") e @bp.route("/x", methods=["POST"]).
|
|
786
|
+
const py = new RegExp(`^\\s*@([A-Za-z_][\\w.]*)\\s*\\.\\s*(${METODOS_PY})\\s*\\(\\s*(['"])([^'"]*)\\3(.*)$`).exec(linha);
|
|
787
|
+
if (py) {
|
|
788
|
+
const caminho = py[4];
|
|
789
|
+
if (!caminho.startsWith('/'))
|
|
790
|
+
continue;
|
|
791
|
+
const metodos = /methods\s*=\s*\[([^\]]*)\]/.exec(py[5]);
|
|
792
|
+
if (py[2].toLowerCase() === 'route' && metodos) {
|
|
793
|
+
for (const m of metodos[1].split(',')) {
|
|
794
|
+
const nome = m.replace(/['"\s]/g, '');
|
|
795
|
+
if (nome !== '')
|
|
796
|
+
registrar(n, nome, caminho, py[5]);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
registrar(n, py[2] === 'route' ? 'GET' : py[2], caminho, py[5]);
|
|
801
|
+
}
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
// Django urlconf: path('admin/', ...) e re_path(r'^admin/', ...).
|
|
805
|
+
const dj = /^\s*(?:re_)?path\s*\(\s*r?(['"])([^'"]*)\1\s*,(.*)$/.exec(linha);
|
|
806
|
+
if (dj) {
|
|
807
|
+
registrar(n, 'ROTA', '/' + dj[2].replace(/^[\^/]+/, ''), dj[3]);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// gin / echo: r.GET("/x", handler) e grupo r.Group("/admin").
|
|
813
|
+
const go = new RegExp(`^\\s*([A-Za-z_]\\w*)\\s*\\.\\s*(${METODOS_GO})\\s*\\(\\s*"([^"]*)"\\s*(.*)$`).exec(linha);
|
|
814
|
+
if (go) {
|
|
815
|
+
const caminho = go[3];
|
|
816
|
+
if (!caminho.startsWith('/'))
|
|
817
|
+
continue;
|
|
818
|
+
if (framework === null && !RECEPTORES_DE_ROTA.test(go[1]))
|
|
819
|
+
continue;
|
|
820
|
+
registrar(n, go[2] === 'Group' ? 'USE' : go[2], caminho, go[4], go[2] === 'Group');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// O bloco de uma rota nao invade a rota seguinte. Sem este corte, a guarda (ou o esquema)
|
|
824
|
+
// da rota de baixo contaria como se fosse da rota de cima, e o oposto tambem: um
|
|
825
|
+
// `c.Param("id")` do vizinho faria um GET sem entrada nenhuma parecer que consome payload.
|
|
826
|
+
rotas.sort((a, b) => a.linha - b.linha);
|
|
827
|
+
for (let i = 0; i < rotas.length; i++) {
|
|
828
|
+
const anterior = rotas[i - 1];
|
|
829
|
+
const proxima = rotas[i + 1];
|
|
830
|
+
let inicio = rotas[i].inicioDoBloco;
|
|
831
|
+
let fim = rotas[i].fimDoBloco;
|
|
832
|
+
if (anterior && anterior.linha !== rotas[i].linha)
|
|
833
|
+
inicio = Math.max(inicio, anterior.linha + 1);
|
|
834
|
+
if (proxima && proxima.linha !== rotas[i].linha)
|
|
835
|
+
fim = Math.min(fim, proxima.linha - 1);
|
|
836
|
+
inicio = Math.min(inicio, rotas[i].linha);
|
|
837
|
+
fim = Math.max(fim, rotas[i].linha);
|
|
838
|
+
rotas[i] = {
|
|
839
|
+
...rotas[i],
|
|
840
|
+
inicioDoBloco: inicio,
|
|
841
|
+
fimDoBloco: fim,
|
|
842
|
+
bloco: linhas.slice(inicio - 1, fim).join('\n'),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return rotas;
|
|
846
|
+
}
|
|
847
|
+
/** As formas de montar middleware GLOBAL em cada stack reconhecida. */
|
|
848
|
+
const LINHAS_DE_MIDDLEWARE_GLOBAL = [
|
|
849
|
+
/^\s*[A-Za-z_$][\w$]*\s*\.\s*use\s*\(\s*(?!['"`]\/)(.*)$/,
|
|
850
|
+
/useGlobalGuards\s*\((.*)$/,
|
|
851
|
+
/APP_GUARD/,
|
|
852
|
+
/addHook\s*\(\s*['"`][^'"`]*['"`]\s*,(.*)$/,
|
|
853
|
+
/add_middleware\s*\((.*)$/,
|
|
854
|
+
/before_request/,
|
|
855
|
+
/(?:APIRouter|include_router)\s*\((?=[^)]*dependencies)(.*)$/,
|
|
856
|
+
/^\s*[A-Za-z_]\w*\s*\.\s*Use\s*\((.*)$/,
|
|
857
|
+
];
|
|
858
|
+
/** O que o arquivo inteiro diz sobre guardas, limites e validacao. */
|
|
859
|
+
function contextoDoArquivo(arquivo, conteudo) {
|
|
860
|
+
const auth = padraoDe(exports.MARCADORES_DE_AUTH);
|
|
861
|
+
const rate = padraoDe(exports.MARCADORES_DE_RATE_LIMIT);
|
|
862
|
+
const conhecidos = padraoDe([
|
|
863
|
+
...exports.MARCADORES_DE_AUTH,
|
|
864
|
+
...exports.MARCADORES_DE_RATE_LIMIT,
|
|
865
|
+
...exports.MARCADORES_DE_VALIDACAO,
|
|
866
|
+
...exports.MARCADORES_NEUTROS,
|
|
867
|
+
]);
|
|
868
|
+
let authGlobal = false;
|
|
869
|
+
let rateLimitGlobal = false;
|
|
870
|
+
const globaisDesconhecidos = [];
|
|
871
|
+
for (const linha of semComentarios(conteudo).split('\n')) {
|
|
872
|
+
for (const forma of LINHAS_DE_MIDDLEWARE_GLOBAL) {
|
|
873
|
+
const casado = forma.exec(linha);
|
|
874
|
+
if (!casado)
|
|
875
|
+
continue;
|
|
876
|
+
const argumento = (casado[1] ?? linha).trim();
|
|
877
|
+
if (auth.test(argumento))
|
|
878
|
+
authGlobal = true;
|
|
879
|
+
else if (rate.test(argumento))
|
|
880
|
+
rateLimitGlobal = true;
|
|
881
|
+
else if (!conhecidos.test(argumento)) {
|
|
882
|
+
const nome = /^[A-Za-z_$][\w$.]*/.exec(argumento)?.[0] ?? argumento.slice(0, 30);
|
|
883
|
+
if (nome !== '' && !globaisDesconhecidos.includes(nome))
|
|
884
|
+
globaisDesconhecidos.push(nome);
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
arquivo,
|
|
891
|
+
framework: frameworkDoArquivo(arquivo, conteudo),
|
|
892
|
+
authGlobal,
|
|
893
|
+
rateLimitGlobal,
|
|
894
|
+
globaisDesconhecidos,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
/** A proposta fixa de cada regra: impacto, fix, estimativa e passo irreversivel. */
|
|
898
|
+
exports.PROPOSTA_POR_REGRA = {
|
|
899
|
+
SP8: {
|
|
900
|
+
impacto: 'a rota executa para quem nao provou quem e: o dado e a acao que ela expoe ficam ao alcance de qualquer requisicao da internet',
|
|
901
|
+
fix: 'exigir a guarda de autenticacao/autorizacao na definicao da rota, ou no roteador que a monta',
|
|
902
|
+
estimativa: '2h',
|
|
903
|
+
irreversivel: 'nenhum',
|
|
904
|
+
},
|
|
905
|
+
SP9: {
|
|
906
|
+
impacto: 'endpoint de administracao/operacao exposto entrega superficie de administracao e metadados de infraestrutura a quem so deveria ver o produto',
|
|
907
|
+
fix: 'restringir por rede (allowlist ou rede interna) ou exigir oAuth/basic auth no endpoint, e tirar o detalhe de infra da resposta publica',
|
|
908
|
+
estimativa: '4h',
|
|
909
|
+
irreversivel: 'nenhum',
|
|
910
|
+
},
|
|
911
|
+
SP10: {
|
|
912
|
+
impacto: 'sem limite de taxa a rota aceita brute-force de credencial, enumeracao de recurso e DDoS de aplicacao pelo preco de uma requisicao',
|
|
913
|
+
fix: 'aplicar middleware de limite de taxa por IP e por identidade na rota, ou no roteador que a monta',
|
|
914
|
+
estimativa: '2h',
|
|
915
|
+
irreversivel: 'nenhum',
|
|
916
|
+
},
|
|
917
|
+
SP11: {
|
|
918
|
+
impacto: 'origem global com credenciais deixa qualquer site ler, pelo navegador da vitima, a resposta autenticada dela',
|
|
919
|
+
fix: 'trocar o curinga por lista explicita de origens e so entao manter o envio de credenciais',
|
|
920
|
+
estimativa: '1h',
|
|
921
|
+
irreversivel: 'nenhum',
|
|
922
|
+
},
|
|
923
|
+
SP12: {
|
|
924
|
+
impacto: 'sem esquema na borda o handler recebe payload arbitrario: o erro aparece no meio da regra de negocio, ou nao aparece',
|
|
925
|
+
fix: 'declarar o esquema de entrada na definicao da rota e recusar payload invalido antes do handler',
|
|
926
|
+
estimativa: '3h',
|
|
927
|
+
irreversivel: 'nenhum',
|
|
928
|
+
},
|
|
929
|
+
};
|
|
930
|
+
/** Peso das severidades, para ordenar o que fica quando a regra atinge o teto. */
|
|
931
|
+
const PESO_DA_SEVERIDADE = { critico: 0, maior: 1, menor: 2 };
|
|
932
|
+
const PESO_DA_CONFIANCA = { alta: 0, media: 1, baixa: 2 };
|
|
933
|
+
/** O caminho da rota (ou o arquivo, no Django) em minusculas, para casar com as listas. */
|
|
934
|
+
function casaCaminho(caminho, fontes) {
|
|
935
|
+
return padraoDe(fontes).test(caminho);
|
|
936
|
+
}
|
|
937
|
+
/** `sed -n 'Np' arquivo | grep -qE '<ancora>'`: prova que a evidencia continua naquela linha. */
|
|
938
|
+
function comandoDaLinha(arquivo, linha, ancora) {
|
|
939
|
+
return `sed -n '${linha}p' '${arquivo}'${filtroDaEvidencia('')} | grep -qEi '${ancora}'`;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* O `sed` que reproduz, no shell, o recorte que o detector fez em memoria: fora comentario
|
|
943
|
+
* de linha, fora o proprio caminho da rota. A ordem e a mesma dos dois lados.
|
|
944
|
+
*/
|
|
945
|
+
function filtroDaEvidencia(caminho) {
|
|
946
|
+
const expressoes = [`-e 's@//.*@@'`, `-e 's@#.*@@'`];
|
|
947
|
+
if (podeRemoverCaminho(caminho))
|
|
948
|
+
expressoes.push(`-e 's@${caminhoParaSed(caminho)}@ @g'`);
|
|
949
|
+
return ` | sed ${expressoes.join(' ')}`;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Prova que o marcador NAO existe (no arquivo inteiro ou so no bloco da rota).
|
|
953
|
+
*
|
|
954
|
+
* O recorte do caminho e o mesmo que o detector fez em memoria: quem reexecuta a claim le
|
|
955
|
+
* exatamente o texto que produziu o achado.
|
|
956
|
+
*/
|
|
957
|
+
function comandoDeAusencia(arquivo, fontes, escopo, caminho = '') {
|
|
958
|
+
const fonte = escopo === null ? `cat '${arquivo}'` : `sed -n '${escopo.inicio},${escopo.fim}p' '${arquivo}'`;
|
|
959
|
+
return `! ${fonte}${filtroDaEvidencia(caminho)} | grep -qEi '${alternanciaDe(fontes)}'`;
|
|
960
|
+
}
|
|
961
|
+
/** Prova que o marcador ESTA la, no bloco citado. */
|
|
962
|
+
function comandoDePresenca(arquivo, fontes, escopo) {
|
|
963
|
+
return (`sed -n '${escopo.inicio},${escopo.fim}p' '${arquivo}'${filtroDaEvidencia('')}` +
|
|
964
|
+
` | grep -qEi '${alternanciaDe(fontes)}'`);
|
|
965
|
+
}
|
|
966
|
+
/** A ancora que prova a linha: o caminho da rota, ou o metodo quando o caminho tem aspas. */
|
|
967
|
+
function ancoraDaRota(rota) {
|
|
968
|
+
if (rota.caminho !== '' && !rota.caminho.includes("'"))
|
|
969
|
+
return escaparParaEre(rota.caminho);
|
|
970
|
+
return escaparParaEre(rota.metodo);
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* A confianca do achado.
|
|
974
|
+
*
|
|
975
|
+
* Middleware nao classificado derruba para `baixa` (pode ser a guarda que o `ork` nao
|
|
976
|
+
* conhece); marcador presente no arquivo mas ausente no bloco derruba um nivel (pode estar
|
|
977
|
+
* cobrindo a rota por um caminho que a leitura de bloco nao ve).
|
|
978
|
+
*/
|
|
979
|
+
function confiancaDoAchado(rota, ctx, escopoDeBloco) {
|
|
980
|
+
if (rota.desconhecidos.length > 0 || ctx.globaisDesconhecidos.length > 0)
|
|
981
|
+
return 'baixa';
|
|
982
|
+
const base = ctx.framework === null ? 'media' : 'alta';
|
|
983
|
+
if (!escopoDeBloco)
|
|
984
|
+
return base;
|
|
985
|
+
return base === 'alta' ? 'media' : 'baixa';
|
|
986
|
+
}
|
|
987
|
+
/** Severidade final: confianca `baixa` nunca passa de `menor`, porque ela pede confirmacao. */
|
|
988
|
+
function severidadeDoAchado(desejada, confianca) {
|
|
989
|
+
if (confianca === 'baixa')
|
|
990
|
+
return 'menor';
|
|
991
|
+
if (confianca === 'media' && desejada === 'critico')
|
|
992
|
+
return 'maior';
|
|
993
|
+
return desejada;
|
|
994
|
+
}
|
|
995
|
+
/** O sufixo que marca, no proprio titulo, o achado que nao e certeza. */
|
|
996
|
+
exports.SUFIXO_DE_CONFIRMACAO = ' (requer confirmacao humana)';
|
|
997
|
+
function montarAchado(entrada) {
|
|
998
|
+
const severidade = severidadeDoAchado(entrada.severidadeDesejada, entrada.confianca);
|
|
999
|
+
const proposta = exports.PROPOSTA_POR_REGRA[entrada.regra];
|
|
1000
|
+
return {
|
|
1001
|
+
regra: entrada.regra,
|
|
1002
|
+
arquivo: entrada.arquivo,
|
|
1003
|
+
linha: entrada.linha,
|
|
1004
|
+
confianca: entrada.confianca,
|
|
1005
|
+
severidade,
|
|
1006
|
+
titulo: entrada.titulo + (entrada.confianca === 'baixa' ? exports.SUFIXO_DE_CONFIRMACAO : ''),
|
|
1007
|
+
descricao: entrada.descricao +
|
|
1008
|
+
` Confianca ${entrada.confianca}: ${exports.DESCRICAO_DA_CONFIANCA[entrada.confianca]}.` +
|
|
1009
|
+
' Varredura deterministica do `ork` (ork audit surface), sem LLM no meio.',
|
|
1010
|
+
alegacao: entrada.alegacao,
|
|
1011
|
+
verificar: entrada.verificar,
|
|
1012
|
+
impacto: proposta.impacto,
|
|
1013
|
+
fix: proposta.fix,
|
|
1014
|
+
irreversivel: proposta.irreversivel,
|
|
1015
|
+
estimativa: proposta.estimativa,
|
|
1016
|
+
framework: entrada.framework,
|
|
1017
|
+
rota: entrada.rota,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/** Os achados de uma rota: SP8, SP9, SP10 e SP12. */
|
|
1021
|
+
function achadosDaRota(rota, ctx, conteudo) {
|
|
1022
|
+
const achados = [];
|
|
1023
|
+
const alvo = `${rota.metodo} ${rota.caminho}`;
|
|
1024
|
+
const ondeEstaDefinida = `${rota.arquivo}:${rota.linha}`;
|
|
1025
|
+
const bloco = { inicio: rota.inicioDoBloco, fim: rota.fimDoBloco };
|
|
1026
|
+
const nomeDoFramework = exports.FRAMEWORKS.find((f) => f.id === rota.framework)?.nome ?? 'framework nao identificado';
|
|
1027
|
+
const mutante = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(rota.metodo);
|
|
1028
|
+
// O caminho da rota sai do texto antes da procura, e o comando da claim faz o MESMO
|
|
1029
|
+
// recorte com `sed`. Sem isso, `/auth/token` se declararia protegida pelo proprio nome.
|
|
1030
|
+
const blocoLimpo = semOCaminho(semComentarios(rota.bloco), rota.caminho);
|
|
1031
|
+
const arquivoLimpo = semOCaminho(semComentarios(conteudo), rota.caminho);
|
|
1032
|
+
const temAuthNoArquivo = padraoDe(exports.MARCADORES_DE_AUTH).test(arquivoLimpo);
|
|
1033
|
+
const temRateLimitNoArquivo = padraoDe(exports.MARCADORES_DE_RATE_LIMIT).test(arquivoLimpo);
|
|
1034
|
+
const temValidacaoNoArquivo = padraoDe(exports.MARCADORES_DE_VALIDACAO).test(arquivoLimpo);
|
|
1035
|
+
const authNoBloco = padraoDe(exports.MARCADORES_DE_AUTH).test(blocoLimpo);
|
|
1036
|
+
const protegida = authNoBloco || ctx.authGlobal;
|
|
1037
|
+
// SP9: endpoint privado de administracao/operacao exposto.
|
|
1038
|
+
const administrativo = casaCaminho(rota.caminho, exports.CAMINHOS_ADMINISTRATIVOS);
|
|
1039
|
+
const saudeDetalhada = casaCaminho(rota.caminho, exports.CAMINHOS_DE_SAUDE) && padraoDe(exports.MARCADORES_DE_SAUDE_DETALHADA).test(blocoLimpo);
|
|
1040
|
+
const guardasDoSP9 = [...exports.MARCADORES_DE_AUTH, ...exports.MARCADORES_DE_RESTRICAO_DE_REDE];
|
|
1041
|
+
const restricaoNoBloco = padraoDe(guardasDoSP9).test(blocoLimpo);
|
|
1042
|
+
if ((administrativo || saudeDetalhada) && !restricaoNoBloco && !ctx.authGlobal) {
|
|
1043
|
+
const noArquivo = padraoDe(guardasDoSP9).test(arquivoLimpo);
|
|
1044
|
+
const confianca = confiancaDoAchado(rota, ctx, noArquivo);
|
|
1045
|
+
achados.push(montarAchado({
|
|
1046
|
+
regra: 'SP9',
|
|
1047
|
+
arquivo: rota.arquivo,
|
|
1048
|
+
linha: rota.linha,
|
|
1049
|
+
confianca,
|
|
1050
|
+
severidadeDesejada: 'critico',
|
|
1051
|
+
titulo: `endpoint ${administrativo ? 'de administracao/operacao' : 'de saude detalhado'} ${alvo} exposto sem restricao de rede nem oAuth`,
|
|
1052
|
+
descricao: `${nomeDoFramework} publica ${alvo} em ${ondeEstaDefinida}` +
|
|
1053
|
+
(saudeDetalhada ? ', e o corpo da resposta devolve detalhe de infraestrutura' : '') +
|
|
1054
|
+
`. Nao ha allowlist de rede, basic auth nem oAuth ${noArquivo ? 'no bloco da rota' : 'no arquivo'}.`,
|
|
1055
|
+
alegacao: `o endpoint ${alvo} definido em ${ondeEstaDefinida} nao tem restricao de rede nem guarda de autenticacao ${noArquivo ? 'no bloco da definicao' : 'em nenhum ponto do arquivo'}`,
|
|
1056
|
+
verificar: [
|
|
1057
|
+
comandoDaLinha(rota.arquivo, rota.linha, ancoraDaRota(rota)),
|
|
1058
|
+
comandoDeAusencia(rota.arquivo, guardasDoSP9, noArquivo ? bloco : null, rota.caminho),
|
|
1059
|
+
],
|
|
1060
|
+
framework: rota.framework,
|
|
1061
|
+
rota: alvo,
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
// SP8: espaco de rota declarado em producao sem camada de autenticacao/autorizacao.
|
|
1065
|
+
// Um endpoint que ja saiu como SP9 nao volta como SP8: e a mesma linha, e o SP9 e a
|
|
1066
|
+
// afirmacao mais forte sobre ela.
|
|
1067
|
+
const jaSaiuComoSP9 = achados.length > 0;
|
|
1068
|
+
const publicaPorNatureza = casaCaminho(rota.caminho, exports.CAMINHOS_PUBLICOS_POR_NATUREZA);
|
|
1069
|
+
if (!jaSaiuComoSP9 && !rota.montagem && !protegida && !publicaPorNatureza) {
|
|
1070
|
+
const confianca = confiancaDoAchado(rota, ctx, temAuthNoArquivo);
|
|
1071
|
+
achados.push(montarAchado({
|
|
1072
|
+
regra: 'SP8',
|
|
1073
|
+
arquivo: rota.arquivo,
|
|
1074
|
+
linha: rota.linha,
|
|
1075
|
+
confianca,
|
|
1076
|
+
severidadeDesejada: 'maior',
|
|
1077
|
+
titulo: `rota ${alvo} exposta sem camada de autenticacao/autorizacao`,
|
|
1078
|
+
descricao: `${nomeDoFramework} declara ${alvo} em ${ondeEstaDefinida} sem guarda de autenticacao ` +
|
|
1079
|
+
`${temAuthNoArquivo ? 'no bloco da rota (ha marcador de auth no arquivo, mas fora dela)' : 'em nenhum ponto do arquivo'}` +
|
|
1080
|
+
`${rota.middlewares.length > 0 ? `. Middlewares lidos na rota: ${rota.middlewares.join(', ')}` : ''}.`,
|
|
1081
|
+
alegacao: `a rota ${alvo} definida em ${ondeEstaDefinida} nao tem guarda de autenticacao ${temAuthNoArquivo ? 'no bloco da definicao' : 'em nenhum ponto do arquivo'}`,
|
|
1082
|
+
verificar: [
|
|
1083
|
+
comandoDaLinha(rota.arquivo, rota.linha, ancoraDaRota(rota)),
|
|
1084
|
+
comandoDeAusencia(rota.arquivo, exports.MARCADORES_DE_AUTH, temAuthNoArquivo ? bloco : null, rota.caminho),
|
|
1085
|
+
],
|
|
1086
|
+
framework: rota.framework,
|
|
1087
|
+
rota: alvo,
|
|
1088
|
+
}));
|
|
1089
|
+
}
|
|
1090
|
+
// SP10: rota sem limite de taxa.
|
|
1091
|
+
const rateNoBloco = padraoDe(exports.MARCADORES_DE_RATE_LIMIT).test(blocoLimpo);
|
|
1092
|
+
const sensivelAForcaBruta = casaCaminho(rota.caminho, exports.CAMINHOS_SENSIVEIS_A_FORCA_BRUTA);
|
|
1093
|
+
if (!rota.montagem && !rateNoBloco && !ctx.rateLimitGlobal && (sensivelAForcaBruta || (!protegida && mutante))) {
|
|
1094
|
+
const confianca = confiancaDoAchado(rota, ctx, temRateLimitNoArquivo);
|
|
1095
|
+
achados.push(montarAchado({
|
|
1096
|
+
regra: 'SP10',
|
|
1097
|
+
arquivo: rota.arquivo,
|
|
1098
|
+
linha: rota.linha,
|
|
1099
|
+
confianca,
|
|
1100
|
+
severidadeDesejada: sensivelAForcaBruta ? 'maior' : 'menor',
|
|
1101
|
+
titulo: `rota ${alvo} sem limite de taxa${sensivelAForcaBruta ? ' num caminho sujeito a brute-force' : ''}`,
|
|
1102
|
+
descricao: `${nomeDoFramework} declara ${alvo} em ${ondeEstaDefinida} sem middleware de limite, cota ou rpm ` +
|
|
1103
|
+
`${temRateLimitNoArquivo ? 'no bloco da rota' : 'em nenhum ponto do arquivo'}.` +
|
|
1104
|
+
(sensivelAForcaBruta
|
|
1105
|
+
? ' O caminho e de uma classe historicamente atacada por forca bruta e enumeracao.'
|
|
1106
|
+
: ' A rota e publica e altera estado, o que a deixa exposta a DDoS de aplicacao.'),
|
|
1107
|
+
alegacao: `a rota ${alvo} definida em ${ondeEstaDefinida} nao tem limite de taxa ${temRateLimitNoArquivo ? 'no bloco da definicao' : 'em nenhum ponto do arquivo'}`,
|
|
1108
|
+
verificar: [
|
|
1109
|
+
comandoDaLinha(rota.arquivo, rota.linha, ancoraDaRota(rota)),
|
|
1110
|
+
comandoDeAusencia(rota.arquivo, exports.MARCADORES_DE_RATE_LIMIT, temRateLimitNoArquivo ? bloco : null, rota.caminho),
|
|
1111
|
+
],
|
|
1112
|
+
framework: rota.framework,
|
|
1113
|
+
rota: alvo,
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
// SP12: definicao de rota sem esquema de payload na borda.
|
|
1117
|
+
const validacaoNoBloco = padraoDe(exports.MARCADORES_DE_VALIDACAO).test(blocoLimpo);
|
|
1118
|
+
const consomeEntrada = mutante || padraoDe(exports.MARCADORES_DE_ENTRADA).test(blocoLimpo);
|
|
1119
|
+
if (!rota.montagem && consomeEntrada && !validacaoNoBloco) {
|
|
1120
|
+
const confianca = confiancaDoAchado(rota, ctx, temValidacaoNoArquivo);
|
|
1121
|
+
achados.push(montarAchado({
|
|
1122
|
+
regra: 'SP12',
|
|
1123
|
+
arquivo: rota.arquivo,
|
|
1124
|
+
linha: rota.linha,
|
|
1125
|
+
confianca,
|
|
1126
|
+
severidadeDesejada: 'maior',
|
|
1127
|
+
titulo: `rota ${alvo} sem esquema de validacao do payload na definicao`,
|
|
1128
|
+
descricao: `${nomeDoFramework} declara ${alvo} em ${ondeEstaDefinida} e a rota consome entrada da requisicao, ` +
|
|
1129
|
+
`mas nao ha esquema nem validador declarado ${temValidacaoNoArquivo ? 'no bloco da rota' : 'em nenhum ponto do arquivo'}. ` +
|
|
1130
|
+
'Complementa o SP6, que olha o USO do dado dentro do handler.',
|
|
1131
|
+
alegacao: `a definicao da rota ${alvo} em ${ondeEstaDefinida} nao declara esquema de validacao de payload ${temValidacaoNoArquivo ? 'no bloco da definicao' : 'em nenhum ponto do arquivo'}`,
|
|
1132
|
+
verificar: [
|
|
1133
|
+
comandoDaLinha(rota.arquivo, rota.linha, ancoraDaRota(rota)),
|
|
1134
|
+
comandoDeAusencia(rota.arquivo, exports.MARCADORES_DE_VALIDACAO, temValidacaoNoArquivo ? bloco : null, rota.caminho),
|
|
1135
|
+
],
|
|
1136
|
+
framework: rota.framework,
|
|
1137
|
+
rota: alvo,
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
1140
|
+
return achados;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* SP11: CORS permissivo demais.
|
|
1144
|
+
*
|
|
1145
|
+
* A ancora e a linha de configuracao do CORS, e nao a rota: e la que o defeito mora. O
|
|
1146
|
+
* achado so nasce quando ha origem global E (credenciais habilitadas OU dado sensivel
|
|
1147
|
+
* trafegando no arquivo), porque `origin: *` num endpoint publico de leitura e escolha
|
|
1148
|
+
* legitima, e acusar isso seria ruido.
|
|
1149
|
+
*/
|
|
1150
|
+
function achadosDeCors(arquivo, conteudo, ctx, rotas) {
|
|
1151
|
+
// Sem comentario, pelo mesmo motivo das regras de rota: uma prosa que explica CORS nao e
|
|
1152
|
+
// configuracao de CORS. Foi assim que esta varredura acusou o proprio `superficie.ts`, na
|
|
1153
|
+
// linha do comentario que documenta a ancora.
|
|
1154
|
+
const linhas = semComentarios(conteudo).split('\n');
|
|
1155
|
+
const ancora = padraoDe(exports.ANCORAS_DE_CORS);
|
|
1156
|
+
const origemGlobal = padraoDe(exports.MARCADORES_DE_ORIGEM_GLOBAL);
|
|
1157
|
+
const credenciais = padraoDe(exports.MARCADORES_DE_CREDENCIAIS_NO_CORS);
|
|
1158
|
+
// Dado sensivel medido pelo CAMINHO das rotas do arquivo, e nao pela prosa dele: qualquer
|
|
1159
|
+
// arquivo menciona `user` ou `token` em alguma linha, e essa leitura larga faria todo
|
|
1160
|
+
// `origin: *` do mundo virar achado.
|
|
1161
|
+
const sensivel = rotas.some((r) => casaCaminho(r.caminho, exports.CAMINHOS_DE_DADO_SENSIVEL));
|
|
1162
|
+
for (let i = 0; i < linhas.length; i++) {
|
|
1163
|
+
if (!ancora.test(linhas[i]) || ehLinhaDeDados(linhas[i]))
|
|
1164
|
+
continue;
|
|
1165
|
+
const bloco = blocoEmVolta(linhas, i + 1);
|
|
1166
|
+
const temGlobal = origemGlobal.test(bloco.texto);
|
|
1167
|
+
const temCredenciais = credenciais.test(bloco.texto);
|
|
1168
|
+
if (!temGlobal)
|
|
1169
|
+
continue;
|
|
1170
|
+
if (!temCredenciais && !sensivel)
|
|
1171
|
+
continue;
|
|
1172
|
+
const escopo = { inicio: bloco.inicio, fim: bloco.fim };
|
|
1173
|
+
const confianca = ctx.framework !== null && temCredenciais ? 'alta' : ctx.framework !== null ? 'media' : 'media';
|
|
1174
|
+
const verificar = [
|
|
1175
|
+
comandoDaLinha(arquivo, i + 1, alternanciaDe(exports.ANCORAS_DE_CORS)),
|
|
1176
|
+
comandoDePresenca(arquivo, exports.MARCADORES_DE_ORIGEM_GLOBAL, escopo),
|
|
1177
|
+
];
|
|
1178
|
+
if (temCredenciais)
|
|
1179
|
+
verificar.push(comandoDePresenca(arquivo, exports.MARCADORES_DE_CREDENCIAIS_NO_CORS, escopo));
|
|
1180
|
+
// Um achado por arquivo: a mesma configuracao de CORS repetida em varias linhas e a
|
|
1181
|
+
// mesma leitura contada duas vezes, e ela inflaria a recorrencia por regra.
|
|
1182
|
+
return [
|
|
1183
|
+
montarAchado({
|
|
1184
|
+
regra: 'SP11',
|
|
1185
|
+
arquivo,
|
|
1186
|
+
linha: i + 1,
|
|
1187
|
+
confianca,
|
|
1188
|
+
severidadeDesejada: temCredenciais ? 'critico' : 'maior',
|
|
1189
|
+
titulo: `CORS com origem global${temCredenciais ? ' E credenciais habilitadas' : ''} em ${arquivo}`,
|
|
1190
|
+
descricao: `A configuracao de CORS em ${arquivo}:${i + 1} libera origem global` +
|
|
1191
|
+
(temCredenciais
|
|
1192
|
+
? ' com envio de credenciais, combinacao que expoe resposta autenticada a qualquer site.'
|
|
1193
|
+
: ' num arquivo que declara rota com dado sensivel.') +
|
|
1194
|
+
' Fechar a origem pode quebrar cliente ja integrado, o que se corrige reabrindo a origem especifica, e por isso o passo continua reversivel.',
|
|
1195
|
+
alegacao: `a configuracao de CORS em ${arquivo}:${i + 1} declara origem global${temCredenciais ? ' com credenciais habilitadas' : ''}`,
|
|
1196
|
+
verificar,
|
|
1197
|
+
framework: ctx.framework?.id ?? 'generico',
|
|
1198
|
+
rota: 'configuracao de CORS',
|
|
1199
|
+
}),
|
|
1200
|
+
];
|
|
1201
|
+
}
|
|
1202
|
+
return [];
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* A varredura deterministica da superficie de ataque de rede.
|
|
1206
|
+
*
|
|
1207
|
+
* Le os arquivos do escopo, extrai as definicoes de rota por framework e devolve os
|
|
1208
|
+
* achados SP8..SP12 ja ordenados por severidade e confianca. Nao toca em disco, nao grava
|
|
1209
|
+
* estado e nao despacha nada: quem registra no board e o `auditrun.ts`.
|
|
1210
|
+
*/
|
|
1211
|
+
function varrerSuperficie(raiz, opcoes = {}) {
|
|
1212
|
+
const limite = opcoes.limitePorRegra ?? exports.LIMITE_PADRAO_POR_REGRA;
|
|
1213
|
+
const arquivos = arquivosDaVarredura(raiz, opcoes);
|
|
1214
|
+
const frameworks = new Set();
|
|
1215
|
+
const brutos = [];
|
|
1216
|
+
let arquivosComRota = 0;
|
|
1217
|
+
let totalDeRotas = 0;
|
|
1218
|
+
for (const arquivo of arquivos) {
|
|
1219
|
+
let conteudo;
|
|
1220
|
+
try {
|
|
1221
|
+
conteudo = fs.readFileSync(path.join(raiz, arquivo), 'utf8');
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const ctx = contextoDoArquivo(arquivo, conteudo);
|
|
1227
|
+
const rotas = rotasDoArquivo(arquivo, conteudo);
|
|
1228
|
+
if (rotas.length > 0) {
|
|
1229
|
+
arquivosComRota++;
|
|
1230
|
+
totalDeRotas += rotas.length;
|
|
1231
|
+
if (ctx.framework)
|
|
1232
|
+
frameworks.add(ctx.framework.nome);
|
|
1233
|
+
}
|
|
1234
|
+
for (const rota of rotas)
|
|
1235
|
+
brutos.push(...achadosDaRota(rota, ctx, conteudo));
|
|
1236
|
+
brutos.push(...achadosDeCors(arquivo, conteudo, ctx, rotas));
|
|
1237
|
+
}
|
|
1238
|
+
const filtrados = opcoes.regra ? brutos.filter((a) => a.regra === opcoes.regra) : brutos;
|
|
1239
|
+
const achados = [];
|
|
1240
|
+
const truncados = [];
|
|
1241
|
+
for (const regra of exports.REGRAS_DE_SUPERFICIE) {
|
|
1242
|
+
const daRegra = filtrados
|
|
1243
|
+
.filter((a) => a.regra === regra)
|
|
1244
|
+
.sort((a, b) => PESO_DA_SEVERIDADE[a.severidade] - PESO_DA_SEVERIDADE[b.severidade] ||
|
|
1245
|
+
PESO_DA_CONFIANCA[a.confianca] - PESO_DA_CONFIANCA[b.confianca] ||
|
|
1246
|
+
a.arquivo.localeCompare(b.arquivo) ||
|
|
1247
|
+
a.linha - b.linha);
|
|
1248
|
+
achados.push(...daRegra.slice(0, limite));
|
|
1249
|
+
if (daRegra.length > limite)
|
|
1250
|
+
truncados.push({ regra, ocultos: daRegra.length - limite });
|
|
1251
|
+
}
|
|
1252
|
+
const detalhe = arquivos.length === 0
|
|
1253
|
+
? 'nenhum arquivo de codigo no escopo: a varredura de superficie nao leu nada (isto NAO e o mesmo que superficie limpa)'
|
|
1254
|
+
: `${arquivos.length} arquivo(s) lido(s), ${arquivosComRota} com definicao de rota, ${totalDeRotas} rota(s), ` +
|
|
1255
|
+
`${achados.length} achado(s) de superficie` +
|
|
1256
|
+
(truncados.length > 0
|
|
1257
|
+
? `; teto de ${limite} por regra atingido em ${truncados.map((t) => `${t.regra} (+${t.ocultos})`).join(', ')}`
|
|
1258
|
+
: '');
|
|
1259
|
+
return {
|
|
1260
|
+
raiz,
|
|
1261
|
+
arquivosLidos: arquivos.length,
|
|
1262
|
+
arquivosComRota,
|
|
1263
|
+
rotas: totalDeRotas,
|
|
1264
|
+
frameworks: [...frameworks].sort(),
|
|
1265
|
+
achados,
|
|
1266
|
+
truncados,
|
|
1267
|
+
limitePorRegra: limite,
|
|
1268
|
+
detalhe,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/** Converte o achado da varredura na entrada bruta do board de divida. */
|
|
1272
|
+
function entradaBrutaDaSuperficie(a) {
|
|
1273
|
+
return {
|
|
1274
|
+
regra: a.regra,
|
|
1275
|
+
severidade: a.severidade,
|
|
1276
|
+
titulo: a.titulo,
|
|
1277
|
+
arquivo: `${a.arquivo}:${a.linha}`,
|
|
1278
|
+
descricao: a.descricao,
|
|
1279
|
+
alegacao: a.alegacao,
|
|
1280
|
+
verificar: a.verificar,
|
|
1281
|
+
impacto: a.impacto,
|
|
1282
|
+
fix: a.fix,
|
|
1283
|
+
irreversivel: a.irreversivel,
|
|
1284
|
+
estimativa: a.estimativa,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/** Tabela dos achados da varredura. */
|
|
1288
|
+
function tabelaDaVarredura(r) {
|
|
1289
|
+
if (r.achados.length === 0) {
|
|
1290
|
+
return `Nenhum achado de superficie de ataque. ${r.detalhe}`;
|
|
1291
|
+
}
|
|
1292
|
+
const linhas = r.achados.map((a) => [
|
|
1293
|
+
a.regra,
|
|
1294
|
+
a.severidade,
|
|
1295
|
+
a.confianca,
|
|
1296
|
+
a.framework,
|
|
1297
|
+
a.rota,
|
|
1298
|
+
`${a.arquivo}:${a.linha}`,
|
|
1299
|
+
a.titulo,
|
|
1300
|
+
]);
|
|
1301
|
+
return (0, util_1.tabela)(['REGRA', 'SEVERIDADE', 'CONFIANCA', 'FRAMEWORK', 'ROTA', 'EVIDENCIA', 'TITULO'], linhas);
|
|
1302
|
+
}
|
|
1303
|
+
/** Texto completo de `ork audit surface`. */
|
|
1304
|
+
function textoDaVarredura(r) {
|
|
1305
|
+
const L = [];
|
|
1306
|
+
L.push('Varredura deterministica da superficie de ataque de rede (pack security-privacy, SP8..SP12)');
|
|
1307
|
+
L.push('');
|
|
1308
|
+
L.push(` raiz ${r.raiz}`);
|
|
1309
|
+
L.push(` escopo ${r.detalhe}`);
|
|
1310
|
+
L.push(` frameworks ${r.frameworks.length > 0 ? r.frameworks.join(', ') : '(nenhum reconhecido)'}`);
|
|
1311
|
+
L.push(` teto ${r.limitePorRegra} achado(s) por regra`);
|
|
1312
|
+
L.push('');
|
|
1313
|
+
L.push(tabelaDaVarredura(r));
|
|
1314
|
+
L.push('');
|
|
1315
|
+
for (const t of r.truncados) {
|
|
1316
|
+
L.push(` ATENCAO: ${t.ocultos} achado(s) de ${t.regra} ficaram fora do teto desta varredura.`);
|
|
1317
|
+
}
|
|
1318
|
+
const baixas = r.achados.filter((a) => a.confianca === 'baixa').length;
|
|
1319
|
+
if (baixas > 0) {
|
|
1320
|
+
L.push(` ${baixas} achado(s) sairam com confianca baixa: ${exports.DESCRICAO_DA_CONFIANCA.baixa}`);
|
|
1321
|
+
}
|
|
1322
|
+
L.push('');
|
|
1323
|
+
L.push('Cada achado carrega o comando que o reexecuta no HEAD real (o auditor nao tem self-report).');
|
|
1324
|
+
L.push('Para registrar no board de divida de uma rodada: ork audit surface --registrar <rodada>');
|
|
1325
|
+
return L.join('\n');
|
|
1326
|
+
}
|
|
1327
|
+
/** O bloco que a rodada deixa no `FORMATO.md` para o auditor nao repetir o que ja foi achado. */
|
|
1328
|
+
function blocoDoFormato(r, registrados) {
|
|
1329
|
+
const L = [];
|
|
1330
|
+
L.push('## Superficie de ataque de rede (SP8..SP12): ja varrida pelo `ork`');
|
|
1331
|
+
L.push('');
|
|
1332
|
+
L.push('As regras SP8 a SP12 tem varredura DETERMINISTICA no proprio `ork` ' +
|
|
1333
|
+
'(`ork audit surface`), que roda junto com esta rodada. Resultado desta rodada:');
|
|
1334
|
+
L.push('');
|
|
1335
|
+
L.push(`- ${r.detalhe}`);
|
|
1336
|
+
L.push(`- frameworks reconhecidos: ${r.frameworks.length > 0 ? r.frameworks.join(', ') : '(nenhum)'}`);
|
|
1337
|
+
L.push(`- achados ${registrados ? 'JA REGISTRADOS no board' : 'encontrados e NAO registrados (rodada em --dry-run)'}: ` +
|
|
1338
|
+
(r.achados.length === 0
|
|
1339
|
+
? 'nenhum'
|
|
1340
|
+
: r.achados.map((a) => `${a.regra} em ${a.arquivo}:${a.linha}`).join('; ')));
|
|
1341
|
+
L.push('');
|
|
1342
|
+
L.push('NAO repita os achados acima. Sobre SP8..SP12, escreva apenas o que a varredura ' +
|
|
1343
|
+
'deterministica nao alcanca (autorizacao por objeto, rota gerada em tempo de execucao, ' +
|
|
1344
|
+
'proxy ou gateway na frente do servico) e o que os achados de confianca `baixa` pedem: ' +
|
|
1345
|
+
'confirmacao humana com a evidencia que decide.');
|
|
1346
|
+
return L.join('\n');
|
|
1347
|
+
}
|