pwn 0.5.712 → 0.5.714

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +3 -0
  3. data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +1 -0
  4. data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
  5. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +16 -0
  6. data/etc/default_skills/pwn/ai/agent/tools/artifacts/SKILL.md +45 -0
  7. data/etc/default_skills/pwn/ai/agent/tools/binary_triage/SKILL.md +45 -0
  8. data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
  9. data/etc/default_skills/pwn/ai/agent/tools/exploitdev/SKILL.md +45 -0
  10. data/etc/default_skills/pwn/ai/agent/tools/finding_record/SKILL.md +45 -0
  11. data/etc/default_skills/pwn/ai/agent/tools/fuzz_campaign/SKILL.md +45 -0
  12. data/etc/default_skills/pwn/ai/agent/tools/intel_lookup/SKILL.md +45 -0
  13. data/etc/default_skills/pwn/ai/agent/tools/job_run/SKILL.md +45 -0
  14. data/etc/default_skills/pwn/ai/agent/tools/pty_session/SKILL.md +45 -0
  15. data/etc/default_skills/pwn/ai/agent/tools/session_export/SKILL.md +45 -0
  16. data/etc/default_skills/pwn/ai/agent/tools/swarm_map/SKILL.md +45 -0
  17. data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
  18. data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
  19. data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +2 -0
  20. data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
  21. data/etc/default_skills/pwn/plugins/detect_os/SKILL.md +1 -0
  22. data/etc/default_skills/pwn/plugins/findings/SKILL.md +50 -0
  23. data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
  24. data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
  25. data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
  26. data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
  27. data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +2 -0
  28. data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +2 -0
  29. data/etc/default_skills/pwn/sessions/SKILL.md +3 -0
  30. data/etc/default_skills/pwn/setup/references/urls.md +1 -0
  31. data/etc/intel/files_exploits.csv +2 -0
  32. data/lib/pwn/ai/agent/dispatch.rb +5 -0
  33. data/lib/pwn/ai/agent/learning.rb +72 -0
  34. data/lib/pwn/ai/agent/loop.rb +62 -13
  35. data/lib/pwn/ai/agent/mistakes.rb +21 -1
  36. data/lib/pwn/ai/agent/prompt_builder.rb +16 -13
  37. data/lib/pwn/ai/agent/registry.rb +21 -1
  38. data/lib/pwn/ai/agent/reward.rb +4 -2
  39. data/lib/pwn/ai/agent/swarm.rb +26 -0
  40. data/lib/pwn/ai/agent/tool_guard.rb +201 -1
  41. data/lib/pwn/ai/agent/tools/artifacts.rb +61 -0
  42. data/lib/pwn/ai/agent/tools/binary_triage.rb +20 -0
  43. data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
  44. data/lib/pwn/ai/agent/tools/exploitdev.rb +45 -0
  45. data/lib/pwn/ai/agent/tools/finding_record.rb +48 -0
  46. data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -0
  47. data/lib/pwn/ai/agent/tools/intel_lookup.rb +24 -0
  48. data/lib/pwn/ai/agent/tools/job_run.rb +56 -0
  49. data/lib/pwn/ai/agent/tools/pty_session.rb +73 -0
  50. data/lib/pwn/ai/agent/tools/session_export.rb +20 -0
  51. data/lib/pwn/ai/agent/tools/shell.rb +5 -0
  52. data/lib/pwn/ai/agent/tools/swarm_map.rb +26 -0
  53. data/lib/pwn/config.rb +10 -1
  54. data/lib/pwn/memory_index.rb +50 -0
  55. data/lib/pwn/plugins/aflplusplus.rb +30 -1
  56. data/lib/pwn/plugins/artifact_registry.rb +43 -0
  57. data/lib/pwn/plugins/binary_parser.rb +31 -0
  58. data/lib/pwn/plugins/detect_os.rb +109 -2
  59. data/lib/pwn/plugins/exploit_db.rb +38 -1
  60. data/lib/pwn/plugins/findings.rb +105 -0
  61. data/lib/pwn/plugins/ghidra.rb +75 -0
  62. data/lib/pwn/plugins/jobs.rb +12 -1
  63. data/lib/pwn/plugins/log.rb +18 -3
  64. data/lib/pwn/plugins/nmap_it.rb +29 -0
  65. data/lib/pwn/plugins/preflight_checker.rb +36 -0
  66. data/lib/pwn/plugins/transparent_browser.rb +29 -0
  67. data/lib/pwn/plugins.rb +2 -0
  68. data/lib/pwn/sessions.rb +92 -1
  69. data/lib/pwn/setup.rb +41 -15
  70. data/lib/pwn/version.rb +1 -1
  71. data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +7 -0
  72. data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
  73. data/spec/lib/pwn/ai/agent/loop_spec.rb +57 -2
  74. data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
  75. data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
  76. data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
  77. data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
  78. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
  79. data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +16 -0
  80. data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +14 -0
  81. data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
  82. data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +14 -0
  83. data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +15 -0
  84. data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +14 -0
  85. data/spec/lib/pwn/ai/agent/tools/intel_lookup_spec.rb +14 -0
  86. data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +16 -0
  87. data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +17 -0
  88. data/spec/lib/pwn/ai/agent/tools/session_export_spec.rb +14 -0
  89. data/spec/lib/pwn/ai/agent/tools/swarm_map_spec.rb +14 -0
  90. data/spec/lib/pwn/cron_spec.rb +2 -2
  91. data/spec/lib/pwn/memory_index_spec.rb +9 -0
  92. data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
  93. data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
  94. data/spec/lib/pwn/plugins/detect_os_spec.rb +40 -0
  95. data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
  96. data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
  97. data/spec/lib/pwn/plugins/findings_spec.rb +30 -0
  98. data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
  99. data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
  100. data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
  101. data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
  102. data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
  103. data/spec/lib/pwn/plugins/log_spec.rb +10 -5
  104. data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
  105. data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
  106. data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
  107. data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
  108. data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
  109. data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
  110. data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
  111. data/spec/lib/pwn/sessions_spec.rb +53 -0
  112. data/spec/lib/pwn/setup_spec.rb +32 -0
  113. data/third_party/pwn_rdoc.jsonl +64 -2
  114. metadata +42 -3
  115. data/spec/lib/pwn/req_queue_spec.rb +0 -168
  116. data/tmp_pwn_critic_skills_flaw.txt +0 -9
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'digest'
4
+ require 'json'
5
+ require 'fileutils'
6
+ require 'securerandom'
4
7
 
