pwn 0.5.739 → 0.5.741
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 +4 -4
- data/Gemfile +2 -2
- data/documentation/Agent-Tool-Registry.md +163 -6
- data/documentation/Diagrams.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Plugin-Degradation.md +6 -2
- data/documentation/Recon-Findings-API.md +33 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
- data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
- data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
- data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
- data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +8 -2
- data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/SKILL.md +2 -0
- data/lib/pwn/ai/agent/confirmation.rb +163 -0
- data/lib/pwn/ai/agent/dispatch.rb +43 -5
- data/lib/pwn/ai/agent/loop.rb +45 -11
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/result.rb +76 -6
- data/lib/pwn/ai/agent/task_dag.rb +220 -0
- data/lib/pwn/ai/agent/tool_guard.rb +166 -24
- data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
- data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
- data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
- data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
- data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
- data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
- data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
- data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
- data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
- data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
- data/lib/pwn/ai/agent/tools/shell.rb +22 -10
- data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
- data/lib/pwn/ai/agent.rb +3 -1
- data/lib/pwn/ai/cli.rb +26 -2
- data/lib/pwn/ai/context.rb +4 -2
- data/lib/pwn/ai/router.rb +196 -0
- data/lib/pwn/ai/tools/README.md +5 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +10 -0
- data/lib/pwn/engagement.rb +45 -1
- data/lib/pwn/ffi/capstone.rb +120 -0
- data/lib/pwn/ffi/keystone.rb +107 -0
- data/lib/pwn/ffi.rb +2 -0
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/aflplusplus.rb +238 -13
- data/lib/pwn/plugins/ai_sandbox.rb +240 -0
- data/lib/pwn/plugins/artifact_registry.rb +188 -22
- data/lib/pwn/plugins/assembly.rb +73 -2
- data/lib/pwn/plugins/basic_auth.rb +17 -4
- data/lib/pwn/plugins/binary_parser.rb +439 -38
- data/lib/pwn/plugins/exploit_dev.rb +83 -14
- data/lib/pwn/plugins/findings.rb +208 -42
- data/lib/pwn/plugins/gdbmi.rb +265 -38
- data/lib/pwn/plugins/httpx.rb +93 -0
- data/lib/pwn/plugins/jobs.rb +260 -102
- data/lib/pwn/plugins/method_catalog.rb +328 -0
- data/lib/pwn/plugins/nmap_it.rb +157 -26
- data/lib/pwn/plugins/nuclei.rb +122 -29
- data/lib/pwn/plugins/preflight_checker.rb +3 -2
- data/lib/pwn/plugins/process_tube.rb +51 -2
- data/lib/pwn/plugins/radare2.rb +120 -26
- data/lib/pwn/plugins/recon.rb +33 -0
- data/lib/pwn/plugins/repl/mesh.rb +203 -13
- data/lib/pwn/plugins/rop.rb +121 -31
- data/lib/pwn/plugins/transparent_browser.rb +212 -14
- data/lib/pwn/plugins/vault.rb +182 -24
- data/lib/pwn/plugins.rb +4 -0
- data/lib/pwn/reports/engagement.rb +8 -2
- data/lib/pwn/reports/html.rb +28 -9
- data/lib/pwn/reports/json.rb +2 -1
- data/lib/pwn/reports/markdown.rb +49 -12
- data/lib/pwn/reports/sarif.rb +17 -3
- data/lib/pwn/reports.rb +184 -34
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reports_spec.rb +93 -0
- data/spec/integration/result_condition_spec.rb +11 -8
- data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
- data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
- data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
- data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
- data/spec/lib/pwn/ai/cli_spec.rb +12 -1
- data/spec/lib/pwn/ai/router_spec.rb +57 -0
- data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
- data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
- data/spec/lib/pwn/migrate_spec.rb +30 -2
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
- data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
- data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
- data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
- data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
- data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
- data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
- data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
- data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
- data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
- data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
- data/spec/lib/pwn/reports_spec.rb +196 -0
- data/spec/spec_helper.rb +3 -0
- data/third_party/pwn_rdoc.jsonl +230 -4
- metadata +47 -5
|
@@ -3,6 +3,202 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
describe PWN::Reports do
|
|
6
|
+
it 'ranks an evidenced critical directed path above isolated high findings without rewriting member CVSS' do
|
|
7
|
+
Dir.mktmpdir do |dir|
|
|
8
|
+
path = File.join(dir, 'combined.txt')
|
|
9
|
+
File.write(path, 'SSRF enables internal-admin access and demonstrated full administrative control in this fixture.')
|
|
10
|
+
evidence = { stored: path, path: path, sha256: Digest::SHA256.file(path).hexdigest, size: File.size(path) }
|
|
11
|
+
rows = [{ id: 'ssrf', title: 'SSRF', severity: 'medium', enables: ['admin'] },
|
|
12
|
+
{ id: 'admin', title: 'Internal admin panel', severity: 'medium', chain_assessments: [
|
|
13
|
+
{ finding_ids: %w[ssrf admin], combined_severity: 'critical', rationale: 'Full administrative control through the SSRF.',
|
|
14
|
+
reproduction_steps: ['Send the SSRF request.', 'Observe internal administrative control.'], evidence_artifacts: [evidence] }
|
|
15
|
+
] }, { id: 'isolated', title: 'Isolated high', severity: 'high' }]
|
|
16
|
+
payload = described_class.report_payload(results_hash: { findings: rows })
|
|
17
|
+
expect(payload[:attack_chains].length).to eq(1)
|
|
18
|
+
expect(payload[:attack_chains].first).to include(finding_ids: %w[ssrf admin], combined_severity: 'critical', assessment_status: 'evidence_backed', links: [{ from: 'ssrf', to: 'admin' }])
|
|
19
|
+
expect(payload[:priorities].map { |item| item[:combined_severity] }).to eq(%w[critical high])
|
|
20
|
+
expect(payload[:findings].first['severity']).to eq('medium')
|
|
21
|
+
packaged = described_class.package_evidence(payload: payload, path: File.join(dir, 'report.json'))
|
|
22
|
+
attachment = packaged[:attack_chains].first[:evidence_artifacts].first[:attachment]
|
|
23
|
+
expect(attachment).to match(%r{\Aattachments/[a-f0-9]{64}\.bin\z})
|
|
24
|
+
expect(Digest::SHA256.file(File.join(dir, attachment)).hexdigest).to eq(evidence[:sha256])
|
|
25
|
+
expect(packaged[:priorities].first[:evidence_artifacts].first[:attachment]).to eq(attachment)
|
|
26
|
+
File.write(path, 'tampered')
|
|
27
|
+
expect { described_class.report_payload(results_hash: { findings: rows }) }.to raise_error(IOError, /integrity/)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'packages durable evidence with normalized metadata for JSON and SARIF' do
|
|
32
|
+
require 'tmpdir'
|
|
33
|
+
require 'digest'
|
|
34
|
+
Dir.mktmpdir do |dir|
|
|
35
|
+
original = File.join(dir, 'original.pcap')
|
|
36
|
+
stored = File.join(dir, 'stored')
|
|
37
|
+
bytes = 'synthetic PCAP regression bytes'
|
|
38
|
+
File.binwrite(original, bytes)
|
|
39
|
+
File.binwrite(stored, bytes)
|
|
40
|
+
File.unlink(original)
|
|
41
|
+
artifact = { handle: 'artifact:test', kind: 'pcap', label: 'capture', path: original,
|
|
42
|
+
stored: stored, sha256: Digest::SHA256.hexdigest(bytes), size: bytes.bytesize }
|
|
43
|
+
finding = { id: 'one', reproduction_steps: ['Run fixture'], severity_justification: 'Observed impact', evidence_artifacts: [artifact] }
|
|
44
|
+
[PWN::Reports::JSON, PWN::Reports::SARIF].each do |writer|
|
|
45
|
+
out = writer.generate(path: File.join(dir, "#{writer.name.split('::').last}.json"), results_hash: { findings: [finding] })
|
|
46
|
+
doc = JSON.parse(File.read(out))
|
|
47
|
+
row = doc['findings'] ? doc['findings'].first : doc['runs'].first['results'].first['properties']
|
|
48
|
+
exported = row.fetch('evidence_artifacts').first
|
|
49
|
+
expect(exported).to include(artifact.transform_keys(&:to_s))
|
|
50
|
+
expect(exported.fetch('attachment')).to match(%r{\Aattachments/[a-f0-9]{64}\.pcap\z})
|
|
51
|
+
expect(Digest::SHA256.file(File.join(dir, exported['attachment'])).hexdigest).to eq(artifact[:sha256])
|
|
52
|
+
expect(row['reproduction_steps']).to eq(['Run fixture'])
|
|
53
|
+
end
|
|
54
|
+
expect(artifact).not_to have_key(:attachment)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'preserves direction and branch-specific impact without promoting unrelated paths' do
|
|
59
|
+
Dir.mktmpdir do |dir|
|
|
60
|
+
path = File.join(dir, 'combined.txt')
|
|
61
|
+
File.write(path, 'Only the source -> admin -> impact branch achieves full control in this synthetic fixture.')
|
|
62
|
+
evidence = { stored: path, sha256: Digest::SHA256.file(path).hexdigest, size: File.size(path) }
|
|
63
|
+
rows = [{ id: 'source', severity: 'medium', enables: %w[admin other] },
|
|
64
|
+
{ id: 'admin', severity: 'medium', enables: ['impact'] },
|
|
65
|
+
{ id: 'other', severity: 'medium', enables: ['impact'] },
|
|
66
|
+
{ id: 'impact', severity: 'medium', combined_severity: 'critical', chain_assessments: [
|
|
67
|
+
{ finding_ids: %w[source admin impact], combined_severity: 'critical', rationale: 'Full control only through admin.', evidence_artifacts: [evidence] }
|
|
68
|
+
] }]
|
|
69
|
+
chains = described_class.attack_chains(findings: rows.reverse)
|
|
70
|
+
expect(chains.map { |chain| chain[:finding_ids] }).to eq([%w[source admin impact], %w[source other impact]])
|
|
71
|
+
expect(chains.map { |chain| chain[:combined_severity] }).to eq(%w[critical medium])
|
|
72
|
+
expect(chains.last[:assessment_status]).to eq('unassessed')
|
|
73
|
+
rows.last[:enables] = ['source']
|
|
74
|
+
expect { described_class.attack_chains(findings: rows) }.to raise_error(ArgumentError, /cycle/)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'ignores cross-engagement edges and never upgrades links from titles or unscoped scores' do
|
|
79
|
+
rows = [{ id: 'ssrf', title: 'SSRF', severity: 'medium', enables: ['admin'], engagement_id: 'alpha' },
|
|
80
|
+
{ id: 'admin', title: 'Internal admin takeover', severity: 'medium', combined_severity: 'critical', engagement_id: 'beta' }]
|
|
81
|
+
expect(described_class.attack_chains(findings: rows)).to eq([])
|
|
82
|
+
rows.last[:engagement_id] = 'alpha'
|
|
83
|
+
expect(described_class.attack_chains(findings: rows).first).to include(combined_severity: 'medium', assessment_status: 'unassessed')
|
|
84
|
+
rows.last[:enables] = ['admin']
|
|
85
|
+
expect { described_class.attack_chains(findings: rows) }.to raise_error(ArgumentError, /cycle/)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'does not count isolated findings against the chain path limit' do
|
|
89
|
+
rows = Array.new(1002) { |index| { id: "isolated-#{index}", severity: 'medium' } }
|
|
90
|
+
expect(described_class.attack_chains(findings: rows)).to eq([])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it 'renders per-finding reproducibility and escaped portable evidence in HTML and Markdown' do
|
|
94
|
+
Dir.mktmpdir do |dir|
|
|
95
|
+
fixtures = { 'screenshot' => "\x89PNG\r\n\x1a\nsynthetic PNG fixture".b,
|
|
96
|
+
'pcap' => 'synthetic pcap', 'crash' => 'synthetic crash', 'poc' => 'synthetic PoC' }
|
|
97
|
+
artifacts = fixtures.map do |kind, bytes|
|
|
98
|
+
path = File.join(dir, kind)
|
|
99
|
+
File.binwrite(path, bytes)
|
|
100
|
+
{ 'kind' => kind, label: '<script>fixture</script>', handle: "artifact:#{kind}",
|
|
101
|
+
path: path, stored: path, sha256: Digest::SHA256.hexdigest(bytes), size: bytes.bytesize }
|
|
102
|
+
end
|
|
103
|
+
rows = [{ id: 'first', title: '<script>title</script>', reproduction_steps: ['Execute <fixture>', 'Observe result'],
|
|
104
|
+
severity_justification: 'Measured <impact>', poc: "printf '<ok>'\n```\n<script>bad</script>", evidence_artifacts: artifacts },
|
|
105
|
+
{ id: 'second', title: 'Legacy finding' }]
|
|
106
|
+
html = File.read(PWN::Reports::HTML.generate(path: File.join(dir, 'report.html'), results_hash: { findings: rows }))
|
|
107
|
+
md = File.read(PWN::Reports::Markdown.generate(path: File.join(dir, 'report.md'), results_hash: { findings: rows }))
|
|
108
|
+
expect(html).to include('<ol><li>Execute <fixture></li><li>Observe result</li></ol>', '<h3>Severity justification</h3>', '<pre><code>', '<img src="attachments/')
|
|
109
|
+
expect(html).not_to include('<script>')
|
|
110
|
+
expect(md).to include('1. Execute <fixture>', '2. Observe result', '#### Severity justification', '````', '![')
|
|
111
|
+
expect(md).to include('<script>title</script>')
|
|
112
|
+
[html, md].each do |text|
|
|
113
|
+
artifacts.each do |artifact|
|
|
114
|
+
expect(text).to include(artifact[:sha256], artifact[:size].to_s, artifact[:handle])
|
|
115
|
+
end
|
|
116
|
+
expect(text).to include('Not supplied')
|
|
117
|
+
end
|
|
118
|
+
expect(html.split('<section>').last).not_to include('artifact:screenshot')
|
|
119
|
+
expect(md.split('### second').last).not_to include('artifact:screenshot')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'rejects missing or altered evidence and tampered export targets explicitly' do
|
|
124
|
+
Dir.mktmpdir do |dir|
|
|
125
|
+
source = File.join(dir, 'source')
|
|
126
|
+
stored = File.join(dir, 'durable')
|
|
127
|
+
bytes = 'synthetic crash fixture'
|
|
128
|
+
File.write(source, bytes)
|
|
129
|
+
File.write(stored, bytes)
|
|
130
|
+
artifact = { kind: 'crash', path: source, stored: stored, sha256: Digest::SHA256.hexdigest(bytes), size: bytes.bytesize }
|
|
131
|
+
opts = { path: File.join(dir, 'report.json'), results_hash: { findings: [{ evidence_artifacts: [artifact] }] } }
|
|
132
|
+
out = PWN::Reports::JSON.generate(opts)
|
|
133
|
+
target = File.join(dir, JSON.parse(File.read(out))['findings'].first['evidence_artifacts'].first['attachment'])
|
|
134
|
+
File.write(target, 'tampered')
|
|
135
|
+
expect { PWN::Reports::JSON.generate(opts) }.to raise_error(IOError, /attachment integrity mismatch/)
|
|
136
|
+
File.unlink(target)
|
|
137
|
+
File.write(stored, 'tampered')
|
|
138
|
+
expect { PWN::Reports::JSON.generate(opts) }.to raise_error(IOError, /integrity mismatch/)
|
|
139
|
+
File.unlink(stored)
|
|
140
|
+
expect { PWN::Reports::JSON.generate(opts) }.to raise_error(IOError, /missing/)
|
|
141
|
+
File.write(stored, bytes)
|
|
142
|
+
artifact[:size] += 1
|
|
143
|
+
expect { PWN::Reports::JSON.generate(opts) }.to raise_error(IOError, /integrity mismatch/)
|
|
144
|
+
artifact[:sha256] = 'abc'
|
|
145
|
+
expect { PWN::Reports::JSON.generate(opts) }.to raise_error(ArgumentError, /full SHA-256/)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'never inlines active screenshot content or trusts supplied attachment URLs' do
|
|
150
|
+
Dir.mktmpdir do |dir|
|
|
151
|
+
bytes = '<svg onload="alert(1)"></svg>'
|
|
152
|
+
source = File.join(dir, 'unsafe.png')
|
|
153
|
+
File.write(source, bytes)
|
|
154
|
+
artifact = { kind: 'screenshot', label: '"](<script>)', path: source, stored: source, sha256: Digest::SHA256.hexdigest(bytes),
|
|
155
|
+
size: bytes.bytesize, attachment: 'javascript:alert(1)', inline_image: true }
|
|
156
|
+
opts = { results_hash: { findings: [{ evidence_artifacts: [artifact] }] }, dir_path: dir, report_name: 'safe' }
|
|
157
|
+
html = File.read(PWN::Reports::HTML.generate(opts))
|
|
158
|
+
md = File.read(PWN::Reports::Markdown.generate(opts))
|
|
159
|
+
expect(html).not_to include('<img', 'javascript:', '<script>')
|
|
160
|
+
expect(md).not_to include('![', 'javascript:', '<script>')
|
|
161
|
+
expect(html).to include("attachments/#{artifact[:sha256]}.bin")
|
|
162
|
+
expect(md).to include("attachments/#{artifact[:sha256]}.bin")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'requires durable storage even when an original source still exists' do
|
|
167
|
+
Dir.mktmpdir do |dir|
|
|
168
|
+
source = File.join(dir, 'source')
|
|
169
|
+
File.write(source, 'fixture')
|
|
170
|
+
artifact = { path: source, sha256: Digest::SHA256.hexdigest('fixture'), size: 7 }
|
|
171
|
+
[nil, '', File.join(dir, 'absent')].each do |stored|
|
|
172
|
+
artifact[:stored] = stored
|
|
173
|
+
expect do
|
|
174
|
+
PWN::Reports::JSON.generate(path: File.join(dir, 'report.json'), results_hash: { findings: [{ evidence_artifacts: [artifact] }] })
|
|
175
|
+
end.to raise_error(IOError, /missing/)
|
|
176
|
+
end
|
|
177
|
+
artifact.delete(:stored)
|
|
178
|
+
expect do
|
|
179
|
+
PWN::Reports::JSON.generate(path: File.join(dir, 'report.json'), results_hash: { findings: [{ evidence_artifacts: [artifact] }] })
|
|
180
|
+
end.to raise_error(IOError, /missing/)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'bounds inline PoC text while exporting the complete non-executed script' do
|
|
185
|
+
Dir.mktmpdir do |dir|
|
|
186
|
+
code = "raise 'NEVER EXECUTE'\n#{'x' * 20_000}END_OF_SCRIPT"
|
|
187
|
+
finding = { id: 'poc', poc: code, severity_justification: 'fixture only' }
|
|
188
|
+
opts = { results_hash: { findings: [finding] }, dir_path: dir, report_name: 'bounded' }
|
|
189
|
+
[PWN::Reports::HTML, PWN::Reports::Markdown].each do |writer|
|
|
190
|
+
text = File.read(writer.generate(opts))
|
|
191
|
+
expect(text).to include('Preview truncated', Digest::SHA256.hexdigest(code))
|
|
192
|
+
expect(text).not_to include('END_OF_SCRIPT')
|
|
193
|
+
end
|
|
194
|
+
row = JSON.parse(File.read(PWN::Reports::JSON.generate(opts)))['findings'].first
|
|
195
|
+
expect(row['poc']).to eq(code)
|
|
196
|
+
attachment = row.fetch('poc_export')
|
|
197
|
+
expect(File.binread(File.join(dir, attachment.fetch('attachment')))).to eq(code)
|
|
198
|
+
expect(attachment).to include('sha256' => Digest::SHA256.hexdigest(code), 'size' => code.bytesize)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
6
202
|
it 'should return data for help method' do
|
|
7
203
|
help_response = PWN::Reports.help
|
|
8
204
|
expect(help_response).not_to be_nil
|
data/spec/spec_helper.rb
CHANGED
|
@@ -23,6 +23,9 @@ RSpec.configure do |config|
|
|
|
23
23
|
config.filter_run_excluding :rtl433_integration unless ENV['PWN_TEST_RTL433'] == '1'
|
|
24
24
|
config.filter_run_excluding :liquid_integration unless ENV['PWN_TEST_LIQUID'] == '1'
|
|
25
25
|
config.filter_run_excluding :radare2_integration unless ENV['PWN_TEST_RADARE2'] == '1'
|
|
26
|
+
config.filter_run_excluding :gdb_integration unless ENV['PWN_TEST_GDB'] == '1'
|
|
27
|
+
config.filter_run_excluding :keystone_integration unless ENV['PWN_TEST_KEYSTONE'] == '1'
|
|
28
|
+
config.filter_run_excluding :capstone_integration unless ENV['PWN_TEST_CAPSTONE'] == '1'
|
|
26
29
|
%w[fftw volk rtl_sdr adalm_pluto soapy_sdr hack_rf].each do |backend|
|
|
27
30
|
config.filter_run_excluding :"#{backend}_integration" unless ENV["PWN_TEST_#{backend.upcase}"] == '1'
|
|
28
31
|
end
|