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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/documentation/Agent-Tool-Registry.md +163 -6
  4. data/documentation/Diagrams.md +1 -1
  5. data/documentation/Home.md +1 -1
  6. data/documentation/How-PWN-Works.md +1 -1
  7. data/documentation/Plugin-Degradation.md +6 -2
  8. data/documentation/Recon-Findings-API.md +33 -1
  9. data/documentation/What-is-PWN.md +1 -1
  10. data/documentation/pwn-ai-Agent.md +1 -1
  11. data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
  12. data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
  13. data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
  14. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
  15. data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
  16. data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
  17. data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
  18. data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
  19. data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
  20. data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
  21. data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
  22. data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
  23. data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
  24. data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
  25. data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
  26. data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
  27. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
  28. data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
  29. data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
  30. data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
  31. data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
  32. data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +8 -2
  33. data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
  34. data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
  35. data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
  36. data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
  37. data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
  38. data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
  39. data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
  40. data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
  41. data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
  42. data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
  43. data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
  44. data/etc/default_skills/pwn/reports/SKILL.md +2 -0
  45. data/lib/pwn/ai/agent/confirmation.rb +163 -0
  46. data/lib/pwn/ai/agent/dispatch.rb +43 -5
  47. data/lib/pwn/ai/agent/loop.rb +45 -11
  48. data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
  49. data/lib/pwn/ai/agent/registry.rb +2 -1
  50. data/lib/pwn/ai/agent/result.rb +76 -6
  51. data/lib/pwn/ai/agent/task_dag.rb +220 -0
  52. data/lib/pwn/ai/agent/tool_guard.rb +166 -24
  53. data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
  54. data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
  55. data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
  56. data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
  57. data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
  58. data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
  59. data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
  60. data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
  61. data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
  62. data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
  63. data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
  64. data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
  65. data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
  66. data/lib/pwn/ai/agent/tools/shell.rb +22 -10
  67. data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
  68. data/lib/pwn/ai/agent.rb +3 -1
  69. data/lib/pwn/ai/cli.rb +26 -2
  70. data/lib/pwn/ai/context.rb +4 -2
  71. data/lib/pwn/ai/router.rb +196 -0
  72. data/lib/pwn/ai/tools/README.md +5 -0
  73. data/lib/pwn/ai.rb +1 -0
  74. data/lib/pwn/config.rb +10 -0
  75. data/lib/pwn/engagement.rb +45 -1
  76. data/lib/pwn/ffi/capstone.rb +120 -0
  77. data/lib/pwn/ffi/keystone.rb +107 -0
  78. data/lib/pwn/ffi.rb +2 -0
  79. data/lib/pwn/migrate.rb +5 -1
  80. data/lib/pwn/plugins/aflplusplus.rb +238 -13
  81. data/lib/pwn/plugins/ai_sandbox.rb +240 -0
  82. data/lib/pwn/plugins/artifact_registry.rb +188 -22
  83. data/lib/pwn/plugins/assembly.rb +73 -2
  84. data/lib/pwn/plugins/basic_auth.rb +17 -4
  85. data/lib/pwn/plugins/binary_parser.rb +439 -38
  86. data/lib/pwn/plugins/exploit_dev.rb +83 -14
  87. data/lib/pwn/plugins/findings.rb +208 -42
  88. data/lib/pwn/plugins/gdbmi.rb +265 -38
  89. data/lib/pwn/plugins/httpx.rb +93 -0
  90. data/lib/pwn/plugins/jobs.rb +260 -102
  91. data/lib/pwn/plugins/method_catalog.rb +328 -0
  92. data/lib/pwn/plugins/nmap_it.rb +157 -26
  93. data/lib/pwn/plugins/nuclei.rb +122 -29
  94. data/lib/pwn/plugins/preflight_checker.rb +3 -2
  95. data/lib/pwn/plugins/process_tube.rb +51 -2
  96. data/lib/pwn/plugins/radare2.rb +120 -26
  97. data/lib/pwn/plugins/recon.rb +33 -0
  98. data/lib/pwn/plugins/repl/mesh.rb +203 -13
  99. data/lib/pwn/plugins/rop.rb +121 -31
  100. data/lib/pwn/plugins/transparent_browser.rb +212 -14
  101. data/lib/pwn/plugins/vault.rb +182 -24
  102. data/lib/pwn/plugins.rb +4 -0
  103. data/lib/pwn/reports/engagement.rb +8 -2
  104. data/lib/pwn/reports/html.rb +28 -9
  105. data/lib/pwn/reports/json.rb +2 -1
  106. data/lib/pwn/reports/markdown.rb +49 -12
  107. data/lib/pwn/reports/sarif.rb +17 -3
  108. data/lib/pwn/reports.rb +184 -34
  109. data/lib/pwn/version.rb +1 -1
  110. data/spec/integration/reports_spec.rb +93 -0
  111. data/spec/integration/result_condition_spec.rb +11 -8
  112. data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
  113. data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
  114. data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
  115. data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
  116. data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
  117. data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
  118. data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
  119. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
  120. data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
  121. data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
  122. data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
  123. data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
  124. data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
  125. data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
  126. data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
  127. data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
  128. data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
  129. data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
  130. data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
  131. data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
  132. data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
  133. data/spec/lib/pwn/ai/cli_spec.rb +12 -1
  134. data/spec/lib/pwn/ai/router_spec.rb +57 -0
  135. data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
  136. data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
  137. data/spec/lib/pwn/migrate_spec.rb +30 -2
  138. data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
  139. data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
  140. data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
  141. data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
  142. data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
  143. data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
  144. data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
  145. data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
  146. data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
  147. data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
  148. data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
  149. data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
  150. data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
  151. data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
  152. data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
  153. data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
  154. data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
  155. data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
  156. data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
  157. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
  158. data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
  159. data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
  160. data/spec/lib/pwn/reports_spec.rb +196 -0
  161. data/spec/spec_helper.rb +3 -0
  162. data/third_party/pwn_rdoc.jsonl +230 -4
  163. 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 &lt;fixture&gt;</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 &lt;fixture&gt;', '2. Observe result', '#### Severity justification', '````', '![')
111
+ expect(md).to include('&lt;script&gt;title&lt;/script&gt;')
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