5
8
  module PWN
6
9
  module AI
@@ -43,11 +46,50 @@ module PWN
43
46
  end
44
47
 
45
48
  public_class_method def self.bashism?(opts = {})
46
- BASHISM_RX.match?(opts[:text].to_s)
49
+ BASHISM_RX.match?(shell_syntax_surface(text: opts[:text]))
47
50
  rescue StandardError
48
51
  false
49
52
  end
50
53
 
54
+ public_class_method def self.shell_syntax_surface(opts = {})
55
+ s = opts[:text].to_s.dup
56
+ s.gsub!(/<<[-~]?\s*(['"])(\w+)\1.*?^\2\s*$/m, ' ')
57
+ s.gsub!(/'[^']*'/, "''")
58
+ s
59
+ end
60
+
61
+ public_class_method def self.mint_canary(opts = {})
62
+ n = (opts[:bytes] || 8).to_i
63
+ n = 8 if n <= 0
64
+ tok = "PWNCANARY#{SecureRandom.hex(n)}"
65
+ Thread.current[:pwn_canary] = tok
66
+ tok
67
+ end
68
+
69
+ public_class_method def self.canary_leak?(opts = {})
70
+ tok = Thread.current[:pwn_canary].to_s
71
+ return false if tok.empty?
72
+
73
+ opts[:text].to_s.include?(tok)
74
+ end
75
+
76
+ public_class_method def self.injection_score(opts = {})
77
+ t = opts[:text].to_s
78
+ score = 0
79
+ score += 3 if t.match?(/ignore (all )?(previous|prior) (instructions|directives)/i)
80
+ score += 2 if t.match?(/system:\s|tool_call|function_call/i)
81
+ score += 2 if t.match?(/\bexfiltrat|\bdo not tell (the )?(user|operator)/i)
82
+ score
83
+ end
84
+
85
+ public_class_method def self.quarantine_output(opts = {})
86
+ text = opts[:text].to_s
87
+ score = injection_score(text: text)
88
+ return text unless score >= 3
89
+
90
+ "[QUARANTINED injection_score=#{score}]\n#{text}"
91
+ end
92
+
51
93
  public_class_method def self.shell_bash?
52
94
  v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
53
95
  v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
@@ -63,6 +105,7 @@ module PWN
63
105
  # assign HTTP = "/path/http" or Digest = "(self.we" and then every
64
106
  # provider hop / payload_sig TypeErrors.
65
107
  CORE_CONSTS = %i[HTTP Digest JSON URI Timeout].freeze
108
+ RUNTIMES_FILE = File.join(Dir.home, '.pwn', 'metrics', 'runtimes.json')
66
109
 
67
110
  public_class_method def self.protect_http!
68
111
  protect_core_constants!
@@ -165,6 +208,9 @@ module PWN
165
208
  asked_i = asked.to_i
166
209
  return asked_i.clamp(1, TIMEOUT_MAX_S) if asked_i.positive?
167
210
 
211
+ learned = predicted_timeout(command_class: command_class(payload: opts[:payload].to_s))
212
+ return learned.clamp(1, TIMEOUT_MAX_S) if learned.to_i.positive?
213
+
168
214
  snap = host_load
169
215
  ncpu = [snap[:ncpu].to_i, 1].max
170
216
  load1 = snap[:load1].to_f
@@ -352,6 +398,96 @@ module PWN
352
398
  false
353
399
  end
354
400
 
401
+ public_class_method def self.scope_refusal(opts = {})
402
+ cmd = opts[:command].to_s
403
+ path = File.join(Dir.home, '.pwn', 'scope.yaml')
404
+ return nil unless File.file?(path)
405
+
406
+ require 'yaml'
407
+ scope = YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
408
+ return nil if scope.nil? || scope.empty?
409
+
410
+ allow = Array(scope['cidr_allowlist'] || scope[:cidr_allowlist]).map(&:to_s)
411
+ domains = Array(scope['domain_allowlist'] || scope[:domain_allowlist]).map(&:to_s)
412
+ return nil if allow.empty? && domains.empty?
413
+
414
+ ips = cmd.scan(/\b\d{1,3}(?:\.\d{1,3}){3}\b/)
415
+ hosts = cmd.scan(/\b[a-z0-9][a-z0-9.-]+\.[a-z]{2,}\b/i)
416
+ bad_ip = allow.any? ? ips.find { |ip| !rfc1918?(ip: ip) && allow.none? { |cidr| ip_in_cidr?(ip: ip, cidr: cidr) } } : nil
417
+ bad_host = domains.any? ? hosts.find { |h| domains.none? { |d| h.downcase.end_with?(d.downcase) } } : nil
418
+ hit = bad_ip || bad_host
419
+ return nil unless hit
420
+
421
+ "Refused: #{hit} is outside ~/.pwn/scope.yaml allowlists."
422
+ rescue StandardError
423
+ nil
424
+ end
425
+
426
+ public_class_method def self.rfc1918?(opts = {})
427
+ oct = opts[:ip].to_s.split('.').map(&:to_i)
428
+ return false unless oct.length == 4
429
+
430
+ return true if [10, 127].include?(oct[0])
431
+ return true if oct[0] == 192 && oct[1] == 168
432
+ return true if oct[0] == 172 && oct[1].between?(16, 31)
433
+
434
+ false
435
+ end
436
+
437
+ public_class_method def self.ip_in_cidr?(opts = {})
438
+ ip = opts[:ip].to_s.split('.').map(&:to_i)
439
+ cidr, bits = opts[:cidr].to_s.split('/')
440
+ return false if ip.length != 4 || cidr.to_s.split('.').length != 4
441
+
442
+ mask = bits.to_i
443
+ mask = 32 if mask <= 0
444
+ a = ip.inject(0) { |acc, oct| (acc << 8) + oct }
445
+ b = cidr.split('.').map(&:to_i).inject(0) { |acc, oct| (acc << 8) + oct }
446
+ shift = 32 - mask
447
+ (a >> shift) == (b >> shift)
448
+ end
449
+
450
+ public_class_method def self.command_class(opts = {})
451
+ cmd = opts[:payload].to_s.split.first.to_s
452
+ cmd.empty? ? 'shell' : File.basename(cmd)
453
+ end
454
+
455
+ public_class_method def self.record_runtime(opts = {})
456
+ klass = opts[:command_class].to_s
457
+ secs = opts[:seconds].to_f
458
+ return {} if klass.empty? || secs <= 0
459
+
460
+ data = load_runtimes
461
+ data[klass] ||= []
462
+ data[klass] << secs
463
+ data[klass] = data[klass].last(50)
464
+ FileUtils.mkdir_p(File.dirname(RUNTIMES_FILE))
465
+ File.write(RUNTIMES_FILE, JSON.generate(data))
466
+ data
467
+ end
468
+
469
+ public_class_method def self.predicted_timeout(opts = {})
470
+ samples = Array(load_runtimes[opts[:command_class].to_s]).map(&:to_f)
471
+ return nil if samples.empty?
472
+
473
+ sorted = samples.sort
474
+ idx = [(sorted.length * 0.95).ceil - 1, 0].max
475
+ (sorted[idx] * 1.5).ceil
476
+ end
477
+
478
+ public_class_method def self.auto_job?(opts = {})
479
+ pred = opts[:predicted] || predicted_timeout(command_class: opts[:command_class] || command_class(payload: opts[:payload].to_s))
480
+ pred.to_i > 120
481
+ end
482
+
483
+ private_class_method def self.load_runtimes
484
+ return {} unless File.file?(RUNTIMES_FILE)
485
+
486
+ JSON.parse(File.read(RUNTIMES_FILE))
487
+ rescue StandardError
488
+ {}
489
+ end
490
+
355
491
  public_class_method def self.authors
356
492
  "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
357
493
  end
@@ -373,6 +509,31 @@ module PWN
373
509
  text: 'optional - text value consumed by #bashism?'
374
510
  )
375
511
 
512
+ # Strip quoted heredocs and single-quoted strings before bash-syntax lint.
513
+ #{self}.shell_syntax_surface(
514
+ text: 'required - command string to lint'
515
+ )
516
+
517
+ # Mint a per-thread session canary token stored on Thread.current.
518
+ #{self}.mint_canary(
519
+ bytes: 'optional - hex length (defaults to 8)'
520
+ )
521
+
522
+ # True when text contains the current session canary.
523
+ #{self}.canary_leak?(
524
+ text: 'required - outbound argv or URL to inspect'
525
+ )
526
+
527
+ # Heuristic injection score for tool output (ignore-previous, tool-call spoof).
528
+ #{self}.injection_score(
529
+ text: 'required - tool output to score'
530
+ )
531
+
532
+ # Prefix high-score tool output with a QUARANTINED frame.
533
+ #{self}.quarantine_output(
534
+ text: 'required - tool output to wrap if injection_score >= 3'
535
+ )
536
+
376
537
  # Run shell bash and return its result
