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.
- checksums.yaml +4 -4
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +16 -0
- data/etc/default_skills/pwn/ai/agent/tools/artifacts/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/binary_triage/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/exploitdev/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/finding_record/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/fuzz_campaign/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/intel_lookup/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/job_run/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/pty_session/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/session_export/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/swarm_map/SKILL.md +45 -0
- data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/detect_os/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +2 -0
- data/etc/default_skills/pwn/sessions/SKILL.md +3 -0
- data/etc/default_skills/pwn/setup/references/urls.md +1 -0
- data/etc/intel/files_exploits.csv +2 -0
- data/lib/pwn/ai/agent/dispatch.rb +5 -0
- data/lib/pwn/ai/agent/learning.rb +72 -0
- data/lib/pwn/ai/agent/loop.rb +62 -13
- data/lib/pwn/ai/agent/mistakes.rb +21 -1
- data/lib/pwn/ai/agent/prompt_builder.rb +16 -13
- data/lib/pwn/ai/agent/registry.rb +21 -1
- data/lib/pwn/ai/agent/reward.rb +4 -2
- data/lib/pwn/ai/agent/swarm.rb +26 -0
- data/lib/pwn/ai/agent/tool_guard.rb +201 -1
- data/lib/pwn/ai/agent/tools/artifacts.rb +61 -0
- data/lib/pwn/ai/agent/tools/binary_triage.rb +20 -0
- data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +45 -0
- data/lib/pwn/ai/agent/tools/finding_record.rb +48 -0
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -0
- data/lib/pwn/ai/agent/tools/intel_lookup.rb +24 -0
- data/lib/pwn/ai/agent/tools/job_run.rb +56 -0
- data/lib/pwn/ai/agent/tools/pty_session.rb +73 -0
- data/lib/pwn/ai/agent/tools/session_export.rb +20 -0
- data/lib/pwn/ai/agent/tools/shell.rb +5 -0
- data/lib/pwn/ai/agent/tools/swarm_map.rb +26 -0
- data/lib/pwn/config.rb +10 -1
- data/lib/pwn/memory_index.rb +50 -0
- data/lib/pwn/plugins/aflplusplus.rb +30 -1
- data/lib/pwn/plugins/artifact_registry.rb +43 -0
- data/lib/pwn/plugins/binary_parser.rb +31 -0
- data/lib/pwn/plugins/detect_os.rb +109 -2
- data/lib/pwn/plugins/exploit_db.rb +38 -1
- data/lib/pwn/plugins/findings.rb +105 -0
- data/lib/pwn/plugins/ghidra.rb +75 -0
- data/lib/pwn/plugins/jobs.rb +12 -1
- data/lib/pwn/plugins/log.rb +18 -3
- data/lib/pwn/plugins/nmap_it.rb +29 -0
- data/lib/pwn/plugins/preflight_checker.rb +36 -0
- data/lib/pwn/plugins/transparent_browser.rb +29 -0
- data/lib/pwn/plugins.rb +2 -0
- data/lib/pwn/sessions.rb +92 -1
- data/lib/pwn/setup.rb +41 -15
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +7 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +57 -2
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/intel_lookup_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +17 -0
- data/spec/lib/pwn/ai/agent/tools/session_export_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_map_spec.rb +14 -0
- data/spec/lib/pwn/cron_spec.rb +2 -2
- data/spec/lib/pwn/memory_index_spec.rb +9 -0
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
- data/spec/lib/pwn/plugins/detect_os_spec.rb +40 -0
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +30 -0
- data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
- data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
- data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
- data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
- data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
- data/spec/lib/pwn/plugins/log_spec.rb +10 -5
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
- data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
- data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
- data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
- data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
- data/spec/lib/pwn/sessions_spec.rb +53 -0
- data/spec/lib/pwn/setup_spec.rb +32 -0
- data/third_party/pwn_rdoc.jsonl +64 -2
- metadata +42 -3
- data/spec/lib/pwn/req_queue_spec.rb +0 -168
- 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]
|
|
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
|