pwn 0.5.713 → 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/tool_guard/SKILL.md +8 -0
- data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
- data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
- data/etc/default_skills/pwn/setup/references/urls.md +1 -0
- data/lib/pwn/ai/agent/dispatch.rb +5 -0
- data/lib/pwn/ai/agent/loop.rb +10 -0
- data/lib/pwn/ai/agent/mistakes.rb +2 -1
- data/lib/pwn/ai/agent/prompt_builder.rb +4 -1
- data/lib/pwn/ai/agent/registry.rb +21 -1
- data/lib/pwn/ai/agent/reward.rb +2 -1
- data/lib/pwn/ai/agent/tool_guard.rb +83 -2
- data/lib/pwn/ai/agent/tools/artifacts.rb +26 -0
- data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
- data/lib/pwn/config.rb +7 -1
- data/lib/pwn/memory_index.rb +50 -0
- data/lib/pwn/plugins/artifact_registry.rb +29 -0
- data/lib/pwn/plugins/ghidra.rb +75 -0
- data/lib/pwn/plugins/log.rb +18 -3
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/setup.rb +5 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/{ai/agent/fixtures/req09_docker.json → lib/pwn/ai/agent/fixtures/error_class_docker.json} +1 -1
- data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +15 -1
- 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 +1 -0
- data/spec/lib/pwn/ai/agent/tools/decompile_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/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 +15 -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 +5 -0
- data/third_party/pwn_rdoc.jsonl +18 -0
- metadata +8 -6
- data/spec/ai/agent/fixtures_spec.rb +0 -13
- data/spec/lib/pwn/remaining_req_spec.rb +0 -143
- data/spec/lib/pwn/req24_spec.rb +0 -82
- data/spec/lib/pwn/req_queue_spec.rb +0 -168
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 347698a0da0cfba54c8e8975a931cd0894940c126e842c17c5bad162e1307eee
|
|
4
|
+
data.tar.gz: fd5495f0f65d8e06acc5695b4ca796818e107ea164285c79161cf2c44c7184db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bdcfa6f78d2ef9d8350cfc7dd69c4294e679fd537112428922858dea21eaad3ed9d295247c194dcf7372289e18c31c63fcf79076e82117792fb3c3214f2ad341
|
|
7
|
+
data.tar.gz: a604db47a0ea8f698cd8c3b659d626951209082a7bfff7b4ec51c6529d200d24ab9d36d7302d6796bdc422d89ab9e8b45ea67bf926b43dee4b2d4ced4c7feeff
|
|
@@ -36,6 +36,11 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
36
36
|
- `present`
|
|
37
37
|
- `placeholder`
|
|
38
38
|
- `bashism`
|
|
39
|
+
- `shell_syntax_surface`
|
|
40
|
+
- `mint_canary`
|
|
41
|
+
- `canary_leak`
|
|
42
|
+
- `injection_score`
|
|
43
|
+
- `quarantine_output`
|
|
39
44
|
- `shell_bash`
|
|
40
45
|
- `shell_name`
|
|
41
46
|
- `protect_http`
|
|
@@ -54,6 +59,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
54
59
|
- `timeout_prior_count`
|
|
55
60
|
- `refuse_copied_persist`
|
|
56
61
|
- `scope_refusal`
|
|
62
|
+
- `rfc1918`
|
|
57
63
|
- `ip_in_cidr`
|
|
58
64
|
- `command_class`
|
|
59
65
|
- `record_runtime`
|
|
@@ -63,6 +69,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
63
69
|
- `help`
|
|
64
70
|
- `auto_job?`
|
|
65
71
|
- `bashism?`
|
|
72
|
+
- `canary_leak?`
|
|
66
73
|
- `ip_in_cidr?`
|
|
67
74
|
- `note_timeout!`
|
|
68
75
|
- `placeholder?`
|
|
@@ -71,6 +78,7 @@ PWN::AI::Agent::ToolGuard.present(opts)
|
|
|
71
78
|
- `protect_http!`
|
|
72
79
|
- `refuse_copied_persist?`
|
|
73
80
|
- `reset_timeout_budget!`
|
|
81
|
+
- `rfc1918?`
|
|
74
82
|
- `shell_bash?`
|
|
75
83
|
|
|
76
84
|
## Source
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwn-ai-agent-tools-decompile
|
|
3
|
+
description: Drive PWN::Ai::Agent::Tools::Decompile from pwn_eval.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, pwn_eval]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
generated: true
|
|
9
|
+
module: PWN::Ai::Agent::Tools::Decompile
|
|
10
|
+
source: pwn/ai/agent/tools/decompile.rb
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PWN::Ai::Agent::Tools::Decompile
|
|
14
|
+
|
|
15
|
+
Public API for PWN::Ai::Agent::Tools::Decompile.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Call `PWN::Ai::Agent::Tools::Decompile` from `pwn_eval` when the task needs this module.
|
|
20
|
+
Do not reimplement it in shell.
|
|
21
|
+
|
|
22
|
+
## Methodologies
|
|
23
|
+
|
|
24
|
+
Generated from `pwn/ai/agent/tools/decompile.rb`. Prefer the public class methods below.
|
|
25
|
+
Class methods take `(opts = {})` and read `opts`.
|
|
26
|
+
|
|
27
|
+
## How to call
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
PWN::Ai::Agent::Tools::Decompile.help
|
|
31
|
+
PWN::Ai::Agent::Tools::Decompile.help(opts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Public methods
|
|
35
|
+
|
|
36
|
+
- _(no public class methods parsed)_
|
|
37
|
+
|
|
38
|
+
## Source
|
|
39
|
+
|
|
40
|
+
`pwn/ai/agent/tools/decompile.rb`
|
|
41
|
+
|
|
42
|
+
## Verification
|
|
43
|
+
|
|
44
|
+
`PWN::Ai::Agent::Tools::Decompile.respond_to?(:help)` after the
|
|
45
|
+
module is loaded. Read the source for parameter names.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwn-plugins-ghidra
|
|
3
|
+
description: Drive PWN::Plugins::Ghidra from pwn_eval.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, pwn_eval]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
generated: true
|
|
9
|
+
module: PWN::Plugins::Ghidra
|
|
10
|
+
source: pwn/plugins/ghidra.rb
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PWN::Plugins::Ghidra
|
|
14
|
+
|
|
15
|
+
Headless Ghidra analyzeHeadless wrapper with r2 fallback.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Call `PWN::Plugins::Ghidra` from `pwn_eval` when the task needs this module.
|
|
20
|
+
Do not reimplement it in shell.
|
|
21
|
+
|
|
22
|
+
## Methodologies
|
|
23
|
+
|
|
24
|
+
Generated from `pwn/plugins/ghidra.rb`. Prefer the public class methods below.
|
|
25
|
+
Class methods take `(opts = {})` and read `opts`.
|
|
26
|
+
|
|
27
|
+
## How to call
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
PWN::Plugins::Ghidra.help
|
|
31
|
+
PWN::Plugins::Ghidra.required_bins(opts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Public methods
|
|
35
|
+
|
|
36
|
+
- `required_bins`
|
|
37
|
+
- `analyze`
|
|
38
|
+
- `decompile`
|
|
39
|
+
- `authors`
|
|
40
|
+
- `help`
|
|
41
|
+
|
|
42
|
+
## Source
|
|
43
|
+
|
|
44
|
+
`pwn/plugins/ghidra.rb`
|
|
45
|
+
|
|
46
|
+
## Verification
|
|
47
|
+
|
|
48
|
+
`PWN::Plugins::Ghidra.respond_to?(:required_bins)` after the
|
|
49
|
+
module is loaded. Read the source for parameter names.
|
|
@@ -46,6 +46,10 @@ module PWN
|
|
|
46
46
|
args = parse_args(raw: raw, entry: entry)
|
|
47
47
|
required = Array(entry.schema&.dig(:parameters, :required))
|
|
48
48
|
args = ToolGuard.coerce_args(args: args, required: required) if defined?(ToolGuard)
|
|
49
|
+
if defined?(ToolGuard) && ToolGuard.respond_to?(:canary_leak?) &&
|
|
50
|
+
ToolGuard.canary_leak?(text: args.inspect)
|
|
51
|
+
return JSON.generate(success: false, error: 'refused: session canary in outbound args')
|
|
52
|
+
end
|
|
49
53
|
if defined?(ToolGuard) && ToolGuard.respond_to?(:refuse_copied_persist?) &&
|
|
50
54
|
ToolGuard.refuse_copied_persist?(name: entry.name, args: args)
|
|
51
55
|
return JSON.generate(
|
|
@@ -54,6 +58,7 @@ module PWN
|
|
|
54
58
|
)
|
|
55
59
|
end
|
|
56
60
|
result = entry.handler.call(args)
|
|
61
|
+
result = ToolGuard.quarantine_output(text: result) if defined?(ToolGuard) && result.is_a?(String) && ToolGuard.respond_to?(:quarantine_output)
|
|
57
62
|
JSON.generate(success: true, result: result, effect: effect(name: entry.name, args: args))
|
|
58
63
|
rescue StandardError => e
|
|
59
64
|
JSON.generate(
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -1168,12 +1168,22 @@ module PWN
|
|
|
1168
1168
|
end
|
|
1169
1169
|
|
|
1170
1170
|
private_class_method def self.note_same_payload!(opts = {})
|
|
1171
|
+
return 0 if read_like?(opts)
|
|
1172
|
+
|
|
1171
1173
|
sig = payload_sig(opts)
|
|
1172
1174
|
counts = Thread.current[:pwn_same_payload] ||= Hash.new(0)
|
|
1173
1175
|
counts[sig] += 1
|
|
1174
1176
|
counts[sig]
|
|
1175
1177
|
end
|
|
1176
1178
|
|
|
1179
|
+
private_class_method def self.read_like?(opts = {})
|
|
1180
|
+
name = opts[:name].to_s
|
|
1181
|
+
return true if name.match?(/_(read|get|list|status|recall|tail)\z/)
|
|
1182
|
+
return true if %w[pty_read artifact_read artifacts_get job_status job_result].include?(name)
|
|
1183
|
+
|
|
1184
|
+
false
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1177
1187
|
private_class_method def self.no_progress_result(opts = {})
|
|
1178
1188
|
checkpoint_result(opts)
|
|
1179
1189
|
end
|
|
@@ -115,6 +115,7 @@ module PWN
|
|
|
115
115
|
return 'auth_denied' if e.match?(/access denied|unauthorized|not authorized|\b401\b|\b403\b|pull access denied/i)
|
|
116
116
|
return 'name_conflict' if e.match?(/already (?:in use|exists)|name conflict|Conflict\.|duplicate/i)
|
|
117
117
|
return 'parse_error' if e.match?(/parse error|parsererror|template.*error|unexpected token/i)
|
|
118
|
+
return 'socket_perm' if e.match?(/docker\.sock|unix(?:\s+|:)socket|eperm.*sock|sock.*permission/i)
|
|
118
119
|
return 'missing_path' if e.match?(%r{no such file|enoent|not found: /}i)
|
|
119
120
|
return 'net_unreach' if e.match?(/network is unreachable|no route to host|ehostunreach/i)
|
|
120
121
|
return 'perm_denied' if e.match?(/permission denied|eacces/i)
|
|
@@ -872,7 +873,7 @@ module PWN
|
|
|
872
873
|
error: 'required - raw error text (will be normalised)'
|
|
873
874
|
)
|
|
874
875
|
|
|
875
|
-
# Classify an error into auth_denied/name_conflict/parse_error/missing_path/net_unreach/perm_denied/timeout/other.
|
|
876
|
+
# Classify an error into auth_denied/name_conflict/parse_error/socket_perm/missing_path/net_unreach/perm_denied/timeout/other.
|
|
876
877
|
#{self}.error_class(
|
|
877
878
|
error: 'required - raw error text'
|
|
878
879
|
)
|
|
@@ -201,9 +201,12 @@ module PWN
|
|
|
201
201
|
load_line = "load1=#{snap[:load1]} ncpu=#{snap[:ncpu]} mem_avail_mb=#{snap[:mem_avail_mb]}"
|
|
202
202
|
doc = (PWN::Plugins::PreflightChecker.host_summary if defined?(PWN::Plugins::PreflightChecker))
|
|
203
203
|
land = (PWN::Plugins::DetectOS.living_off_the_land[:summary] if defined?(PWN::Plugins::DetectOS))
|
|
204
|
+
canary = (Thread.current[:pwn_canary] || ToolGuard.mint_canary if defined?(ToolGuard) && ToolGuard.respond_to?(:mint_canary))
|
|
204
205
|
"#{load_line} pwn_eval/shell timeout = conservative seconds for this host " \
|
|
205
206
|
"(defaults eval=#{eval_s || 20}s shell=#{shell_s || 30}s, clamped). " \
|
|
206
|
-
"LIVING OFF THE LAND #{land} #{doc}"
|
|
207
|
+
"LIVING OFF THE LAND #{land} #{doc} " \
|
|
208
|
+
"CANARY #{canary} never copy this token into tool args. " \
|
|
209
|
+
'DOMAIN TOOLS for pentest/RE: binary_triage, pty_open, job_run, exploitdev, fuzz_campaign, finding_record, decompile.'
|
|
207
210
|
rescue StandardError
|
|
208
211
|
'load unknown — pass a conservative timeout on pwn_eval/shell anyway.'
|
|
209
212
|
end
|
|
@@ -127,7 +127,8 @@ module PWN
|
|
|
127
127
|
pool = pool.select { |e| CORE_TOOLS.include?(e.name) }
|
|
128
128
|
elsif opts[:relevance] && router_enabled?
|
|
129
129
|
keep = rank({ query: opts[:relevance], entries: pool }.merge(pref_fwd)).first(opts[:top_k] || 10).map(&:name)
|
|
130
|
-
|
|
130
|
+
pinned = domain_pin_names(query: opts[:relevance])
|
|
131
|
+
names = (CORE_TOOLS + keep + pinned).uniq
|
|
131
132
|
pool = pool.select { |e| names.include?(e.name) }
|
|
132
133
|
end
|
|
133
134
|
|
|
@@ -338,6 +339,25 @@ module PWN
|
|
|
338
339
|
{}
|
|
339
340
|
end
|
|
340
341
|
|
|
342
|
+
DOMAIN_PINS = {
|
|
343
|
+
re: %w[pty_open pty_send pty_read pty_close binary_triage exploitdev job_run decompile],
|
|
344
|
+
pentest: %w[pty_open job_run finding_record binary_triage fuzz_campaign exploitdev],
|
|
345
|
+
fuzzing: %w[fuzz_campaign job_run job_status job_result],
|
|
346
|
+
exploitation: %w[exploitdev pty_open job_run finding_record],
|
|
347
|
+
reporting: %w[finding_record finding_report artifacts_list]
|
|
348
|
+
}.freeze
|
|
349
|
+
|
|
350
|
+
private_class_method def self.domain_pin_names(opts = {})
|
|
351
|
+
q = opts[:query].to_s.downcase
|
|
352
|
+
classes = []
|
|
353
|
+
classes << :re if q.match?(/reverse.?engineer|binary.?analy|decompile/)
|
|
354
|
+
classes << :pentest if q.match?(/penet|bug.?bounty|offensive|red.?team|pentest/)
|
|
355
|
+
classes << :fuzzing if q.match?(/\bfuzz|afl/)
|
|
356
|
+
classes << :exploitation if q.match?(/exploit|payload/)
|
|
357
|
+
classes << :reporting if q.match?(/finding|report|severity/)
|
|
358
|
+
classes.flat_map { |c| Array(DOMAIN_PINS[c]) }.uniq
|
|
359
|
+
end
|
|
360
|
+
|
|
341
361
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
342
362
|
|
|
343
363
|
public_class_method def self.authors
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -1252,7 +1252,8 @@ module PWN
|
|
|
1252
1252
|
private_class_method def self.judge_model
|
|
1253
1253
|
return nil unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1254
1254
|
|
|
1255
|
-
m = PWN::Env.dig(:ai, :agent, :
|
|
1255
|
+
m = PWN::Env.dig(:ai, :agent, :model_routes, :judge)
|
|
1256
|
+
m = PWN::Env.dig(:ai, :agent, :reward_model) if m.to_s.empty?
|
|
1256
1257
|
m = PWN::Env.dig(:ai, :reflect_model) if m.to_s.empty?
|
|
1257
1258
|
m.to_s.empty? ? nil : m.to_s
|
|
1258
1259
|
rescue StandardError
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'fileutils'
|
|
6
|
+
require 'securerandom'
|
|
6
7
|
|
|
7
8
|
module PWN
|
|
8
9
|
module AI
|
|
@@ -45,11 +46,50 @@ module PWN
|
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
public_class_method def self.bashism?(opts = {})
|
|
48
|
-
BASHISM_RX.match?(opts[:text]
|
|
49
|
+
BASHISM_RX.match?(shell_syntax_surface(text: opts[:text]))
|
|
49
50
|
rescue StandardError
|
|
50
51
|
false
|
|
51
52
|
end
|
|
52
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
|
+
|
|
53
93
|
public_class_method def self.shell_bash?
|
|
54
94
|
v = (PWN::Env.dig(:ai, :agent, :shell_bash) if defined?(PWN::Env))
|
|
55
95
|
v == true || v.to_s.match?(/\A(1|true|yes|on)\z/i)
|
|
@@ -373,7 +413,7 @@ module PWN
|
|
|
373
413
|
|
|
374
414
|
ips = cmd.scan(/\b\d{1,3}(?:\.\d{1,3}){3}\b/)
|
|
375
415
|
hosts = cmd.scan(/\b[a-z0-9][a-z0-9.-]+\.[a-z]{2,}\b/i)
|
|
376
|
-
bad_ip = allow.any? ? ips.find { |ip| allow.none? { |cidr| ip_in_cidr?(ip: ip, cidr: cidr) } } : nil
|
|
416
|
+
bad_ip = allow.any? ? ips.find { |ip| !rfc1918?(ip: ip) && allow.none? { |cidr| ip_in_cidr?(ip: ip, cidr: cidr) } } : nil
|
|
377
417
|
bad_host = domains.any? ? hosts.find { |h| domains.none? { |d| h.downcase.end_with?(d.downcase) } } : nil
|
|
378
418
|
hit = bad_ip || bad_host
|
|
379
419
|
return nil unless hit
|
|
@@ -383,6 +423,17 @@ module PWN
|
|
|
383
423
|
nil
|
|
384
424
|
end
|
|
385
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
|
+
|
|
386
437
|
public_class_method def self.ip_in_cidr?(opts = {})
|
|
387
438
|
ip = opts[:ip].to_s.split('.').map(&:to_i)
|
|
388
439
|
cidr, bits = opts[:cidr].to_s.split('/')
|
|
@@ -458,6 +509,31 @@ module PWN
|
|
|
458
509
|
text: 'optional - text value consumed by #bashism?'
|
|
459
510
|
)
|
|
460
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
|
+
|
|
461
537
|
# Run shell bash and return its result
|
|
462
538
|
#{self}.shell_bash?
|
|
463
539
|
|
|
@@ -548,6 +624,11 @@ module PWN
|
|
|
548
624
|
command: 'required - shell command to inspect for IPs and hostnames'
|
|
549
625
|
)
|
|
550
626
|
|
|
627
|
+
# True when ip is RFC1918 or loopback (always in-scope unless strict).
|
|
628
|
+
#{self}.rfc1918?(
|
|
629
|
+
ip: 'required - IPv4 address'
|
|
630
|
+
)
|
|
631
|
+
|
|
551
632
|
# True when ip is inside cidr (e.g. 10.1.2.3 in 10.0.0.0/8).
|
|
552
633
|
#{self}.ip_in_cidr?(
|
|
553
634
|
ip: 'required - IPv4 address',
|
|
@@ -33,3 +33,29 @@ PWN::AI::Agent::Registry.register(
|
|
|
33
33
|
PWN::Plugins::ArtifactRegistry.get(path: args[:path] || args['path'])
|
|
34
34
|
}
|
|
35
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,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
|
+
)
|
data/lib/pwn/config.rb
CHANGED
|
@@ -196,7 +196,13 @@ module PWN
|
|
|
196
196
|
operator_account: nil,
|
|
197
197
|
sessions_keep_days: 90,
|
|
198
198
|
artifacts_keep_days: 90,
|
|
199
|
-
max_total_mb: 512
|
|
199
|
+
max_total_mb: 512,
|
|
200
|
+
model_routes: {
|
|
201
|
+
summarize: nil,
|
|
202
|
+
judge: nil,
|
|
203
|
+
act: nil,
|
|
204
|
+
sensitive: nil
|
|
205
|
+
}
|
|
200
206
|
# multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
|
|
201
207
|
# swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
|
|
202
208
|
}
|
data/lib/pwn/memory_index.rb
CHANGED
|
@@ -272,6 +272,50 @@ module PWN
|
|
|
272
272
|
[]
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
+
public_class_method def self.bm25(opts = {})
|
|
276
|
+
query = opts[:query].to_s.downcase
|
|
277
|
+
docs = Array(opts[:docs]).map(&:to_s)
|
|
278
|
+
qtok = expand_query(query: query)
|
|
279
|
+
return docs.map { |d| { doc: d, score: 0.0 } } if qtok.empty?
|
|
280
|
+
|
|
281
|
+
df = Hash.new(0)
|
|
282
|
+
tokenized = docs.map do |d|
|
|
283
|
+
toks = d.downcase.scan(/[a-z0-9]{3,}/)
|
|
284
|
+
toks.uniq.each { |t| df[t] += 1 }
|
|
285
|
+
toks
|
|
286
|
+
end
|
|
287
|
+
n = docs.length.to_f
|
|
288
|
+
avgdl = tokenized.sum(&:length) / [n, 1].max
|
|
289
|
+
k1 = 1.2
|
|
290
|
+
b = 0.75
|
|
291
|
+
ranked = docs.each_with_index.map do |doc, i|
|
|
292
|
+
toks = tokenized[i]
|
|
293
|
+
tf = Hash.new(0)
|
|
294
|
+
toks.each { |t| tf[t] += 1 }
|
|
295
|
+
dl = toks.length.to_f
|
|
296
|
+
score = qtok.sum do |t|
|
|
297
|
+
f = tf[t]
|
|
298
|
+
next 0.0 if f.zero?
|
|
299
|
+
|
|
300
|
+
idf = Math.log(((n - df[t] + 0.5) / (df[t] + 0.5)) + 1)
|
|
301
|
+
idf * ((f * (k1 + 1)) / (f + (k1 * (1 - b + (b * dl / avgdl)))))
|
|
302
|
+
end
|
|
303
|
+
{ doc: doc, score: score }
|
|
304
|
+
end
|
|
305
|
+
ranked.sort_by { |h| -h[:score] }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private_class_method def self.expand_query(opts = {})
|
|
309
|
+
toks = opts[:query].to_s.downcase.scan(/[a-z0-9]{3,}/)
|
|
310
|
+
table = {
|
|
311
|
+
'bypass' => %w[evasion evade],
|
|
312
|
+
'rate' => %w[throttle],
|
|
313
|
+
'limit' => %w[throttle],
|
|
314
|
+
'login' => %w[auth]
|
|
315
|
+
}
|
|
316
|
+
toks.flat_map { |t| [t] + Array(table[t]) }.uniq
|
|
317
|
+
end
|
|
318
|
+
|
|
275
319
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
276
320
|
|
|
277
321
|
public_class_method def self.authors
|
|
@@ -308,6 +352,12 @@ module PWN
|
|
|
308
352
|
texts: 'required - Array texts value consumed by #embed'
|
|
309
353
|
)
|
|
310
354
|
|
|
355
|
+
# Rank documents by BM25 (pure Ruby; no embedder required).
|
|
356
|
+
#{self}.bm25(
|
|
357
|
+
query: 'required - search text',
|
|
358
|
+
docs: 'required - Array of document strings'
|
|
359
|
+
)
|
|
360
|
+
|
|
311
361
|
# Run reset and return its result
|
|
312
362
|
#{self}.reset
|
|
313
363
|
|
|
@@ -49,6 +49,27 @@ module PWN
|
|
|
49
49
|
{ path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path)[0, 65_536] }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
public_class_method def self.read_page(opts = {})
|
|
53
|
+
path = opts[:path].to_s
|
|
54
|
+
raise 'ERROR: path is required' if path.empty?
|
|
55
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
56
|
+
|
|
57
|
+
offset = opts[:offset].to_i
|
|
58
|
+
length = (opts[:length] || 4_096).to_i
|
|
59
|
+
length = 4_096 if length <= 0
|
|
60
|
+
mode = (opts[:mode] || 'text').to_s
|
|
61
|
+
data = File.binread(path, length, offset).to_s
|
|
62
|
+
case mode
|
|
63
|
+
when 'hex'
|
|
64
|
+
{ path: path, offset: offset, bytes: data.bytesize, hex: data.unpack1('H*') }
|
|
65
|
+
when 'base64'
|
|
66
|
+
require 'base64'
|
|
67
|
+
{ path: path, offset: offset, bytes: data.bytesize, body: Base64.strict_encode64(data) }
|
|
68
|
+
else
|
|
69
|
+
{ path: path, offset: offset, bytes: data.bytesize, body: data.force_encoding('UTF-8').scrub }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
52
73
|
public_class_method def self.authors
|
|
53
74
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
54
75
|
end
|
|
@@ -75,6 +96,14 @@ module PWN
|
|
|
75
96
|
path: 'required - filesystem path of a registered artifact'
|
|
76
97
|
)
|
|
77
98
|
|
|
99
|
+
# Page an artifact as text, hex, or base64 from an offset.
|
|
100
|
+
#{self}.read_page(
|
|
101
|
+
path: 'required - filesystem path of the artifact',
|
|
102
|
+
offset: 'optional - byte offset (defaults to 0)',
|
|
103
|
+
length: 'optional - byte count (defaults to 4096)',
|
|
104
|
+
mode: 'optional - text, hex, or base64 (defaults to text)'
|
|
105
|
+
)
|
|
106
|
+
|
|
78
107
|
# Print the AUTHOR(S) string for this module.
|
|
79
108
|
#{self}.authors
|
|
80
109
|
"
|