377
538
  #{self}.shell_bash?
378
539
 
@@ -458,6 +619,45 @@ module PWN
458
619
  args: 'optional - args value consumed by #refuse_copied_persist?'
459
620
  )
460
621
 
622
+ # Refuse a command whose IPs/hosts are outside the ~/.pwn scope yaml allowlists.
623
+ #{self}.scope_refusal(
624
+ command: 'required - shell command to inspect for IPs and hostnames'
625
+ )
626
+
627
+ # True when ip is RFC1918 or loopback (always in-scope unless strict).
628
+ #{self}.rfc1918?(
629
+ ip: 'required - IPv4 address'
630
+ )
631
+
632
+ # True when ip is inside cidr (e.g. 10.1.2.3 in 10.0.0.0/8).
633
+ #{self}.ip_in_cidr?(
634
+ ip: 'required - IPv4 address',
635
+ cidr: 'required - CIDR (e.g. 10.0.0.0/8)'
636
+ )
637
+
638
+ # First token of a payload used as the runtime class key.
639
+ #{self}.command_class(
640
+ payload: 'required - command string whose first token is the class'
641
+ )
642
+
643
+ # Append an observed runtime sample for a command class.
644
+ #{self}.record_runtime(
645
+ command_class: 'required - class key from the command first token',
646
+ seconds: 'required - observed wall seconds'
647
+ )
648
+
649
+ # p95 * 1.5 timeout from ~/.pwn/metrics/runtimes.json, or nil.
650
+ #{self}.predicted_timeout(
651
+ command_class: 'required - class key to look up'
652
+ )
653
+
654
+ # True when predicted timeout exceeds 120s (route to job_run).
655
+ #{self}.auto_job?(
656
+ command_class: 'optional - class key',
657
+ predicted: 'optional - override predicted seconds',
658
+ payload: 'optional - command string if class omitted'
659
+ )
660
+
461
661
  # Print the AUTHOR(S) string for this module.
