security_pentest_planner 1.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +121 -0
- data/exe/security-pentest-planner +7 -0
- data/lib/security_pentest_planner/cli.rb +97 -0
- data/lib/security_pentest_planner/contract_analyzer.rb +148 -0
- data/lib/security_pentest_planner/errors.rb +7 -0
- data/lib/security_pentest_planner/openapi_parser.rb +149 -0
- data/lib/security_pentest_planner/plan_renderer.rb +240 -0
- data/lib/security_pentest_planner/planner.rb +54 -0
- data/lib/security_pentest_planner/vector_catalog.rb +439 -0
- data/lib/security_pentest_planner/version.rb +5 -0
- data/lib/security_pentest_planner.rb +22 -0
- metadata +88 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SecurityPentestPlanner
|
|
4
|
+
class VectorCatalog
|
|
5
|
+
Vector = Struct.new(
|
|
6
|
+
:id,
|
|
7
|
+
:name,
|
|
8
|
+
:category_slug,
|
|
9
|
+
:category_title,
|
|
10
|
+
:layer,
|
|
11
|
+
:triggers,
|
|
12
|
+
:objective,
|
|
13
|
+
:procedure,
|
|
14
|
+
:payload,
|
|
15
|
+
:payload_type,
|
|
16
|
+
:expected_validation,
|
|
17
|
+
:failure_criteria,
|
|
18
|
+
:risk,
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
Category = Struct.new(:slug, :title, :layer, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
API_CATEGORIES = [
|
|
25
|
+
Category.new(slug: "controle-de-acesso", title: "Testes de Controle de Acesso (BOLA/IDOR)", layer: :api),
|
|
26
|
+
Category.new(slug: "ataques-de-injecao-e-manipulacao-de-params", title: "Ataques de Injeção e Manipulação de Parâmetros", layer: :api),
|
|
27
|
+
Category.new(slug: "seguranca-da-camada-de-negocio-e-logica", title: "Segurança da Camada de Negócio e Lógica", layer: :api),
|
|
28
|
+
Category.new(slug: "fuzzing-e-tratamento-de-erros", title: "Fuzzing e Tratamento de Erros", layer: :api)
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
DATALAKE_CATEGORIES = [
|
|
32
|
+
Category.new(slug: "testes-de-injecao-e-logica", title: "Testes de Injeção e Lógica", layer: :datalake),
|
|
33
|
+
Category.new(slug: "testes-de-controle-de-acesso", title: "Testes de Controle de Acesso (IAM & Permissões)", layer: :datalake),
|
|
34
|
+
Category.new(slug: "testes-de-performance-e-custo", title: "Testes de Performance e Custo (DoS de Data Lake)", layer: :datalake)
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
def self.vectors_for(analyzer)
|
|
38
|
+
new(analyzer).selected_vectors
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(analyzer)
|
|
42
|
+
@analyzer = analyzer
|
|
43
|
+
@context = build_context
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def selected_vectors
|
|
47
|
+
all_vectors.select { |vector| applicable?(vector) }.map { |vector| expand(vector) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def grouped_vectors
|
|
51
|
+
selected_vectors.group_by { |v| [v.layer, v.category_slug] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :analyzer, :context
|
|
57
|
+
|
|
58
|
+
def build_context
|
|
59
|
+
endpoint = analyzer.primary_endpoint
|
|
60
|
+
{
|
|
61
|
+
team: "TIME",
|
|
62
|
+
tenant_header: analyzer.tenant_header_name,
|
|
63
|
+
endpoint_path: endpoint&.path || "/api/v1/resource",
|
|
64
|
+
method: endpoint&.method || "GET",
|
|
65
|
+
host: analyzer.host.gsub(%r{^https?://}, ""),
|
|
66
|
+
date_params: analyzer.date_param_names,
|
|
67
|
+
date_param_1: analyzer.date_param_names[0] || "start_date",
|
|
68
|
+
date_param_2: analyzer.date_param_names[1] || "end_date",
|
|
69
|
+
enum_param: analyzer.enum_param&.name || "channels",
|
|
70
|
+
enum_values: analyzer.enum_param&.enum_values&.join(", ") || "email, sms, whatsapp",
|
|
71
|
+
timezone_header: analyzer.timezone_headers.first&.name || "Timezone",
|
|
72
|
+
input_sources: analyzer.parser.source_path || "OpenAPI spec"
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def applicable?(vector)
|
|
77
|
+
vector.triggers.all? { |trigger| analyzer.signals[trigger] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def expand(vector)
|
|
81
|
+
Vector.new(
|
|
82
|
+
id: vector.id,
|
|
83
|
+
name: vector.name,
|
|
84
|
+
category_slug: vector.category_slug,
|
|
85
|
+
category_title: vector.category_title,
|
|
86
|
+
layer: vector.layer,
|
|
87
|
+
triggers: vector.triggers,
|
|
88
|
+
objective: interpolate(vector.objective),
|
|
89
|
+
procedure: vector.procedure.map { |step| interpolate(step) },
|
|
90
|
+
payload: interpolate(vector.payload),
|
|
91
|
+
payload_type: vector.payload_type,
|
|
92
|
+
expected_validation: interpolate(vector.expected_validation),
|
|
93
|
+
failure_criteria: interpolate(vector.failure_criteria),
|
|
94
|
+
risk: vector.risk
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def interpolate(text)
|
|
99
|
+
text.gsub(/\{\{(\w+)\}\}/) { context[Regexp.last_match(1).to_sym] || "{{#{Regexp.last_match(1)}}}" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def all_vectors
|
|
103
|
+
@all_vectors ||= api_vectors + datalake_vectors
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def api_vectors
|
|
107
|
+
[
|
|
108
|
+
vector(
|
|
109
|
+
id: "tenant-traversal",
|
|
110
|
+
name: "Tenant Traversal",
|
|
111
|
+
category_slug: "controle-de-acesso",
|
|
112
|
+
category_title: API_CATEGORIES[0].title,
|
|
113
|
+
layer: :api,
|
|
114
|
+
triggers: [:tenant_scoping],
|
|
115
|
+
objective: "Substituir o header `{{tenant_header}}` por ID de outro cliente ou ID inexistente.",
|
|
116
|
+
procedure: [
|
|
117
|
+
"Obter baseline com tenant legítimo e intervalo de datas válido.",
|
|
118
|
+
"Repetir a requisição substituindo `{{tenant_header}}` por ID de tenant alheio.",
|
|
119
|
+
"Repetir com IDs inexistentes e valores extremamente longos."
|
|
120
|
+
],
|
|
121
|
+
payload: "GET {{endpoint_path}}?{{date_param_1}}=2026-01-01&{{date_param_2}}=2026-01-31 HTTP/1.1\nHost: {{host}}\n{{tenant_header}}: 999999999999999999",
|
|
122
|
+
payload_type: "http",
|
|
123
|
+
expected_validation: "Sem retorno de estatísticas/dados de terceiros; preferencialmente métricas zeradas ou 403/404.",
|
|
124
|
+
failure_criteria: "200 com estatísticas ou dados de tenant não autorizado.",
|
|
125
|
+
risk: "BOLA/IDOR — Cross-Tenant Data Leak"
|
|
126
|
+
),
|
|
127
|
+
vector(
|
|
128
|
+
id: "header-injection",
|
|
129
|
+
name: "Header Injection",
|
|
130
|
+
category_slug: "controle-de-acesso",
|
|
131
|
+
category_title: API_CATEGORIES[0].title,
|
|
132
|
+
layer: :api,
|
|
133
|
+
triggers: [:tenant_scoping],
|
|
134
|
+
objective: "Enviar headers conflitantes para verificar qual valor o backend prioriza.",
|
|
135
|
+
procedure: [
|
|
136
|
+
"Enviar `Tenant-ID` e `X-Tenant-ID` com valores distintos.",
|
|
137
|
+
"Inverter ordem dos headers e repetir.",
|
|
138
|
+
"Comparar resposta com baseline de cada tenant isolado."
|
|
139
|
+
],
|
|
140
|
+
payload: "Tenant-ID: tenant-vitima\nX-Tenant-ID: tenant-atacante",
|
|
141
|
+
payload_type: "http",
|
|
142
|
+
expected_validation: "Comportamento determinístico; priorização segura do header canônico; sem vazamento do tenant vítima.",
|
|
143
|
+
failure_criteria: "Resposta alinhada ao tenant errado ou comportamento inconsistente.",
|
|
144
|
+
risk: "Bypass de isolamento por ambiguidade de parsing HTTP"
|
|
145
|
+
),
|
|
146
|
+
vector(
|
|
147
|
+
id: "privilege-escalation",
|
|
148
|
+
name: "Privilege Escalation — omissão de tenant",
|
|
149
|
+
category_slug: "controle-de-acesso",
|
|
150
|
+
category_title: API_CATEGORIES[0].title,
|
|
151
|
+
layer: :api,
|
|
152
|
+
triggers: [:tenant_scoping],
|
|
153
|
+
objective: "Acessar o endpoint omitindo o header `{{tenant_header}}`.",
|
|
154
|
+
procedure: [
|
|
155
|
+
"Chamar `{{endpoint_path}}` sem `{{tenant_header}}`.",
|
|
156
|
+
"Verificar se query param alternativo de tenant é aceito.",
|
|
157
|
+
"Observar se a API retorna dados agregados cross-tenant."
|
|
158
|
+
],
|
|
159
|
+
payload: "GET {{endpoint_path}}?{{date_param_1}}=2026-01-01&{{date_param_2}}=2026-01-31 HTTP/1.1\nHost: {{host}}",
|
|
160
|
+
payload_type: "http",
|
|
161
|
+
expected_validation: "400 Bad Request ou 401/403; nunca dados agregados cross-tenant.",
|
|
162
|
+
failure_criteria: "200 com métricas de negócio sem escopo de tenant.",
|
|
163
|
+
risk: "Visualização de dados de todos os clientes"
|
|
164
|
+
),
|
|
165
|
+
vector(
|
|
166
|
+
id: "sqli-basic",
|
|
167
|
+
name: "SQL Injection — parâmetros de data",
|
|
168
|
+
category_slug: "ataques-de-injecao-e-manipulacao-de-params",
|
|
169
|
+
category_title: API_CATEGORIES[1].title,
|
|
170
|
+
layer: :api,
|
|
171
|
+
triggers: [:date_range],
|
|
172
|
+
objective: "Inserir payloads de escape em campos de data/filtro (`{{date_param_1}}`, `{{date_param_2}}`).",
|
|
173
|
+
procedure: [
|
|
174
|
+
"Substituir datas por payloads SQLi clássicos.",
|
|
175
|
+
"Observar código HTTP e corpo de erro.",
|
|
176
|
+
"Verificar se queries downstream permanecem inalteradas."
|
|
177
|
+
],
|
|
178
|
+
payload: "{{date_param_1}}=2026-01-01' OR 1=1--\n{{date_param_2}}=2026-01-01' /*\n' OR '1'='1\n1; DROP TABLE users--",
|
|
179
|
+
payload_type: "txt",
|
|
180
|
+
expected_validation: "Sanitização ou rejeição; query inalterada; sem erro 500.",
|
|
181
|
+
failure_criteria: "Erro 500, stack trace SQL ou dados fora do escopo esperado.",
|
|
182
|
+
risk: "Injeção SQL via parâmetros de filtro"
|
|
183
|
+
),
|
|
184
|
+
vector(
|
|
185
|
+
id: "enum-injection",
|
|
186
|
+
name: "Enum Injection",
|
|
187
|
+
category_slug: "ataques-de-injecao-e-manipulacao-de-params",
|
|
188
|
+
category_title: API_CATEGORIES[1].title,
|
|
189
|
+
layer: :api,
|
|
190
|
+
triggers: [:enum_params],
|
|
191
|
+
objective: "Enviar valores fora do enum/array homologado no parâmetro `{{enum_param}}`.",
|
|
192
|
+
procedure: [
|
|
193
|
+
"Enviar valores válidos como baseline ({{enum_values}}).",
|
|
194
|
+
"Testar `{{enum_param}}=all`, `{{enum_param}}=admin` e arrays malformados.",
|
|
195
|
+
"Verificar se o escopo da query permanece inalterado."
|
|
196
|
+
],
|
|
197
|
+
payload: "{{enum_param}}=all\n{{enum_param}}=admin\n{{enum_param}}[]=invalid",
|
|
198
|
+
payload_type: "txt",
|
|
199
|
+
expected_validation: "Rejeição ou ignore seguro; escopo da query inalterado.",
|
|
200
|
+
failure_criteria: "Dados retornados fora do canal/escopo esperado.",
|
|
201
|
+
risk: "Bypass de filtro por enum corrompido"
|
|
202
|
+
),
|
|
203
|
+
vector(
|
|
204
|
+
id: "timezone-manipulation",
|
|
205
|
+
name: "Timezone Manipulation",
|
|
206
|
+
category_slug: "ataques-de-injecao-e-manipulacao-de-params",
|
|
207
|
+
category_title: API_CATEGORIES[1].title,
|
|
208
|
+
layer: :api,
|
|
209
|
+
triggers: [:free_text_headers],
|
|
210
|
+
objective: "Explorar header `{{timezone_header}}` como string livre.",
|
|
211
|
+
procedure: [
|
|
212
|
+
"Enviar timezone inválido (ex: `Invalid/Zone`).",
|
|
213
|
+
"Testar CRLF injection no header.",
|
|
214
|
+
"Enviar buffer longo (8KB) para testar robustez do parser."
|
|
215
|
+
],
|
|
216
|
+
payload: "{{timezone_header}}: Invalid/Zone\n{{timezone_header}}: America/Sao_Paulo\\r\\nInjected-Header: true\n{{timezone_header}}: AAAA...(buffer 8KB)",
|
|
217
|
+
payload_type: "txt",
|
|
218
|
+
expected_validation: "Whitelist IANA ou rejeição; sem crash; sem header injection reflexivo.",
|
|
219
|
+
failure_criteria: "Crash, 500 ou headers injetados refletidos na resposta.",
|
|
220
|
+
risk: "Header injection e parsing inseguro"
|
|
221
|
+
),
|
|
222
|
+
vector(
|
|
223
|
+
id: "data-range-exhaustion",
|
|
224
|
+
name: "Data Range Exhaustion (DoS Lógico)",
|
|
225
|
+
category_slug: "seguranca-da-camada-de-negocio-e-logica",
|
|
226
|
+
category_title: API_CATEGORIES[2].title,
|
|
227
|
+
layer: :api,
|
|
228
|
+
triggers: [:date_range],
|
|
229
|
+
objective: "Solicitar intervalo de datas massivo para forçar processamento pesado.",
|
|
230
|
+
procedure: [
|
|
231
|
+
"Enviar `{{date_param_1}}=1900-01-01` e `{{date_param_2}}=2100-12-31`.",
|
|
232
|
+
"Medir tempo de resposta e consumo downstream.",
|
|
233
|
+
"Verificar se há limite máximo de intervalo."
|
|
234
|
+
],
|
|
235
|
+
payload: "{{date_param_1}}=1900-01-01\n{{date_param_2}}=2100-12-31",
|
|
236
|
+
payload_type: "txt",
|
|
237
|
+
expected_validation: "Limite máximo de intervalo; timeout controlado; 400 com mensagem clara.",
|
|
238
|
+
failure_criteria: "200 com processamento completo de range abusivo ou timeout sem controle.",
|
|
239
|
+
risk: "DoS lógico no banco / motor analítico"
|
|
240
|
+
),
|
|
241
|
+
vector(
|
|
242
|
+
id: "negative-values",
|
|
243
|
+
name: "Negative Values — datas invertidas",
|
|
244
|
+
category_slug: "seguranca-da-camada-de-negocio-e-logica",
|
|
245
|
+
category_title: API_CATEGORIES[2].title,
|
|
246
|
+
layer: :api,
|
|
247
|
+
triggers: [:date_range],
|
|
248
|
+
objective: "Enviar data de início posterior à data de fim.",
|
|
249
|
+
procedure: [
|
|
250
|
+
"Enviar `{{date_param_1}}=2026-12-31` e `{{date_param_2}}=2026-01-01`.",
|
|
251
|
+
"Observar código HTTP e consistência dos resultados."
|
|
252
|
+
],
|
|
253
|
+
payload: "{{date_param_1}}=2026-12-31\n{{date_param_2}}=2026-01-01",
|
|
254
|
+
payload_type: "txt",
|
|
255
|
+
expected_validation: "Rejeição com erro controlado; sem 500; sem resultados inconsistentes.",
|
|
256
|
+
failure_criteria: "500 ou resultados incoerentes retornados.",
|
|
257
|
+
risk: "Lógica de negócio frágil em validação cronológica"
|
|
258
|
+
),
|
|
259
|
+
vector(
|
|
260
|
+
id: "rate-limit-absence",
|
|
261
|
+
name: "Rate Limit Absence",
|
|
262
|
+
category_slug: "seguranca-da-camada-de-negocio-e-logica",
|
|
263
|
+
category_title: API_CATEGORIES[2].title,
|
|
264
|
+
layer: :api,
|
|
265
|
+
triggers: [:analytics],
|
|
266
|
+
objective: "Verificar proteção contra abuso volumétrico em endpoints analíticos.",
|
|
267
|
+
procedure: [
|
|
268
|
+
"Enviar N requisições paralelas (ex: 50 em 10s) com payload válido.",
|
|
269
|
+
"Observar se há throttling por tenant/usuário.",
|
|
270
|
+
"Documentar ausência como item de hardening se não houver 429."
|
|
271
|
+
],
|
|
272
|
+
payload: "50 requisições GET {{endpoint_path}} em 10 segundos",
|
|
273
|
+
payload_type: "txt",
|
|
274
|
+
expected_validation: "429 Too Many Requests ou throttling por tenant/usuário.",
|
|
275
|
+
failure_criteria: "Todas as requisições processadas sem limitação.",
|
|
276
|
+
risk: "DoS volumétrico em endpoint analítico"
|
|
277
|
+
),
|
|
278
|
+
vector(
|
|
279
|
+
id: "verbosity-check",
|
|
280
|
+
name: "Verbosity Check",
|
|
281
|
+
category_slug: "fuzzing-e-tratamento-de-erros",
|
|
282
|
+
category_title: API_CATEGORIES[3].title,
|
|
283
|
+
layer: :api,
|
|
284
|
+
triggers: [:enum_params],
|
|
285
|
+
objective: "Provocar erros 400/404 e inspecionar corpo da resposta.",
|
|
286
|
+
procedure: [
|
|
287
|
+
"Path inexistente → 404.",
|
|
288
|
+
"Parâmetro malformado → 400.",
|
|
289
|
+
"Body JSON inválido → 400 (se aplicável)."
|
|
290
|
+
],
|
|
291
|
+
payload: "GET /api/v1/inexistente HTTP/1.1\nGET {{endpoint_path}}?{{date_param_1}}=INVALID HTTP/1.1",
|
|
292
|
+
payload_type: "http",
|
|
293
|
+
expected_validation: "Mensagens genéricas; sem stack trace, paths internos ou nomes de colunas.",
|
|
294
|
+
failure_criteria: "Vazamento de stack trace, `/usr/src/app`, nomes de tabelas ou versões de framework.",
|
|
295
|
+
risk: "Information disclosure via error handling"
|
|
296
|
+
),
|
|
297
|
+
vector(
|
|
298
|
+
id: "type-juggling",
|
|
299
|
+
name: "Type Juggling",
|
|
300
|
+
category_slug: "fuzzing-e-tratamento-de-erros",
|
|
301
|
+
category_title: API_CATEGORIES[3].title,
|
|
302
|
+
layer: :api,
|
|
303
|
+
triggers: [:enum_params],
|
|
304
|
+
objective: "Enviar tipos inesperados onde se espera array/object/primitive em `{{enum_param}}`.",
|
|
305
|
+
procedure: [
|
|
306
|
+
"Enviar boolean, hash, null e integer no lugar de array.",
|
|
307
|
+
"Observar código HTTP e robustez do parser JSON."
|
|
308
|
+
],
|
|
309
|
+
payload: "{ \"{{enum_param}}\": true }\n{ \"{{enum_param}}\": { \"value\": \"email\" } }\n{ \"{{enum_param}}\": null }",
|
|
310
|
+
payload_type: "json",
|
|
311
|
+
expected_validation: "Parser JSON robusto; 400 controlado; sem 500.",
|
|
312
|
+
failure_criteria: "500 ou execução parcial de lógica de negócio.",
|
|
313
|
+
risk: "Type confusion em parsing de entrada"
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def datalake_vectors
|
|
319
|
+
[
|
|
320
|
+
vector(
|
|
321
|
+
id: "bypass-tenant",
|
|
322
|
+
name: "Bypass de Tenant",
|
|
323
|
+
category_slug: "testes-de-injecao-e-logica",
|
|
324
|
+
category_title: DATALAKE_CATEGORIES[0].title,
|
|
325
|
+
layer: :datalake,
|
|
326
|
+
triggers: [:datalake],
|
|
327
|
+
objective: "Omitir ou neutralizar filtro de tenant na query final.",
|
|
328
|
+
procedure: [
|
|
329
|
+
"Executar query sem cláusula WHERE tenant_id.",
|
|
330
|
+
"Forçar bypass lógico com `OR 1=1`.",
|
|
331
|
+
"Verificar se row-level security impede cross-tenant."
|
|
332
|
+
],
|
|
333
|
+
payload: "SELECT count(*) FROM `project.dataset.table`;\n\nSELECT count(*) FROM `project.dataset.table`\nWHERE tenant_id = @tenant_id OR 1=1;",
|
|
334
|
+
payload_type: "sql",
|
|
335
|
+
expected_validation: "Impossível cruzar dados de múltiplos tenants; parametrização enforced.",
|
|
336
|
+
failure_criteria: "Contagem ou dados de múltiplos tenants retornados.",
|
|
337
|
+
risk: "Cross-Tenant Data Leak no Data Lake"
|
|
338
|
+
),
|
|
339
|
+
vector(
|
|
340
|
+
id: "union-exploitation",
|
|
341
|
+
name: "Union Exploitation",
|
|
342
|
+
category_slug: "testes-de-injecao-e-logica",
|
|
343
|
+
category_title: DATALAKE_CATEGORIES[0].title,
|
|
344
|
+
layer: :datalake,
|
|
345
|
+
triggers: [:datalake],
|
|
346
|
+
objective: "Injetar UNION apontando para metadados ou tabelas sensíveis.",
|
|
347
|
+
procedure: [
|
|
348
|
+
"Simular input malicioso chegando à camada SQL.",
|
|
349
|
+
"Tentar UNION ALL SELECT em INFORMATION_SCHEMA.",
|
|
350
|
+
"Verificar permissões da Service Account."
|
|
351
|
+
],
|
|
352
|
+
payload: "SELECT cols FROM `table` WHERE tenant_id = @tenant_id\nUNION ALL SELECT table_name, column_name, data_type\nFROM `project.dataset.INFORMATION_SCHEMA.COLUMNS`;",
|
|
353
|
+
payload_type: "sql",
|
|
354
|
+
expected_validation: "SA sem permissão de leitura em INFORMATION_SCHEMA alheio; query rejeitada.",
|
|
355
|
+
failure_criteria: "Metadados de tabelas sensíveis retornados.",
|
|
356
|
+
risk: "Schema enumeration via SQL injection"
|
|
357
|
+
),
|
|
358
|
+
vector(
|
|
359
|
+
id: "sa-overprivilege",
|
|
360
|
+
name: "Service Account Overprivilege",
|
|
361
|
+
category_slug: "testes-de-controle-de-acesso",
|
|
362
|
+
category_title: DATALAKE_CATEGORIES[1].title,
|
|
363
|
+
layer: :datalake,
|
|
364
|
+
triggers: [:datalake],
|
|
365
|
+
objective: "Verificar se a Service Account da API pode executar DDL/DML.",
|
|
366
|
+
procedure: [
|
|
367
|
+
"Tentar CREATE TABLE, DROP TABLE, INSERT, UPDATE, DELETE.",
|
|
368
|
+
"Verificar roles atribuídas (bigquery.dataViewer, jobUser)."
|
|
369
|
+
],
|
|
370
|
+
payload: "CREATE TABLE test_pentest (id INT64);\nDROP TABLE `project.dataset.table`;\nINSERT INTO `table` VALUES (1);",
|
|
371
|
+
payload_type: "sql",
|
|
372
|
+
expected_validation: "SA estritamente Read-Only; DDL/DML rejeitados.",
|
|
373
|
+
failure_criteria: "Qualquer operação de escrita executada com sucesso.",
|
|
374
|
+
risk: "Comprometimento de integridade de dados"
|
|
375
|
+
),
|
|
376
|
+
vector(
|
|
377
|
+
id: "metadata-leak",
|
|
378
|
+
name: "Metadata Leak",
|
|
379
|
+
category_slug: "testes-de-controle-de-acesso",
|
|
380
|
+
category_title: DATALAKE_CATEGORIES[1].title,
|
|
381
|
+
layer: :datalake,
|
|
382
|
+
triggers: [:datalake],
|
|
383
|
+
objective: "Mapear estrutura física via metadados (INFORMATION_SCHEMA).",
|
|
384
|
+
procedure: [
|
|
385
|
+
"Consultar INFORMATION_SCHEMA.COLUMNS e TABLES.",
|
|
386
|
+
"Verificar se SA expõe clusterização/particionamento.",
|
|
387
|
+
"Documentar como ATENÇÃO se leitura permitida sem vazamento de dados de negócio."
|
|
388
|
+
],
|
|
389
|
+
payload: "SELECT * FROM `project.dataset.INFORMATION_SCHEMA.COLUMNS` WHERE table_name LIKE '%';\nSELECT ddl FROM `project.dataset.INFORMATION_SCHEMA.TABLES`;",
|
|
390
|
+
payload_type: "sql",
|
|
391
|
+
expected_validation: "Mínimo privilégio; SA não expõe metadados sensíveis via API.",
|
|
392
|
+
failure_criteria: "DDL completo ou esquema físico exposto a atacante.",
|
|
393
|
+
risk: "Information disclosure de metadados"
|
|
394
|
+
),
|
|
395
|
+
vector(
|
|
396
|
+
id: "cartesian-product",
|
|
397
|
+
name: "Cartesian Product (Cross Join)",
|
|
398
|
+
category_slug: "testes-de-performance-e-custo",
|
|
399
|
+
category_title: DATALAKE_CATEGORIES[2].title,
|
|
400
|
+
layer: :datalake,
|
|
401
|
+
triggers: [:datalake],
|
|
402
|
+
objective: "Forçar cross join entre tabelas grandes para exaurir cotas.",
|
|
403
|
+
procedure: [
|
|
404
|
+
"Executar CROSS JOIN entre tabelas de alto volume.",
|
|
405
|
+
"Monitorar bytes billed e tempo de execução."
|
|
406
|
+
],
|
|
407
|
+
payload: "SELECT a.*, b.*\nFROM `big_table` a\nCROSS JOIN `big_table` b\nWHERE a.tenant_id = @tenant_id\nLIMIT 100;",
|
|
408
|
+
payload_type: "sql",
|
|
409
|
+
expected_validation: "Timeout, quota exceeded ou bloqueio por policy; custo monitorado.",
|
|
410
|
+
failure_criteria: "Query executada com custo financeiro significativo sem alerta.",
|
|
411
|
+
risk: "DoS financeiro no Data Lake"
|
|
412
|
+
),
|
|
413
|
+
vector(
|
|
414
|
+
id: "full-scan",
|
|
415
|
+
name: "Full Scan Forçado",
|
|
416
|
+
category_slug: "testes-de-performance-e-custo",
|
|
417
|
+
category_title: DATALAKE_CATEGORIES[2].title,
|
|
418
|
+
layer: :datalake,
|
|
419
|
+
triggers: [:datalake],
|
|
420
|
+
objective: "Query que ignora coluna de partição.",
|
|
421
|
+
procedure: [
|
|
422
|
+
"Executar COUNT sem filtro de partição.",
|
|
423
|
+
"Comparar bytes billed com query particionada.",
|
|
424
|
+
"Verificar se API sempre injeta filtro de partição."
|
|
425
|
+
],
|
|
426
|
+
payload: "SELECT count(*) FROM `partitioned_table` WHERE tenant_id = @tenant_id;\n\n-- Comparar com:\nSELECT count(*) FROM `partitioned_table`\nWHERE tenant_id = @tenant_id AND partition_date BETWEEN '2026-01-01' AND '2026-01-31';",
|
|
427
|
+
payload_type: "sql",
|
|
428
|
+
expected_validation: "API sempre injeta filtro de partição; bytes billed controlados.",
|
|
429
|
+
failure_criteria: "Full scan de tabela particionada sem restrição.",
|
|
430
|
+
risk: "DoS de custo por scan completo"
|
|
431
|
+
)
|
|
432
|
+
]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def vector(**attrs)
|
|
436
|
+
Vector.new(**attrs)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "security_pentest_planner/version"
|
|
4
|
+
require_relative "security_pentest_planner/errors"
|
|
5
|
+
require_relative "security_pentest_planner/openapi_parser"
|
|
6
|
+
require_relative "security_pentest_planner/contract_analyzer"
|
|
7
|
+
require_relative "security_pentest_planner/vector_catalog"
|
|
8
|
+
require_relative "security_pentest_planner/plan_renderer"
|
|
9
|
+
require_relative "security_pentest_planner/planner"
|
|
10
|
+
require_relative "security_pentest_planner/cli"
|
|
11
|
+
|
|
12
|
+
module SecurityPentestPlanner
|
|
13
|
+
class << self
|
|
14
|
+
def generate(input_path:, **options)
|
|
15
|
+
Planner.new(input_path: input_path, **options).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_from_spec(spec:, **options)
|
|
19
|
+
Planner.new(spec: spec, **options).call
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: security_pentest_planner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Saulo Filho
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
description: |
|
|
41
|
+
Ruby gem que analisa contratos OpenAPI/Swagger e gera Planos de Ação de Pentest
|
|
42
|
+
estruturados com vetores OWASP API Security, procedimentos, payloads, critérios
|
|
43
|
+
de validação e recomendações de remediação para sistemas multi-tenant e Data Lake.
|
|
44
|
+
email:
|
|
45
|
+
- saulofilho@users.noreply.github.com
|
|
46
|
+
executables:
|
|
47
|
+
- security-pentest-planner
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- exe/security-pentest-planner
|
|
55
|
+
- lib/security_pentest_planner.rb
|
|
56
|
+
- lib/security_pentest_planner/cli.rb
|
|
57
|
+
- lib/security_pentest_planner/contract_analyzer.rb
|
|
58
|
+
- lib/security_pentest_planner/errors.rb
|
|
59
|
+
- lib/security_pentest_planner/openapi_parser.rb
|
|
60
|
+
- lib/security_pentest_planner/plan_renderer.rb
|
|
61
|
+
- lib/security_pentest_planner/planner.rb
|
|
62
|
+
- lib/security_pentest_planner/vector_catalog.rb
|
|
63
|
+
- lib/security_pentest_planner/version.rb
|
|
64
|
+
homepage: https://saulofilho.github.io/security-pentest-planner/
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata:
|
|
68
|
+
homepage_uri: https://saulofilho.github.io/security-pentest-planner/
|
|
69
|
+
source_code_uri: https://github.com/saulofilho/security-pentest-planner
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 3.0.0
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 3.7.1
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Gera Planos de Ação de Pentest a partir de especificações OpenAPI
|
|
88
|
+
test_files: []
|