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
@@ -7,37 +7,57 @@ PWN::AI::Agent::Registry.register(
7
7
  toolset: 'pwn',
8
8
  schema: {
9
9
  name: 'exploitdev',
10
- description: 'Exploit-dev helpers: pattern_create, pattern_offset, rop_gadgets, checksec, packing.',
10
+ description: 'Exploit-dev: cyclic pattern generate/offset, ROP gadgets, assemble/disassemble, ELF GOT/PLT resolve, process/remote IO, ret2libc chain.',
11
11
  parameters: {
12
12
  type: 'object',
13
13
  properties: {
14
- action: { type: 'string', description: 'pattern_create|pattern_offset|rop_gadgets|p32|p64|checksec' },
14
+ action: { type: 'string', description: 'pattern_create|pattern_offset|rop_gadgets|assemble|disassemble|elf_resolve|p32|p64|io|ret2libc|checksec' },
15
15
  n: { type: 'integer' },
16
16
  length: { type: 'integer' },
17
17
  value: { type: 'string' },
18
18
  path: { type: 'string' },
19
- filter: { type: 'string' }
19
+ filter: { type: 'string' },
20
+ asm: { type: 'string' },
21
+ bytes: { type: 'string' },
22
+ arch: { type: 'string' },
23
+ offset: { type: 'integer' },
24
+ extra: { type: 'string' },
25
+ cmd: { type: 'string' },
26
+ host: { type: 'string' },
27
+ port: { type: 'integer' },
28
+ kind: { type: 'string' }
20
29
  },
21
30
  required: %w[action]
22
31
  }
23
32
  },
24
33
  handler: lambda { |args|
25
- act = (args[:action] || args['action']).to_s
34
+ args = args.transform_keys(&:to_sym)
35
+ act = args[:action].to_s
26
36
  case act
27
37
  when 'pattern_create'
28
- PWN::Plugins::ExploitDev.cyclic(length: args[:n] || args[:length] || args['n'] || 200)
38
+ PWN::Plugins::ExploitDev.cyclic(length: args[:n] || args[:length] || 200)
29
39
  when 'pattern_offset'
30
- PWN::Plugins::ExploitDev.cyclic_find(value: args[:value] || args['value'], length: args[:length] || args['length'] || 400)
40
+ PWN::Plugins::ExploitDev.cyclic_find(value: args[:value], length: args[:length] || 400)
31
41
  when 'rop_gadgets'
32
- rows = Array(PWN::Plugins::ExploitDev.gadgets(path: args[:path] || args['path']))
33
- filt = (args[:filter] || args['filter']).to_s
34
- filt.empty? ? rows : rows.select { |r| r.to_s.downcase.include?(filt.downcase) }
42
+ PWN::Plugins::ExploitDev.gadgets(path: args[:path], filter: args[:filter], backend: args[:backend] || 'scan')
43
+ when 'assemble'
44
+ PWN::Plugins::Assembly.assemble(asm: args[:asm] || args[:value], arch: args[:arch] || 'x86_64')
45
+ when 'disassemble'
46
+ raw = args[:bytes] || args[:value]
47
+ raw = [raw].pack('H*') if raw.to_s.match?(/\A[0-9a-fA-F]+\z/)
48
+ PWN::Plugins::Assembly.disassemble(bytes: raw, arch: args[:arch] || 'x86_64')
49
+ when 'elf_resolve'
50
+ PWN::Plugins::BinaryParser.elf_resolve(path: args[:path])
35
51
  when 'p32'
36
- PWN::Plugins::ExploitDev.p32(value: args[:value] || args['value'])
52
+ PWN::Plugins::ExploitDev.p32(value: args[:value])
37
53
  when 'p64'
38
- PWN::Plugins::ExploitDev.p64(value: args[:value] || args['value'])
54
+ PWN::Plugins::ExploitDev.p64(value: args[:value])
55
+ when 'io'
56
+ PWN::Plugins::ExploitDev.io(kind: args[:kind] || 'process', cmd: args[:cmd] || args[:path], host: args[:host], port: args[:port])
57
+ when 'ret2libc'
58
+ PWN::Plugins::ExploitDev.ret2libc(path: args[:path], offset: args[:offset], extra: args[:extra])
39
59
  when 'checksec'
40
- PWN::Plugins::GDB.mitigations(binary: args[:path] || args['path'])
60
+ PWN::Plugins::GDB.mitigations(binary: args[:path])
41
61
  else
42
62
  { error: "unknown action #{act}" }
43
63
  end
@@ -7,7 +7,7 @@ PWN::AI::Agent::Registry.register(
7
7
  toolset: 'pwn',
8
8
  schema: {
9
9
  name: 'finding_record',
10
- description: 'Record a validated finding with CVSS 3.x, evidence file paths and a required command/code PoC. File existence is not execution proof. Also supports query, chain and export.',
10
+ description: 'Record a finding with CVSS 3.x, severity justification, ordered reproduction steps, and evidence paths or artifact handles (pcap, screenshot, crash, PoC). Export embeds hash-verified evidence. Hashes are not execution proof.',
11
11
  parameters: {
12
12
  type: 'object',
13
13
  properties: {
@@ -17,7 +17,21 @@ PWN::AI::Agent::Registry.register(
17
17
  cvss_score: { type: 'number', minimum: 0, maximum: 10 },
18
18
  affected_asset: { type: 'string' },
19
19
  evidence_paths: { type: 'array', minItems: 1, items: { type: 'string' } },
20
+ artifact_handles: {
21
+ type: 'array', minItems: 1,
22
+ items: {
23
+ anyOf: [
24
+ { type: 'string', minLength: 1 },
25
+ { type: 'object', required: %w[handle], additionalProperties: false,
26
+ properties: { handle: { type: 'string', minLength: 1 }, kind: { type: 'string', enum: %w[pcap screenshot crash poc evidence] },
27
+ label: { type: 'string', minLength: 1 }, sha256: { type: 'string', pattern: '^[0-9a-f]{64}$' } } }
28
+ ]
29
+ }
30
+ },
31
+ severity_justification: { type: 'string', minLength: 1, description: 'Explain demonstrated impact and why it supports the chosen CVSS metrics; do not claim untested escalation.' },
32
+ reproduction_steps: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 }, description: 'Ordered setup, PoC commands and expected observations for independent reproduction.' },
20
33
  attack_chain_refs: { type: 'array', items: { type: 'string' } },
34
+ enables: { type: 'array', uniqueItems: true, items: { type: 'string', minLength: 1 }, description: 'Outgoing finding IDs this finding enables, within the same engagement. Use op=link to update an existing finding.' },
21
35
  remediation: { type: 'string' },
22
36
  confidence: { type: 'number', minimum: 0, maximum: 1 },
23
37
  dir_path: { type: 'string' },
@@ -28,7 +42,8 @@ PWN::AI::Agent::Registry.register(
28
42
  poc: { type: 'string' },
29
43
  poc_artifacts: { type: 'array', items: { type: 'string' } },
30
44
  session_id: { type: 'string' },
31
- op: { type: 'string', enum: %w[query export chain record verify retest chain_impact gaps] },
45
+ op: { type: 'string', enum: %w[query export chain record link verify retest chain_impact gaps] },
46
+ id: { type: 'string', minLength: 1 },
32
47
  parent_id: { type: 'string' },
33
48
  engagement_id: { type: 'string' },
34
49
  kind: { type: 'string', enum: %w[http script] },
@@ -36,21 +51,30 @@ PWN::AI::Agent::Registry.register(
36
51
  request_path: { type: 'string' },
37
52
  response_path: { type: 'string' },
38
53
  execution_log: { type: 'string' },
39
- ids: { type: 'array', items: { type: 'string' } },
54
+ ids: { type: 'array', uniqueItems: true, items: { type: 'string' }, description: 'Ordered source-to-impact finding IDs for chain_impact.' },
40
55
  combined_impact_path: { type: 'string' },
41
56
  escalate: { type: 'boolean' },
42
57
  combined_severity: { type: 'string' }
43
58
  },
44
59
  required: %w[],
45
60
  anyOf: [
46
- { properties: { op: { enum: %w[query export] } }, required: %w[op] },
47
- { required: %w[title cwe cvss_vector cvss_score affected_asset evidence_paths poc attack_chain_refs remediation confidence] }
61
+ { properties: { op: { enum: %w[query export gaps] } }, required: %w[op] },
62
+ { properties: { op: { enum: %w[verify retest] } }, required: %w[op id kind impact],
63
+ anyOf: [{ required: %w[request_path response_path] }, { required: %w[execution_log] }] },
64
+ { properties: { op: { enum: %w[chain_impact] } }, required: %w[op ids combined_impact_path] },
65
+ { properties: { op: { enum: %w[link] } }, required: %w[op id enables] },
66
+ { properties: { op: { enum: %w[record chain] } }, required: %w[title cwe cvss_vector cvss_score affected_asset poc attack_chain_refs remediation confidence severity_justification reproduction_steps],
67
+ anyOf: [{ required: %w[evidence_paths] }, { required: %w[artifact_handles] }] }
48
68
  ]
49
69
  }
50
70
  },
51
71
  handler: lambda { |args|
52
72
  args = args.transform_keys(&:to_sym)
53
73
  op = (args[:op] || 'record').to_s
74
+ if %w[record chain].include?(op)
75
+ raise ArgumentError, 'severity_justification must be a nonempty string' unless args[:severity_justification].is_a?(String) && !args[:severity_justification].strip.empty?
76
+ raise ArgumentError, 'reproduction_steps are required' unless args.key?(:reproduction_steps) && !args[:reproduction_steps].nil?
77
+ end
54
78
  case op
55
79
  when 'query'
56
80
  PWN::Plugins::Findings.query(args)
@@ -67,6 +91,8 @@ PWN::AI::Agent::Registry.register(
67
91
  PWN::Plugins::Findings.retest(args)
68
92
  when 'chain_impact'
69
93
  PWN::Plugins::Findings.chain_impact(args)
94
+ when 'link'
95
+ PWN::Plugins::Findings.link(args)
70
96
  when 'gaps'
71
97
  PWN::Plugins::Findings.issue_work_gaps(args)
72
98
  when 'record'
@@ -1,54 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pwn/ai/agent/registry'
4
+ require 'shellwords'
4
5
 
5
6
  PWN::AI::Agent::Registry.register(
6
7
  name: 'fuzz_campaign',
7
8
  toolset: 'pwn',
8
9
  schema: {
9
10
  name: 'fuzz_campaign',
10
- description: 'Start, status, stop, or triage an AFL++ campaign via background jobs.',
11
+ description: 'AFL++/libFuzzer campaign: corpus, dictionary from binary strings, durable Jobs, crash dedup by backtrace hash, auto PWN-RE-003 triage (GDBMI + binary_triage + exploitdev). Poll job_status; no manual glue.',
11
12
  parameters: {
12
13
  type: 'object',
13
14
  properties: {
14
15
  action: { type: 'string', description: 'start|status|stop|triage|minimize' },
16
+ engine: { type: 'string', description: 'aflplusplus (default) or libfuzzer' },
15
17
  in_dir: { type: 'string' },
16
18
  out_dir: { type: 'string' },
17
19
  target: { type: 'string' },
18
- id: { type: 'string' }
20
+ max_runtime: { type: 'integer', minimum: 0 },
21
+ session_id: { type: 'string' },
22
+ cwd: { type: 'string' },
23
+ env: { type: 'object', additionalProperties: { type: 'string' } },
24
+ idempotency_key: { type: 'string' },
25
+ id: { type: 'string' },
26
+ crash: { type: 'string' },
27
+ out: { type: 'string' }
19
28
  },
20
29
  required: %w[action]
21
30
  }
22
31
  },
23
32
  handler: lambda { |args|
24
- act = (args[:action] || args['action']).to_s
33
+ args = args.transform_keys(&:to_sym)
34
+ act = args[:action].to_s
25
35
  case act
26
36
  when 'start'
27
- in_dir = args[:in_dir] || args['in_dir']
28
- out_dir = args[:out_dir] || args['out_dir']
29
- target = args[:target] || args['target']
30
- cmd = "afl-fuzz -i #{in_dir} -o #{out_dir} -- #{target}"
31
- PWN::Plugins::Jobs.start(command: cmd)
37
+ in_dir = args[:in_dir]
38
+ out_dir = args[:out_dir]
39
+ target = args[:target]
40
+ raise ArgumentError, 'in_dir, out_dir and target are required' if [in_dir, out_dir, target].any? { |value| value.to_s.empty? }
41
+
42
+ env = args[:env]
43
+ cmd = PWN::Plugins::AFLplusplus.campaign_command(
44
+ engine: args[:engine] || 'aflplusplus',
45
+ in_dir: in_dir,
46
+ out_dir: out_dir,
47
+ target: target,
48
+ max_runtime: args[:max_runtime],
49
+ cwd: args[:cwd],
50
+ env: env&.transform_keys(&:to_s)
51
+ )
52
+ PWN::Plugins::Jobs.start(
53
+ command: cmd,
54
+ max_runtime: args[:max_runtime],
55
+ session_id: args[:session_id] || Thread.current[:pwn_session_id],
56
+ cwd: args[:cwd],
57
+ env: env&.transform_keys(&:to_s),
58
+ idempotency_key: args[:idempotency_key]
59
+ )
32
60
  when 'status'
33
- row = PWN::Plugins::Jobs.status(id: args[:id] || args['id'])
34
- out_dir = args[:out_dir] || args['out_dir']
61
+ row = PWN::Plugins::Jobs.status(id: args[:id])
62
+ out_dir = args[:out_dir]
35
63
  row = row.merge(PWN::Plugins::AFLplusplus.parse_stats(out_dir: out_dir)) if out_dir
36
- if out_dir && defined?(PWN::Plugins::AFLplusplus)
37
- begin
38
- triage = PWN::Plugins::AFLplusplus.crash_triage(out_dir: out_dir)
39
- row = row.merge(triage: triage) if triage
40
- rescue StandardError
41
- nil
42
- end
64
+ if out_dir
65
+ triage = PWN::Plugins::AFLplusplus.crash_triage(out_dir: out_dir, target: args[:target], handoff: true)
66
+ row = row.merge(triage: triage)
43
67
  end
44
68
  row
45
69
  when 'stop'
46
- PWN::Plugins::Jobs.stop(id: args[:id] || args['id'])
70
+ PWN::Plugins::Jobs.stop(id: args[:id])
47
71
  when 'triage'
48
- out_dir = args[:out_dir] || args['out_dir']
49
- PWN::Plugins::Fuzz.triage(dir: File.join(out_dir.to_s, 'crashes'), record: true, target: args[:target] || args['target'])
72
+ PWN::Plugins::AFLplusplus.crash_triage(out_dir: args[:out_dir], target: args[:target], handoff: true)
50
73
  when 'minimize'
51
- PWN::Plugins::AFLplusplus.minimize(crash: args[:crash] || args['crash'], out: args[:out] || args['out'], target: args[:target] || args['target'])
74
+ PWN::Plugins::AFLplusplus.minimize(crash: args[:crash], out: args[:out], target: args[:target])
52
75
  else
53
76
  { error: "unknown action #{act}" }
54
77
  end
@@ -1,31 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pwn/ai/agent/registry'
4
+ require 'shellwords'
5
+ require 'rbconfig'
4
6
 
5
7
  PWN::AI::Agent::Registry.register(
6
8
  name: 'job_run',
7
9
  toolset: 'terminal',
8
10
  schema: {
9
11
  name: 'job_run',
10
- description: 'Start a detached background job (nmap, afl, hashcat). Returns {id, pid, log}.',
12
+ description: 'Start durable detached work: fuzzers, full scans, Ghidra analysis or a job graph. Returns a job id immediately; survives agent/session exit. Poll job_status and incrementally read job_tail using next_offset. max_runtime is the supervisor lifetime, NOT a blocking tool timeout; 21600 allows six hours. Reuse idempotency_key on launch retries, never relaunch to poll.',
11
13
  parameters: {
12
14
  type: 'object',
13
15
  properties: {
14
16
  command: { type: 'string' },
15
17
  cmd: { type: 'string' },
16
18
  tag: { type: 'string' },
19
+ session_id: { type: 'string' },
20
+ max_runtime: { type: 'integer', minimum: 0, description: 'Independent wall runtime in seconds; 0 means no runtime limit.' },
21
+ cwd: { type: 'string' },
22
+ env: { type: 'object', additionalProperties: { type: 'string' } },
23
+ idempotency_key: { type: 'string', description: 'Stable launch key reused across retries/sessions to recover the same job.' },
17
24
  jobs: { type: 'array' },
18
25
  artifact_dir: { type: 'string' }
19
26
  },
20
- required: %w[]
27
+ anyOf: [{ required: %w[command] }, { required: %w[cmd] }, { required: %w[jobs] }]
21
28
  }
22
29
  },
23
30
  handler: lambda { |args|
24
- if args[:jobs] || args['jobs']
25
- PWN::Plugins::Jobs.graph(jobs: args[:jobs] || args['jobs'], artifact_dir: args[:artifact_dir] || args['artifact_dir'])
26
- else
27
- PWN::Plugins::Jobs.start(command: args[:command] || args[:cmd] || args['command'], session_id: args[:tag] || args['tag'])
31
+ args = args.transform_keys(&:to_sym)
32
+ command = args[:command] || args[:cmd]
33
+ if args[:jobs]
34
+ payload = JSON.generate(jobs: args[:jobs], artifact_dir: args[:artifact_dir])
35
+ script = 'r=PWN::Plugins::Jobs.graph(JSON.parse(ARGV.fetch(0), symbolize_names:true)); puts JSON.generate(r); exit(r[:ok] ? 0 : 1)'
36
+ command = Shellwords.join([RbConfig.ruby, '-I', File.expand_path('../../../..', __dir__), '-rpwn', '-rjson', '-e', script, payload])
28
37
  end
38
+ PWN::Plugins::Jobs.start(command: command, session_id: args[:session_id] || args[:tag] || Thread.current[:pwn_session_id], max_runtime: args[:max_runtime], cwd: args[:cwd], env: args[:env]&.transform_keys(&:to_s), idempotency_key: args[:idempotency_key])
29
39
  }
30
40
  )
31
41
  PWN::AI::Agent::Registry.register(
@@ -33,15 +43,15 @@ PWN::AI::Agent::Registry.register(
33
43
  toolset: 'terminal',
34
44
  schema: {
35
45
  name: 'job_status',
36
- description: 'Poll a job started by job_run.',
46
+ description: 'Poll durable job status by id, including exit/signal. Omit id to rediscover recent jobs from prior sessions. RUNNING is not completion; poll again instead of rerunning job_run.',
37
47
  parameters: {
38
48
  type: 'object',
39
- properties: { id: { type: 'string' } },
40
- required: %w[id]
49
+ properties: { id: { type: 'string' } }
41
50
  }
42
51
  },
43
52
  handler: lambda { |args|
44
- PWN::Plugins::Jobs.status(id: args[:id] || args['id'])
53
+ id = args[:id] || args['id']
54
+ id ? PWN::Plugins::Jobs.status(id: id) : PWN::Plugins::Jobs.list(limit: 20)
45
55
  }
46
56
  )
47
57
  PWN::AI::Agent::Registry.register(
@@ -49,7 +59,7 @@ PWN::AI::Agent::Registry.register(
49
59
  toolset: 'terminal',
50
60
  schema: {
51
61
  name: 'job_result',
52
- description: 'Fetch log tail and paths for a job_run id.',
62
+ description: 'Fetch durable status and a bounded log tail for a job id. Use job_tail for incremental byte-cursor reads.',
53
63
  parameters: {
54
64
  type: 'object',
55
65
  properties: { id: { type: 'string' }, lines: { type: 'integer' } },
@@ -65,15 +75,17 @@ PWN::AI::Agent::Registry.register(
65
75
  toolset: 'terminal',
66
76
  schema: {
67
77
  name: 'job_tail',
68
- description: 'Tail a job log started by job_run.',
78
+ description: 'Read a bounded incremental job log page. Start offset=0, then use next_offset. eof means current log end, not job completion: also poll job_status. Preserve encoding when decoding binary logs.',
69
79
  parameters: {
70
80
  type: 'object',
71
- properties: { id: { type: 'string' }, handle: { type: 'string' }, lines: { type: 'integer' } },
72
- required: %w[id]
81
+ properties: { id: { type: 'string' }, handle: { type: 'string' }, offset: { type: 'integer', minimum: 0 }, length: { type: 'integer', minimum: 1 }, lines: { type: 'integer' } },
82
+ anyOf: [{ required: %w[id] }, { required: %w[handle] }]
73
83
  }
74
84
  },
75
85
  handler: lambda { |args|
76
- PWN::Plugins::Jobs.job_tail(id: args[:id] || args[:handle] || args['id'], lines: args[:lines] || args['lines'])
86
+ args = args.transform_keys(&:to_sym)
87
+ length = (args[:length] || PWN::AI::Agent::Result.page_length).to_i.clamp(1, PWN::AI::Agent::Result.page_length)
88
+ PWN::Plugins::Jobs.tail(id: args[:id] || args[:handle], offset: args[:offset] || 0, length: length)
77
89
  }
78
90
  )
79
91
  PWN::AI::Agent::Registry.register(
@@ -81,14 +93,14 @@ PWN::AI::Agent::Registry.register(
81
93
  toolset: 'terminal',
82
94
  schema: {
83
95
  name: 'job_kill',
84
- description: 'Stop a job started by job_run.',
96
+ description: 'Request supervised cancellation of a job and its process group. Poll job_status for terminal STOPPED state.',
85
97
  parameters: {
86
98
  type: 'object',
87
99
  properties: { id: { type: 'string' }, handle: { type: 'string' } },
88
- required: %w[id]
100
+ anyOf: [{ required: %w[id] }, { required: %w[handle] }]
89
101
  }
90
102
  },
91
103
  handler: lambda { |args|
92
- PWN::Plugins::Jobs.job_kill(id: args[:id] || args[:handle] || args['id'])
104
+ PWN::Plugins::Jobs.job_kill(id: args[:id] || args[:handle] || args['id'] || args['handle'])
93
105
  }
94
106
  )
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'loot_query',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'loot_query',
10
+ description: 'Query the engagement-scoped encrypted loot store (creds/tokens/keys with provenance). Use for lateral-movement planning; BasicAuth.encode(host:) auto-offers matching secrets when a service prompts for auth.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ host: { type: 'string' },
15
+ service: { type: 'string' },
16
+ kind: { type: 'string' },
17
+ engagement: { type: 'string' },
18
+ engagement_id: { type: 'string' },
19
+ finding_id: { type: 'string' },
20
+ op: { type: 'string', enum: %w[query offer ingest store] },
21
+ text: { type: 'string' },
22
+ secret: { type: 'string' },
23
+ username: { type: 'string' },
24
+ where: { type: 'string' },
25
+ source: { type: 'string' },
26
+ label: { type: 'string' }
27
+ }
28
+ }
29
+ },
30
+ handler: lambda { |args|
31
+ args = args.transform_keys(&:to_sym)
32
+ op = (args[:op] || 'query').to_s
33
+ case op
34
+ when 'offer'
35
+ PWN::Plugins::Vault.offer(args)
36
+ when 'ingest'
37
+ PWN::Plugins::Vault.ingest(args.merge(source: args[:source] || 'recon'))
38
+ when 'store'
39
+ PWN::Plugins::Vault.store(args)
40
+ else
41
+ { records: PWN::Plugins::Vault.query(args) }
42
+ end
43
+ }
44
+ )
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'nmap_scan',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'nmap_scan',
10
+ description: 'Ingest nmap XML into the engagement store (hosts, services, scripts) and return diff vs last scan. since=yesterday answers what changed since yesterday in one call.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ xml: { type: 'string', description: 'Path to nmap XML (ingest).' },
15
+ xml_file: { type: 'string' },
16
+ since: { type: 'string', description: 'yesterday | last-scan | ISO8601. Omit xml to only query the diff.' },
17
+ engagement: { type: 'string' },
18
+ override: { type: 'boolean' },
19
+ at: { type: 'string' }
20
+ }
21
+ }
22
+ },
23
+ handler: lambda { |args|
24
+ args = args.transform_keys(&:to_sym)
25
+ xml = args[:xml] || args[:xml_file]
26
+ if xml.to_s.empty?
27
+ PWN::Plugins::NmapIt.changes(since: args[:since] || 'yesterday', engagement: args[:engagement])
28
+ else
29
+ PWN::Plugins::NmapIt.scan(xml: xml, engagement: args[:engagement], override: args[:override], at: args[:at])
30
+ end
31
+ }
32
+ )
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'nuclei_scan',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'nuclei_scan',
10
+ description: 'Run or ingest nuclei JSONL (optionally after httpx tech-detect). Records web findings with matched-at URL, template id, and severity into the findings store.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ jsonl: { type: 'string', description: 'nuclei JSONL path or raw JSONL (skips the nuclei binary).' },
15
+ target: { type: 'string' },
16
+ url: { type: 'string' },
17
+ httpx_jsonl: { type: 'string', description: 'httpx JSONL used to select nuclei tags by tech stack.' },
18
+ techs: { type: 'array', items: { type: 'string' } },
19
+ record: { type: 'boolean' },
20
+ templates: { type: 'string' },
21
+ severity: { type: 'string' }
22
+ }
23
+ }
24
+ },
25
+ handler: lambda { |args|
26
+ args = args.transform_keys(&:to_sym)
27
+ techs = Array(args[:techs])
28
+ techs = PWN::Plugins::Httpx.probe(jsonl: args[:httpx_jsonl])[:techs] if techs.empty? && !args[:httpx_jsonl].to_s.empty?
29
+ PWN::Plugins::Nuclei.scan(args.merge(techs: techs, record: args.fetch(:record, true)))
30
+ }
31
+ )
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'r2_functions',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'r2_functions',
10
+ description: 'Open a binary in r2/rizin (r2pipe) and return parsed aflj function JSON. Pair with r2_disasm for main.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ bin: { type: 'string', description: 'Filesystem path of the binary.' },
15
+ path: { type: 'string', description: 'Alias for bin.' },
16
+ session: { type: 'string', description: 'Existing r2pipe session id from a prior r2_* call.' },
17
+ backend: { type: 'string', enum: %w[r2 rizin] }
18
+ },
19
+ anyOf: [{ required: %w[bin] }, { required: %w[path] }, { required: %w[session] }]
20
+ }
21
+ },
22
+ handler: lambda { |args|
23
+ args = args.transform_keys(&:to_sym)
24
+ session = args[:session] || PWN::Plugins::Radare2.open(args)
25
+ { session: session, command: 'aflj', functions: PWN::Plugins::Radare2.functions(session: session) }
26
+ }
27
+ )
28
+ PWN::AI::Agent::Registry.register(
29
+ name: 'r2_disasm',
30
+ toolset: 'pwn',
31
+ schema: {
32
+ name: 'r2_disasm',
33
+ description: 'Return parsed pdfj/pdj disassembly JSON for an address or function (default main) from an r2pipe session.',
34
+ parameters: {
35
+ type: 'object',
36
+ properties: {
37
+ bin: { type: 'string' },
38
+ path: { type: 'string' },
39
+ session: { type: 'string' },
40
+ addr: { type: 'string', description: 'Function, symbol, or 0x address. Defaults to main.' },
41
+ n: { type: 'integer', minimum: 1, maximum: 4096, description: 'If set, use pdj for n instructions; otherwise pdfj.' },
42
+ backend: { type: 'string', enum: %w[r2 rizin] }
43
+ },
44
+ anyOf: [{ required: %w[bin] }, { required: %w[path] }, { required: %w[session] }]
45
+ }
46
+ },
47
+ handler: lambda { |args|
48
+ args = args.transform_keys(&:to_sym)
49
+ session = args[:session] || PWN::Plugins::Radare2.open(args)
50
+ addr = args[:addr]
51
+ addr = 'main' if addr.nil? || addr.to_s.empty?
52
+ { session: session, command: args[:n] ? 'pdj' : 'pdfj', addr: addr,
53
+ disasm: PWN::Plugins::Radare2.disasm(args.merge(session: session, addr: addr)) }
54
+ }
55
+ )
@@ -35,6 +35,7 @@ PWN::AI::Agent::Registry.register(
35
35
  code: { type: 'string', description: 'Ruby source to evaluate.' },
36
36
  encoding: { type: 'string', description: 'Set to base64 when data holds the Ruby source.' },
37
37
  data: { type: 'string', description: 'Base64 Ruby source when encoding is base64.' },
38
+ placeholder_ok: { type: 'boolean', description: 'Explicitly permit placeholder tokens; Ruby must still parse and execute. Literal strings/heredocs never require this.' },
38
39
  timeout: {
39
40
  type: 'integer',
40
41
  description: 'Conservative seconds this eval should take given HOST LOAD. Omit for a host-derived default. Explicit values honored 1..10800 (3 hours). On timeout keep the same payload and timeout += 180; rewrite only after the 3-hour budget (max 10 mutations/task).'
@@ -58,11 +59,27 @@ PWN::AI::Agent::Registry.register(
58
59
  code = args[:code].to_s
59
60
  return PWN::AI::Agent::ToolGuard.invalid_payload(hint: 'code must be a nonempty string') unless args[:code].is_a?(String) && !code.empty?
60
61
 
62
+ placeholder = PWN::AI::Agent::ToolGuard.placeholder_match(text: code, language: :ruby, placeholder_ok: args[:placeholder_ok])
63
+ if placeholder
64
+ return PWN::AI::Agent::ToolGuard.invalid_payload(
65
+ **placeholder,
66
+ text: code,
67
+ hint: 'Ruby source contains an elided code stub outside literal data.',
68
+ remedy: 'Replace the matched stub with complete Ruby code. Strings and heredocs may contain ellipses without placeholder_ok. Explicit placeholder_ok: true skips this guard but not Ruby parsing; base64 data is decoded before validation.'
69
+ )
70
+ end
71
+
72
+ timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :eval, payload: code)
73
+ PWN::AI::Agent::ToolGuard.protect_http!
74
+ sandboxed = PWN::Plugins::AISandbox.wrap_ruby(code: code, timeout: timeout)
75
+ return sandboxed unless sandboxed.nil?
76
+
77
+ blocked = PWN::Plugins::MethodCatalog.guard_eval(code: code)
78
+ return blocked if blocked
79
+
61
80
  old_stdout = $stdout
62
81
  buf = StringIO.new
63
82
  $stdout = buf
64
- timeout = PWN::AI::Agent::ToolGuard.deadline_s(timeout: args[:timeout], kind: :eval, payload: code)
65
- PWN::AI::Agent::ToolGuard.protect_http!
66
83
  begin
67
84
  # rubocop:disable Security/Eval
68
85
  # INTENTIONAL: this IS the pwn-ai → PWN bridge