462
662
  #{self}.authors
463
663
  "
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'artifacts_list',
7
+ toolset: 'sessions',
8
+ schema: {
9
+ name: 'artifacts_list',
10
+ description: 'List loot under ~/.pwn/artifacts for a session.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: { session_id: { type: 'string' } }
14
+ }
15
+ },
16
+ handler: lambda { |args|
17
+ PWN::Plugins::ArtifactRegistry.list(session_id: args[:session_id] || args['session_id'] || 'default')
18
+ }
19
+ )
20
+ PWN::AI::Agent::Registry.register(
21
+ name: 'artifacts_get',
22
+ toolset: 'sessions',
23
+ schema: {
24
+ name: 'artifacts_get',
25
+ description: 'Read an artifact by path (sha256 + body cap).',
26
+ parameters: {
27
+ type: 'object',
28
+ properties: { path: { type: 'string' } },
29
+ required: %w[path]
30
+ }
31
+ },
32
+ handler: lambda { |args|
33
+ PWN::Plugins::ArtifactRegistry.get(path: args[:path] || args['path'])
34
+ }
35
+ )
36
+ PWN::AI::Agent::Registry.register(
37
+ name: 'artifact_read',
38
+ toolset: 'sessions',
39
+ schema: {
40
+ name: 'artifact_read',
41
+ description: 'Page an artifact as text/hex/base64 from offset.',
42
+ parameters: {
43
+ type: 'object',
44
+ properties: {
45
+ path: { type: 'string' },
46
+ offset: { type: 'integer' },
47
+ length: { type: 'integer' },
48
+ mode: { type: 'string', description: 'text|hex|base64' }
49
+ },
50
+ required: %w[path]
51
+ }
52
+ },
53
+ handler: lambda { |args|
54
+ PWN::Plugins::ArtifactRegistry.read_page(
55
+ path: args[:path] || args['path'],
56
+ offset: args[:offset] || args['offset'],
57
+ length: args[:length] || args['length'],
58
+ mode: args[:mode] || args['mode']
59
+ )
60
+ }
61
+ )
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'binary_triage',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'binary_triage',
10
+ description: 'Triage an ELF/PE/Mach-O binary (format, arch, imports, exports) and spill JSON to ~/.pwn/artifacts.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: { path: { type: 'string', description: 'Filesystem path of the binary.' } },
14
+ required: %w[path]
15
+ }
16
+ },
17
+ handler: lambda { |args|
18
+ PWN::Plugins::BinaryParser.triage(path: args[:path] || args['path'])
19
+ }
20
+ )
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'decompile',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'decompile',
10
+ description: 'Decompile a binary via Ghidra analyzeHeadless, or r2 pdg if Ghidra is absent.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ bin: { type: 'string' },
15
+ path: { type: 'string' },
16
+ project_dir: { type: 'string' }
17
+ },
18
+ required: %w[bin]
19
+ }
20
+ },
21
+ handler: lambda { |args|
22
+ PWN::Plugins::Ghidra.decompile(
23
+ bin: args[:bin] || args[:path] || args['bin'],
24
+ project_dir: args[:project_dir] || args['project_dir']
25
+ )
26
+ }
27
+ )
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'exploitdev',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'exploitdev',
10
+ description: 'Exploit-dev helpers: pattern_create, pattern_offset, rop_gadgets, checksec, packing.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ action: { type: 'string', description: 'pattern_create|pattern_offset|rop_gadgets|p32|p64|checksec' },
15
+ n: { type: 'integer' },
16
+ length: { type: 'integer' },
17
+ value: { type: 'string' },
18
+ path: { type: 'string' },
19
+ filter: { type: 'string' }
20
+ },
21
+ required: %w[action]
22
+ }
23
+ },
24
+ handler: lambda { |args|
25
+ act = (args[:action] || args['action']).to_s
26
+ case act
27
+ when 'pattern_create'
28
+ PWN::Plugins::ExploitDev.cyclic(length: args[:n] || args[:length] || args['n'] || 200)
29
+ when 'pattern_offset'
30
+ PWN::Plugins::ExploitDev.cyclic_find(value: args[:value] || args['value'], length: args[:length] || args['length'] || 400)
31
+ 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) }
35
+ when 'p32'
36
+ PWN::Plugins::ExploitDev.p32(value: args[:value] || args['value'])
37
+ when 'p64'
38
+ PWN::Plugins::ExploitDev.p64(value: args[:value] || args['value'])
39
+ when 'checksec'
40
+ PWN::Plugins::GDB.mitigations(binary: args[:path] || args['path'])
41
+ else
42
+ { error: "unknown action #{act}" }
43
+ end
44
+ }
45
+ )
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'finding_record',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'finding_record',
10
+ description: 'Append a finding row (title, severity, host, evidence, poc path) to ~/.pwn/findings.jsonl.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ title: { type: 'string' },
15
+ severity: { type: 'string' },
16
+ host: { type: 'string' },
17
+ evidence: { type: 'string' },
18
+ poc: { type: 'string' },
19
+ poc_artifacts: { type: 'array', items: { type: 'string' } },
20
+ session_id: { type: 'string' }
21
+ },
22
+ required: %w[title]
23
+ }
24
+ },
25
+ handler: lambda { |args|
26
+ PWN::Plugins::Findings.record(
27
+ title: args[:title] || args['title'],
28
+ severity: args[:severity] || args['severity'],
29
+ host: args[:host] || args['host'],
30
+ evidence: args[:evidence] || args['evidence'],
31
+ poc: args[:poc] || args['poc'],
32
+ poc_artifacts: args[:poc_artifacts] || args['poc_artifacts'],
33
+ session_id: args[:session_id] || args['session_id']
34
+ )
35
+ }
36
+ )
37
+ PWN::AI::Agent::Registry.register(
38
+ name: 'finding_report',
39
+ toolset: 'pwn',
40
+ schema: {
41
+ name: 'finding_report',
42
+ description: 'List recorded findings from ~/.pwn/findings.jsonl.',
43
+ parameters: { type: 'object', properties: {} }
44
+ },
45
+ handler: lambda { |_args|
46
+ PWN::Plugins::Findings.report
47
+ }
48
+ )
@@ -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: 'fuzz_campaign',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'fuzz_campaign',
10
+ description: 'Start, status, stop, or triage an AFL++ campaign via background jobs.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ action: { type: 'string', description: 'start|status|stop|triage' },
15
+ in_dir: { type: 'string' },
16
+ out_dir: { type: 'string' },
17
+ target: { type: 'string' },
18
+ id: { type: 'string' }
19
+ },
20
+ required: %w[action]
21
+ }
22
+ },
23
+ handler: lambda { |args|
24
+ act = (args[:action] || args['action']).to_s
25
+ case act
26
+ 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)
32
+ when 'status'
33
+ row = PWN::Plugins::Jobs.status(id: args[:id] || args['id'])
34
+ out_dir = args[:out_dir] || args['out_dir']
35
+ out_dir ? row.merge(PWN::Plugins::AFLplusplus.parse_stats(out_dir: out_dir)) : row
36
+ when 'stop'
37
+ PWN::Plugins::Jobs.stop(id: args[:id] || args['id'])
38
+ when 'triage'
39
+ PWN::Plugins::AFLplusplus.crash_triage(out_dir: args[:out_dir] || args['out_dir'])
40
+ else
41
+ { error: "unknown action #{act}" }
42
+ end
43
+ }
44
+ )
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'intel_lookup',
7
+ toolset: 'pwn',
8
+ schema: {
9
+ name: 'intel_lookup',
10
+ description: 'Look up CVE/CWE/CPE via local intel then searchsploit JSON.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ query: { type: 'string' },
15
+ cve: { type: 'string' },
16
+ cpe: { type: 'string' }
17
+ }
18
+ }
19
+ },
20
+ handler: lambda { |args|
21
+ q = args[:query] || args['query'] || args[:cve] || args['cve'] || args[:cpe] || args['cpe']
22
+ PWN::Plugins::ExploitDB.search(query: q)
23
+ }
24
+ )
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'job_run',
7
+ toolset: 'terminal',
8
+ schema: {
9
+ name: 'job_run',
10
+ description: 'Start a detached background job (nmap, afl, hashcat). Returns {id, pid, log}.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ command: { type: 'string' },
15
+ cmd: { type: 'string' },
16
+ tag: { type: 'string' }
17
+ },
18
+ required: %w[command]
19
+ }
20
+ },
21
+ handler: lambda { |args|
22
+ PWN::Plugins::Jobs.start(command: args[:command] || args[:cmd] || args['command'], session_id: args[:tag] || args['tag'])
23
+ }
24
+ )
25
+ PWN::AI::Agent::Registry.register(
26
+ name: 'job_status',
27
+ toolset: 'terminal',
28
+ schema: {
29
+ name: 'job_status',
30
+ description: 'Poll a job started by job_run.',
31
+ parameters: {
32
+ type: 'object',
33
+ properties: { id: { type: 'string' } },
34
+ required: %w[id]
35
+ }
36
+ },
37
+ handler: lambda { |args|
38
+ PWN::Plugins::Jobs.status(id: args[:id] || args['id'])
39
+ }
40
+ )
41
+ PWN::AI::Agent::Registry.register(
42
+ name: 'job_result',
43
+ toolset: 'terminal',
44
+ schema: {
45
+ name: 'job_result',
46
+ description: 'Fetch log tail and paths for a job_run id.',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: { id: { type: 'string' }, lines: { type: 'integer' } },
50
+ required: %w[id]
51
+ }
52
+ },
53
+ handler: lambda { |args|
54
+ PWN::Plugins::Jobs.result(id: args[:id] || args['id'], lines: args[:lines] || args['lines'])
55
+ }
56
+ )
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'pty_open',
7
+ toolset: 'terminal',
8
+ schema: {
9
+ name: 'pty_open',
10
+ description: 'Open a persistent PTY (gdb, r2, ssh, sh). Returns {id, pid}.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: { cmd: { type: 'string' }, command: { type: 'string' } },
14
+ required: %w[cmd]
15
+ }
16
+ },
17
+ handler: lambda { |args|
18
+ PWN::Plugins::ProcessTube.spawn(cmd: args[:cmd] || args[:command] || args['cmd'])
19
+ }
20
+ )
21
+ PWN::AI::Agent::Registry.register(
22
+ name: 'pty_send',
23
+ toolset: 'terminal',
24
+ schema: {
25
+ name: 'pty_send',
26
+ description: 'Write a line to a PTY opened by pty_open.',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: { id: { type: 'string' }, line: { type: 'string' }, data: { type: 'string' } },
30
+ required: %w[id]
31
+ }
32
+ },
33
+ handler: lambda { |args|
34
+ PWN::Plugins::ProcessTube.write_line(id: args[:id] || args['id'], line: args[:line] || args[:data] || args['line'])
35
+ }
36
+ )
37
+ PWN::AI::Agent::Registry.register(
38
+ name: 'pty_read',
39
+ toolset: 'terminal',
40
+ schema: {
41
+ name: 'pty_read',
42
+ description: 'Read a PTY until a string or one line.',
43
+ parameters: {
44
+ type: 'object',
45
+ properties: { id: { type: 'string' }, until: { type: 'string' }, timeout: { type: 'integer' } },
46
+ required: %w[id]
47
+ }
48
+ },
49
+ handler: lambda { |args|
50
+ id = args[:id] || args['id']
51
+ if args[:until] || args['until']
52
+ PWN::Plugins::ProcessTube.recvuntil(id: id, until: args[:until] || args['until'], timeout: args[:timeout])
53
+ else
54
+ PWN::Plugins::ProcessTube.recvline(id: id, timeout: args[:timeout])
55
+ end
56
+ }
57
+ )
58
+ PWN::AI::Agent::Registry.register(
59
+ name: 'pty_close',
60
+ toolset: 'terminal',
61
+ schema: {
62
+ name: 'pty_close',
63
+ description: 'Close a PTY handle from pty_open.',
64
+ parameters: {
65
+ type: 'object',
66
+ properties: { id: { type: 'string' } },
67
+ required: %w[id]
68
+ }
69
+ },
70
+ handler: lambda { |args|
71
+ PWN::Plugins::ProcessTube.close(id: args[:id] || args['id'])
72
+ }
73
+ )
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pwn/ai/agent/registry'
4
+
5
+ PWN::AI::Agent::Registry.register(
6
+ name: 'session_export',
7
+ toolset: 'sessions',
8
+ schema: {
9
+ name: 'session_export',
10
+ description: 'Export a session transcript plus attached artifacts as a tar.gz under ~/.pwn/exports.',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: { session_id: { type: 'string' } },
14
+ required: %w[session_id]
15
+ }
16
+ },
17
+ handler: lambda { |args|
18
+ PWN::Sessions.export(session_id: args[:session_id] || args['session_id'])
19
+ }
20
+ )
@@ -71,6 +71,11 @@ PWN::AI::Agent::Registry.register(
71
71
  )
72
72
  end
73
73
 
74
+ scoped = PWN::AI::Agent::ToolGuard.scope_refusal(command: cmd)
75
+ return scoped if scoped
76
+
77
+ return PWN::Plugins::Jobs.start(command: cmd).merge(routed: 'job_run') if args[:timeout].to_i <= 0 && PWN::AI::Agent::ToolGuard.auto_job?(payload: cmd)
78
+
74
79
  stdout = +''
75
80
  stderr = +''
76
81
  exitstatus = nil