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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bcc45ece1c0bd69559144e7fe445ccf7d6b5ebef75ead44a5c154a466bdc9a96
4
+ data.tar.gz: 2e697b751eba0431ed172b985700e1d38289f5755212dbc4db240536cf4a805a
5
+ SHA512:
6
+ metadata.gz: 9508039a75d5287b49f5c9f9c73d2136e848d8e03d2b3cb41fb698c43474eecc67ff796dc901be0695291346f949ad209dd41b67e79eec3e19434c144dc09dda
7
+ data.tar.gz: 7dfadfcb457ce8177642f56bf48222b6d3f92f9ff859ce0f2d7f5c1735747e7afa87564b8cca2f390588a603b8a00646149717aec45ef83e9e37c0997dcc1e5e
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## [1.0.1] - 2026-07-07
4
+
5
+ ### Fixed
6
+
7
+ - YAML parser no longer references unresolved `Date`/`Time` constants inside `OpenAPIParser`, fixing CI on Ruby 3.1–3.3
8
+
9
+ ## [1.0.0] - 2026-06-26
10
+
11
+ ### Added
12
+
13
+ - Parser OpenAPI 3.x (YAML/JSON)
14
+ - Analisador de contrato com detecção de red flags
15
+ - Catálogo de vetores OWASP API Security e Data Lake
16
+ - Renderizador de Plano de Ação em Markdown
17
+ - CLI `security-pentest-planner`
18
+ - API programática `SecurityPentestPlanner.generate`
19
+
20
+ ## [Unreleased]
21
+
22
+ ### Changed
23
+
24
+ - Repositório movido para projeto pessoal open source
25
+ - Removidas referências corporativas e exemplos específicos de produto interno
26
+ - Defaults genéricos para camadas API (`API` em vez de nome de produto)
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saulo Filho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # security_pentest_planner
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/security_pentest_planner.svg)](https://badge.fury.io/rb/security_pentest_planner)
4
+
5
+ **Site:** [saulofilho.github.io/security-pentest-planner](https://saulofilho.github.io/security-pentest-planner/)
6
+
7
+ Gera **Planos de Ação de Pentest** a partir de especificações OpenAPI/Swagger.
8
+
9
+ Ferramenta open source para times de engenharia e AppSec que precisam estruturar testes ofensivos **antes** da execução — com vetores OWASP, procedimentos, payloads e critérios de validação.
10
+
11
+ ## O que ela faz
12
+
13
+ - Analisa contratos **OpenAPI 3.x** (YAML ou JSON)
14
+ - Detecta sinais de risco: tenant headers, parâmetros de data, enums, timezone livre
15
+ - Seleciona vetores OWASP API Security aplicáveis via taxonomia
16
+ - Gera um **design doc** em Markdown com:
17
+ - Procedimentos passo a passo por vetor
18
+ - Payloads e exemplos HTTP/SQL
19
+ - Tabela resumo (Acesso · Entrada · Disponibilidade · Informação)
20
+ - Recomendações imediatas de remediação
21
+ - Red flags do contrato
22
+
23
+ ## O que ela NÃO faz
24
+
25
+ - Scripts executáveis (Faraday, Ruby, SQL runnable)
26
+ - Resultados de teste (APROVADO/REPROVADO)
27
+ - RFCs de implementação
28
+
29
+ Esses artefatos pertencem à **fase de execução** do pentest.
30
+
31
+ ## Instalação
32
+
33
+ ```bash
34
+ gem install security_pentest_planner
35
+ ```
36
+
37
+ Ou no `Gemfile`:
38
+
39
+ ```ruby
40
+ gem "security_pentest_planner"
41
+ ```
42
+
43
+ ## Uso via CLI
44
+
45
+ ```bash
46
+ # Gerar plano no stdout
47
+ security-pentest-planner swagger/v2/swagger.json
48
+
49
+ # Salvar em arquivo
50
+ security-pentest-planner openapi.yaml -o plano-de-acao-pentest.md
51
+
52
+ # Com escopo e Data Lake
53
+ security-pentest-planner openapi.yaml \
54
+ --team "Platform Team" \
55
+ --scope /api/v1/metrics \
56
+ --datalake \
57
+ -o plano.md
58
+ ```
59
+
60
+ ### Opções
61
+
62
+ | Flag | Descrição |
63
+ |------|-----------|
64
+ | `-t, --team TEAM` | Nome do time no título |
65
+ | `-s, --scope PATH` | Limitar a um endpoint |
66
+ | `-d, --datalake` | Incluir vetores de Data Lake |
67
+ | `--api-layer NAME` | Nome da camada API (default: API) |
68
+ | `--data-layer NAME` | Nome da camada Data Lake (default: Data Lake) |
69
+ | `-o, --output FILE` | Salvar em arquivo |
70
+
71
+ ## Uso programático
72
+
73
+ ```ruby
74
+ require "security_pentest_planner"
75
+
76
+ plan = SecurityPentestPlanner.generate(
77
+ input_path: "openapi.yaml",
78
+ team: "Platform Team",
79
+ scope: "/api/v1/metrics",
80
+ include_datalake: true
81
+ )
82
+
83
+ File.write("plano-de-acao-pentest.md", plan)
84
+ ```
85
+
86
+ ## Publicar no RubyGems.org
87
+
88
+ ```bash
89
+ bundle install
90
+ bundle exec rspec
91
+ gem build security_pentest_planner.gemspec
92
+ gem push security_pentest_planner-1.0.0.gem
93
+ ```
94
+
95
+ > Requer conta no [RubyGems.org](https://rubygems.org) e MFA habilitado.
96
+
97
+ ## Desenvolvimento
98
+
99
+ ```bash
100
+ git clone https://github.com/saulofilho/security-pentest-planner.git
101
+ cd security-pentest-planner
102
+ bundle install
103
+ bundle exec rspec
104
+ bundle exec rake
105
+ ```
106
+
107
+ ## Fluxo recomendado
108
+
109
+ ```
110
+ OpenAPI spec → security_pentest_planner → Plano de Ação (Markdown)
111
+
112
+ Execução manual / CI
113
+
114
+ Resultados + RFCs
115
+ ```
116
+
117
+ A gem cobre a fase de **planejamento** de forma determinística — ideal para CI/CD, pipelines e integração em projetos Ruby/Rails.
118
+
119
+ ## Licença
120
+
121
+ MIT — veja [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "security_pentest_planner/cli"
6
+
7
+ SecurityPentestPlanner::CLI.run
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require_relative "../security_pentest_planner"
6
+
7
+ module SecurityPentestPlanner
8
+ class CLI
9
+ def self.run(argv = ARGV)
10
+ new.run(argv)
11
+ end
12
+
13
+ def run(argv)
14
+ options = default_options
15
+ parser = build_option_parser(options)
16
+ parser.parse!(argv)
17
+
18
+ input = argv.first
19
+ abort parser.help if input.nil?
20
+
21
+ plan = SecurityPentestPlanner.generate(
22
+ input_path: input,
23
+ **options
24
+ )
25
+
26
+ if options[:output]
27
+ File.write(options[:output], plan)
28
+ warn "Plano de Ação gerado em #{options[:output]}"
29
+ else
30
+ puts plan
31
+ end
32
+ rescue Error => e
33
+ warn "Erro: #{e.message}"
34
+ exit 1
35
+ end
36
+
37
+ private
38
+
39
+ def default_options
40
+ {
41
+ team: nil,
42
+ scope: nil,
43
+ include_datalake: false,
44
+ api_layer: "API",
45
+ data_layer: "Data Lake",
46
+ output: nil
47
+ }
48
+ end
49
+
50
+ def build_option_parser(options)
51
+ OptionParser.new do |opts|
52
+ opts.banner = <<~BANNER
53
+ Usage: security-pentest-planner [options] <openapi.yaml>
54
+
55
+ Gera um Plano de Ação de Pentest a partir de especificação OpenAPI.
56
+
57
+ BANNER
58
+
59
+ opts.on("-t", "--team TEAM", "Nome do time no título do documento") do |team|
60
+ options[:team] = team
61
+ end
62
+
63
+ opts.on("-s", "--scope PATH", "Limitar escopo a um path (ex: /api/v1/metrics)") do |scope|
64
+ options[:scope] = scope
65
+ end
66
+
67
+ opts.on("-d", "--datalake", "Incluir vetores de Data Lake no plano") do
68
+ options[:include_datalake] = true
69
+ end
70
+
71
+ opts.on("--api-layer NAME", "Nome da camada de API no documento") do |name|
72
+ options[:api_layer] = name
73
+ end
74
+
75
+ opts.on("--data-layer NAME", "Nome da camada de Data Lake no documento") do |name|
76
+ options[:data_layer] = name
77
+ end
78
+
79
+ opts.on("-o", "--output FILE", "Salvar plano em arquivo (default: stdout)") do |file|
80
+ options[:output] = file
81
+ end
82
+
83
+ opts.on("-v", "--version", "Exibe a versão") do
84
+ puts "security_pentest_planner #{SecurityPentestPlanner::VERSION}"
85
+ exit
86
+ end
87
+
88
+ opts.on("-h", "--help", "Exibe esta ajuda") do
89
+ puts opts
90
+ exit
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ SecurityPentestPlanner::CLI.run if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecurityPentestPlanner
4
+ class ContractAnalyzer
5
+ RedFlag = Struct.new(:message, :severity, keyword_init: true)
6
+
7
+ attr_reader :parser, :scope_paths, :include_datalake
8
+
9
+ def initialize(parser, scope_paths: nil, include_datalake: false)
10
+ @parser = parser
11
+ @scope_paths = scope_paths
12
+ @include_datalake = include_datalake
13
+ end
14
+
15
+ def scoped_endpoints
16
+ endpoints = parser.endpoints
17
+ return endpoints if scope_paths.nil? || scope_paths.empty?
18
+
19
+ paths = Array(scope_paths)
20
+ endpoints.select { |ep| paths.any? { |p| ep.path == p || ep.path.start_with?(p) } }
21
+ end
22
+
23
+ def tenant_headers
24
+ scoped_endpoints.flat_map { |ep| parameters_for(ep).select(&:tenant_header?) }
25
+ .uniq { |p| p.name }
26
+ end
27
+
28
+ def date_params
29
+ scoped_endpoints.flat_map { |ep| parameters_for(ep).select(&:date_param?) }
30
+ .uniq { |p| p.name }
31
+ end
32
+
33
+ def enum_params
34
+ scoped_endpoints.flat_map { |ep| parameters_for(ep).select(&:enum_param?) }
35
+ .uniq { |p| p.name }
36
+ end
37
+
38
+ def timezone_headers
39
+ scoped_endpoints.flat_map { |ep| parameters_for(ep).select(&:timezone_header?) }
40
+ .uniq { |p| p.name }
41
+ end
42
+
43
+ def has_tenant_scoping?
44
+ tenant_headers.any?
45
+ end
46
+
47
+ def has_date_range?
48
+ date_params.any?
49
+ end
50
+
51
+ def has_enum_params?
52
+ enum_params.any?
53
+ end
54
+
55
+ def has_free_text_headers?
56
+ timezone_headers.any? || tenant_headers.any? { |h| !h.enum_values.any? }
57
+ end
58
+
59
+ def analytics_endpoint?
60
+ scoped_endpoints.any? do |ep|
61
+ ep.path.match?(/metric|analytic|report|channel|insight|dashboard|aggregate/i) ||
62
+ ep.summary.to_s.match?(/metric|analytic|report|aggregate/i)
63
+ end
64
+ end
65
+
66
+ def red_flags
67
+ flags = []
68
+
69
+ if parser.security_schemes.empty? && scoped_endpoints.any?
70
+ flags << RedFlag.new(
71
+ message: "Ausência de `securitySchemes` documentado no contrato OpenAPI",
72
+ severity: :high
73
+ )
74
+ end
75
+
76
+ tenant_headers.each do |header|
77
+ next if header.required
78
+
79
+ flags << RedFlag.new(
80
+ message: "Header `#{header.name}` documentado como opcional (`required: false`)",
81
+ severity: :high
82
+ )
83
+ end
84
+
85
+ if tenant_headers.map(&:name).uniq.size > 1
86
+ flags << RedFlag.new(
87
+ message: "Divergência de nomes de header de tenant: #{tenant_headers.map(&:name).uniq.join(' vs ')}",
88
+ severity: :medium
89
+ )
90
+ end
91
+
92
+ timezone_headers.each do |header|
93
+ next if header.enum_values.any?
94
+
95
+ flags << RedFlag.new(
96
+ message: "Header `#{header.name}` como string livre (sem enum/whitelist IANA)",
97
+ severity: :medium
98
+ )
99
+ end
100
+
101
+ if analytics_endpoint?
102
+ flags << RedFlag.new(
103
+ message: "Nenhum rate limiting documentado em endpoint analítico",
104
+ severity: :medium
105
+ )
106
+ end
107
+
108
+ flags
109
+ end
110
+
111
+ def signals
112
+ {
113
+ tenant_scoping: has_tenant_scoping?,
114
+ date_range: has_date_range?,
115
+ enum_params: has_enum_params?,
116
+ free_text_headers: has_free_text_headers?,
117
+ analytics: analytics_endpoint?,
118
+ datalake: include_datalake
119
+ }
120
+ end
121
+
122
+ def primary_endpoint
123
+ scoped_endpoints.first
124
+ end
125
+
126
+ def host
127
+ parser.servers.first || "api.example.com"
128
+ end
129
+
130
+ def tenant_header_name
131
+ tenant_headers.first&.name || "X-Tenant-ID"
132
+ end
133
+
134
+ def date_param_names
135
+ date_params.map(&:name)
136
+ end
137
+
138
+ def enum_param
139
+ enum_params.first
140
+ end
141
+
142
+ private
143
+
144
+ def parameters_for(endpoint)
145
+ parser.parameters_for(endpoint)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SecurityPentestPlanner
4
+ class Error < StandardError; end
5
+ class ParseError < Error; end
6
+ class InputError < Error; end
7
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module SecurityPentestPlanner
7
+ class OpenAPIParser
8
+ TENANT_HEADER_PATTERN = /tenant/i
9
+ DATE_PARAM_PATTERN = /date|start|end|from|to|period|range/i
10
+ TIMEZONE_HEADER_PATTERN = /timezone|time_zone|tz/i
11
+
12
+ Endpoint = Struct.new(
13
+ :path,
14
+ :method,
15
+ :operation_id,
16
+ :summary,
17
+ :parameters,
18
+ :security,
19
+ keyword_init: true
20
+ )
21
+
22
+ Parameter = Struct.new(
23
+ :name,
24
+ :location,
25
+ :required,
26
+ :schema,
27
+ :description,
28
+ keyword_init: true
29
+ ) do
30
+ def enum_values
31
+ schema&.dig("enum") || schema&.dig("items", "enum") || []
32
+ end
33
+
34
+ def type
35
+ schema&.dig("type")
36
+ end
37
+
38
+ def tenant_header?
39
+ location == "header" && name.match?(TENANT_HEADER_PATTERN)
40
+ end
41
+
42
+ def date_param?
43
+ location == "query" && name.match?(DATE_PARAM_PATTERN)
44
+ end
45
+
46
+ def timezone_header?
47
+ location == "header" && name.match?(TIMEZONE_HEADER_PATTERN)
48
+ end
49
+
50
+ def enum_param?
51
+ enum_values.any? || schema&.dig("items", "enum")&.any?
52
+ end
53
+ end
54
+
55
+ attr_reader :raw, :source_path
56
+
57
+ def initialize(content, source_path: nil)
58
+ @raw = content
59
+ @source_path = source_path
60
+ end
61
+
62
+ def self.parse_file(path)
63
+ content = File.read(path)
64
+ parsed = case File.extname(path).downcase
65
+ when ".yaml", ".yml" then YAML.safe_load(content, aliases: true)
66
+ when ".json" then JSON.parse(content)
67
+ else
68
+ raise ParseError, "Unsupported file extension: #{path}. Use .yaml, .yml or .json"
69
+ end
70
+ new(parsed, source_path: path)
71
+ end
72
+
73
+ def title
74
+ info["title"]
75
+ end
76
+
77
+ def version
78
+ info["version"]
79
+ end
80
+
81
+ def servers
82
+ Array(raw["servers"]).map { |s| s["url"] }.compact
83
+ end
84
+
85
+ def security_schemes
86
+ raw.dig("components", "securitySchemes") || {}
87
+ end
88
+
89
+ def endpoints
90
+ @endpoints ||= build_endpoints
91
+ end
92
+
93
+ def parameters_for(endpoint)
94
+ global = Array(raw.dig("paths", endpoint.path, "parameters"))
95
+ local = Array(endpoint.parameters)
96
+ merge_parameters(global + local)
97
+ end
98
+
99
+ private
100
+
101
+ def info
102
+ raw["info"] || {}
103
+ end
104
+
105
+ def build_endpoints
106
+ paths = raw["paths"] || {}
107
+ paths.flat_map do |path, methods|
108
+ methods.filter_map do |method, operation|
109
+ next unless operation.is_a?(Hash)
110
+ next unless %w[get post put patch delete head options].include?(method)
111
+
112
+ Endpoint.new(
113
+ path: path,
114
+ method: method.upcase,
115
+ operation_id: operation["operationId"],
116
+ summary: operation["summary"] || operation["description"],
117
+ parameters: Array(operation["parameters"]).map { |p| build_parameter(p) },
118
+ security: operation["security"]
119
+ )
120
+ end
121
+ end
122
+ end
123
+
124
+ def build_parameter(param)
125
+ resolved = resolve_ref(param)
126
+ Parameter.new(
127
+ name: resolved["name"],
128
+ location: resolved["in"],
129
+ required: resolved["required"] == true,
130
+ schema: resolved["schema"] || {},
131
+ description: resolved["description"]
132
+ )
133
+ end
134
+
135
+ def resolve_ref(obj)
136
+ return obj unless obj.is_a?(Hash) && obj["$ref"]
137
+
138
+ ref = obj["$ref"]
139
+ parts = ref.split("/")
140
+ parts.reduce(raw) { |node, key| node[key] }
141
+ end
142
+
143
+ def merge_parameters(params)
144
+ params
145
+ .map { |p| p.is_a?(Parameter) ? p : build_parameter(p) }
146
+ .uniq { |p| [p.location, p.name] }
147
+ end
148
+ end
149
+ end