pwn 0.5.717 → 0.5.718
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/swarm/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/tools/verify/SKILL.md +45 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_db/SKILL.md +7 -0
- data/etc/default_skills/pwn/plugins/exploit_db/references/urls.md +4 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/sarif/SKILL.md +51 -0
- data/etc/default_skills/pwn/reports/sarif/references/urls.md +3 -0
- data/lib/pwn/ai/agent/dispatch.rb +1 -1
- data/lib/pwn/ai/agent/loop.rb +46 -12
- data/lib/pwn/ai/agent/mistakes.rb +5 -2
- data/lib/pwn/ai/agent/prompt_builder.rb +12 -1
- data/lib/pwn/ai/agent/reward.rb +33 -5
- data/lib/pwn/ai/agent/swarm.rb +45 -0
- data/lib/pwn/ai/agent/tool_guard.rb +38 -6
- data/lib/pwn/ai/agent/tools/finding_record.rb +32 -11
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +3 -1
- data/lib/pwn/ai/agent/tools/intel_lookup.rb +7 -3
- data/lib/pwn/ai/agent/tools/pty_session.rb +43 -0
- data/lib/pwn/ai/agent/tools/session_export.rb +2 -2
- data/lib/pwn/ai/agent/tools/shell.rb +4 -1
- data/lib/pwn/ai/agent/tools/verify.rb +63 -0
- data/lib/pwn/memory.rb +56 -6
- data/lib/pwn/plugins/aflplusplus.rb +18 -0
- data/lib/pwn/plugins/binary_parser.rb +23 -5
- data/lib/pwn/plugins/detect_os.rb +1 -11
- data/lib/pwn/plugins/exploit_db.rb +155 -27
- data/lib/pwn/plugins/exploit_dev.rb +33 -5
- data/lib/pwn/plugins/findings.rb +41 -2
- data/lib/pwn/plugins/jobs.rb +43 -2
- data/lib/pwn/plugins/preflight_checker.rb +11 -1
- data/lib/pwn/plugins/process_tube.rb +44 -0
- data/lib/pwn/plugins/radare2.rb +9 -1
- data/lib/pwn/reports/sarif.rb +60 -0
- data/lib/pwn/reports.rb +1 -0
- data/lib/pwn/sessions.rb +22 -4
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +2 -1
- data/spec/lib/pwn/ai/agent/reward_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +2 -1
- data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +1 -0
- data/spec/lib/pwn/ai/agent/tools/verify_spec.rb +24 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +14 -0
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +45 -5
- data/spec/lib/pwn/plugins/findings_spec.rb +13 -1
- data/spec/lib/pwn/reports/sarif_spec.rb +28 -0
- data/third_party/pwn_rdoc.jsonl +28 -1
- metadata +9 -2
- data/etc/intel/files_exploits.csv +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c9afec4ab615d169982f573cf42ccd571c2593eec77e5905c131adf0bfb7660
|
|
4
|
+
data.tar.gz: 12d38fd2f938a1d6a68dc0418da107207297d23e0bd4d6a4561e9080f943875b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfceaf016937af5fe50f5a47b3e4b23bda9c9dc3395c602bd17d36bea8dc9e18eec1b0372d11aea34fe5f856ce323b02368b31094ca28817b5a5beedacae89e7
|
|
7
|
+
data.tar.gz: 3896ef7de35eae9e17c043ea36b1f9258db192f776ee977aa37dde2f5470cd1441109caea605a631bea5ca7a5446aa3a231db779dd83423a003793989c55eca9
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwn-ai-agent-tools-verify
|
|
3
|
+
description: Drive PWN::Ai::Agent::Tools::Verify 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::Verify
|
|
10
|
+
source: pwn/ai/agent/tools/verify.rb
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PWN::Ai::Agent::Tools::Verify
|
|
14
|
+
|
|
15
|
+
Public API for PWN::Ai::Agent::Tools::Verify.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Call `PWN::Ai::Agent::Tools::Verify` 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/verify.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::Verify.help
|
|
31
|
+
PWN::Ai::Agent::Tools::Verify.help(opts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Public methods
|
|
35
|
+
|
|
36
|
+
- _(no public class methods parsed)_
|
|
37
|
+
|
|
38
|
+
## Source
|
|
39
|
+
|
|
40
|
+
`pwn/ai/agent/tools/verify.rb`
|
|
41
|
+
|
|
42
|
+
## Verification
|
|
43
|
+
|
|
44
|
+
`PWN::Ai::Agent::Tools::Verify.respond_to?(:help)` after the
|
|
45
|
+
module is loaded. Read the source for parameter names.
|
|
@@ -36,9 +36,16 @@ PWN::Plugins::ExploitDB.required_bins(opts)
|
|
|
36
36
|
- `required_bins`
|
|
37
37
|
- `search`
|
|
38
38
|
- `cve_for_cpe`
|
|
39
|
+
- `gtfobins`
|
|
40
|
+
- `lookup`
|
|
41
|
+
- `sync`
|
|
39
42
|
- `authors`
|
|
40
43
|
- `help`
|
|
41
44
|
|
|
45
|
+
## References
|
|
46
|
+
|
|
47
|
+
- `references/urls.md` — URLs from source
|
|
48
|
+
|
|
42
49
|
## Source
|
|
43
50
|
|
|
44
51
|
`pwn/plugins/exploit_db.rb`
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pwn-reports-sarif
|
|
3
|
+
description: Drive PWN::Reports::SARIF from pwn_eval.
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: [pwn, pwn_eval]
|
|
6
|
+
metadata:
|
|
7
|
+
bundled: true
|
|
8
|
+
generated: true
|
|
9
|
+
module: PWN::Reports::SARIF
|
|
10
|
+
source: pwn/reports/sarif.rb
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PWN::Reports::SARIF
|
|
14
|
+
|
|
15
|
+
SARIF 2.1.0 writer for finding export.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
Call `PWN::Reports::SARIF` from `pwn_eval` when the task needs this module.
|
|
20
|
+
Do not reimplement it in shell.
|
|
21
|
+
|
|
22
|
+
## Methodologies
|
|
23
|
+
|
|
24
|
+
Generated from `pwn/reports/sarif.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::Reports::SARIF.help
|
|
31
|
+
PWN::Reports::SARIF.generate(opts)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Public methods
|
|
35
|
+
|
|
36
|
+
- `generate`
|
|
37
|
+
- `authors`
|
|
38
|
+
- `help`
|
|
39
|
+
|
|
40
|
+
## References
|
|
41
|
+
|
|
42
|
+
- `references/urls.md` — URLs from source
|
|
43
|
+
|
|
44
|
+
## Source
|
|
45
|
+
|
|
46
|
+
`pwn/reports/sarif.rb`
|
|
47
|
+
|
|
48
|
+
## Verification
|
|
49
|
+
|
|
50
|
+
`PWN::Reports::SARIF.respond_to?(:generate)` after the
|
|
51
|
+
module is loaded. Read the source for parameter names.
|
|
@@ -48,7 +48,7 @@ module PWN
|
|
|
48
48
|
args = ToolGuard.coerce_args(args: args, required: required) if defined?(ToolGuard)
|
|
49
49
|
if defined?(ToolGuard) && ToolGuard.respond_to?(:canary_leak?) &&
|
|
50
50
|
ToolGuard.canary_leak?(text: args.inspect)
|
|
51
|
-
return JSON.generate(success: false, error: 'refused: session canary in outbound args')
|
|
51
|
+
return JSON.generate(success: false, error: 'refused: session canary in outbound args', code: 'CANARY_DENY', rule_id: 'canary')
|
|
52
52
|
end
|
|
53
53
|
if defined?(ToolGuard) && ToolGuard.respond_to?(:refuse_copied_persist?) &&
|
|
54
54
|
ToolGuard.refuse_copied_persist?(name: entry.name, args: args)
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -660,24 +660,57 @@ module PWN
|
|
|
660
660
|
end
|
|
661
661
|
|
|
662
662
|
private_class_method def self.declared_contract_unsatisfied?(opts = {})
|
|
663
|
+
return false if verify_passed?(messages: opts[:messages])
|
|
664
|
+
return false if write_or_read_evidenced?(messages: opts[:messages])
|
|
665
|
+
|
|
666
|
+
completion_unmet(opts).any?
|
|
667
|
+
rescue StandardError
|
|
668
|
+
false
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
private_class_method def self.completion_unmet(opts = {})
|
|
672
|
+
return [] if verify_passed?(messages: opts[:messages])
|
|
673
|
+
return [] if write_or_read_evidenced?(messages: opts[:messages])
|
|
674
|
+
|
|
663
675
|
contract = declared_contract(request: opts[:request])
|
|
664
676
|
request = opts[:request].to_s
|
|
677
|
+
unmet = []
|
|
665
678
|
files = Array(contract[:paths]) + Array(contract[:proofs])
|
|
666
|
-
|
|
667
|
-
|
|
679
|
+
files.each { |path| unmet << "file_exists:#{path}" if deliverable_missing?(path: path) }
|
|
668
680
|
trace_files = session_files(messages: opts[:messages])
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return true if declared_skills_missing?(skills: contract[:skills], request: request)
|
|
672
|
-
|
|
681
|
+
unmet << 'issue_work_proofs' if contract[:issue_work] && Array(contract[:proofs]).empty? && files.empty? && trace_files.empty?
|
|
682
|
+
unmet << 'skills' if declared_skills_missing?(skills: contract[:skills], request: request)
|
|
673
683
|
blob = evidence_blob(messages: opts[:messages], files: files + trace_files)
|
|
674
684
|
asked_hosts = Array(contract[:hosts]).select { |host| request.downcase.include?(host.to_s.downcase) }
|
|
675
685
|
asked_tech = Array(contract[:techniques]).select { |tech| request.downcase.include?(tech.to_s.downcase) }
|
|
676
|
-
|
|
677
|
-
|
|
686
|
+
unmet << 'hosts' if evidence_tokens_missing?(tokens: asked_hosts, blob: blob)
|
|
687
|
+
unmet << 'techniques' if evidence_tokens_missing?(tokens: asked_tech, blob: blob)
|
|
688
|
+
unmet
|
|
689
|
+
rescue StandardError
|
|
690
|
+
[]
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
private_class_method def self.verify_passed?(opts = {})
|
|
694
|
+
Array(opts[:messages]).reverse_each do |msg|
|
|
695
|
+
next unless msg.is_a?(Hash) && msg[:role].to_s == 'tool'
|
|
696
|
+
|
|
697
|
+
raw = unwrap_untrusted(text: msg[:content].to_s)
|
|
698
|
+
parsed = JSON.parse(raw)
|
|
699
|
+
parsed = parsed['result'] || parsed[:result] || parsed
|
|
700
|
+
next unless parsed.is_a?(Hash)
|
|
678
701
|
|
|
702
|
+
return true if parsed[:passed] == true || parsed['passed'] == true
|
|
703
|
+
rescue StandardError
|
|
704
|
+
next
|
|
705
|
+
end
|
|
679
706
|
false
|
|
680
|
-
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
private_class_method def self.write_or_read_evidenced?(opts = {})
|
|
710
|
+
effects = tool_effects(messages: opts[:messages])
|
|
711
|
+
files = session_files(messages: opts[:messages])
|
|
712
|
+
return true if effects.intersect?(%i[write read]) && files.any?
|
|
713
|
+
|
|
681
714
|
false
|
|
682
715
|
end
|
|
683
716
|
|
|
@@ -2971,11 +3004,12 @@ module PWN
|
|
|
2971
3004
|
text: text
|
|
2972
3005
|
)
|
|
2973
3006
|
turn_fails['unsatisfied'] += 1
|
|
2974
|
-
|
|
2975
|
-
|
|
3007
|
+
unmet = completion_unmet(request: request, messages: messages)
|
|
3008
|
+
warn "[pwn-ai/loop] original request not evidenced on iter=#{i} unmet=#{unmet.join(',')}; continuing"
|
|
3009
|
+
debug_progress(msg: "bounce unsatisfied unmet=#{unmet.join(',')} snippet=#{debug_snippet(text: text)}")
|
|
2976
3010
|
messages << {
|
|
2977
3011
|
role: 'user',
|
|
2978
|
-
content:
|
|
3012
|
+
content: "[pwn-ai] The original request is not evidenced yet. unmet=#{unmet.join(',')} " \
|
|
2979
3013
|
'Keep calling CORE_TOOLS (shell, pwn_eval) until that request is ' \
|
|
2980
3014
|
'done or a tool returned failure evidence. pwn-ai does not decide ' \
|
|
2981
3015
|
'authorization. Do not declare completion from a listing or a refusal.'
|
|
@@ -112,7 +112,10 @@ module PWN
|
|
|
112
112
|
|
|
113
113
|
public_class_method def self.error_class(opts = {})
|
|
114
114
|
e = opts[:error].to_s
|
|
115
|
-
return '
|
|
115
|
+
return 'docker_daemon' if e.match?(/cannot connect to the docker daemon|docker daemon|is the docker daemon running/i)
|
|
116
|
+
return 'docker_registry_auth' if e.match?(/unauthorized: authentication required|pull access denied/i)
|
|
117
|
+
return 'oom' if e.match?(/\boom\b|out of memory|cannot allocate memory/i)
|
|
118
|
+
return 'auth_denied' if e.match?(/access denied|unauthorized|not authorized|\b401\b|\b403\b/i)
|
|
116
119
|
return 'name_conflict' if e.match?(/already (?:in use|exists)|name conflict|Conflict\.|duplicate/i)
|
|
117
120
|
return 'parse_error' if e.match?(/parse error|parsererror|template.*error|unexpected token/i)
|
|
118
121
|
return 'socket_perm' if e.match?(/docker\.sock|unix(?:\s+|:)socket|eperm.*sock|sock.*permission/i)
|
|
@@ -873,7 +876,7 @@ module PWN
|
|
|
873
876
|
error: 'required - raw error text (will be normalised)'
|
|
874
877
|
)
|
|
875
878
|
|
|
876
|
-
# Classify an error into auth_denied/name_conflict/parse_error/socket_perm/missing_path/net_unreach/perm_denied/timeout/other.
|
|
879
|
+
# Classify an error into docker_daemon/docker_registry_auth/oom/auth_denied/name_conflict/parse_error/socket_perm/missing_path/net_unreach/perm_denied/timeout/other.
|
|
877
880
|
#{self}.error_class(
|
|
878
881
|
error: 'required - raw error text'
|
|
879
882
|
)
|
|
@@ -201,10 +201,21 @@ 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
|
+
jobs = if defined?(PWN::Plugins::Jobs)
|
|
205
|
+
run = Array(PWN::Plugins::Jobs.list).select { |j| j[:alive] }
|
|
206
|
+
run.empty? ? '' : " JOBS running=#{run.map { |j| j[:id] }.join(',')}"
|
|
207
|
+
else
|
|
208
|
+
''
|
|
209
|
+
end
|
|
210
|
+
facts = (PWN::AI::Agent::Swarm.facts_prompt if defined?(PWN::AI::Agent::Swarm))
|
|
211
|
+
rev = [
|
|
212
|
+
(File.mtime(PWN::AI::Agent::Mistakes::MISTAKES_FILE).to_i if defined?(PWN::AI::Agent::Mistakes::MISTAKES_FILE) && File.file?(PWN::AI::Agent::Mistakes::MISTAKES_FILE)),
|
|
213
|
+
(File.mtime(PWN::AI::Agent::Learning::LESSONS_FILE).to_i if defined?(PWN::AI::Agent::Learning::LESSONS_FILE) && File.file?(PWN::AI::Agent::Learning::LESSONS_FILE))
|
|
214
|
+
].compact.max
|
|
204
215
|
canary = (Thread.current[:pwn_canary] || ToolGuard.mint_canary if defined?(ToolGuard) && ToolGuard.respond_to?(:mint_canary))
|
|
205
216
|
"#{load_line} pwn_eval/shell timeout = conservative seconds for this host " \
|
|
206
217
|
"(defaults eval=#{eval_s || 20}s shell=#{shell_s || 30}s, clamped). " \
|
|
207
|
-
"LIVING OFF THE LAND #{land} #{doc} " \
|
|
218
|
+
"LIVING OFF THE LAND #{land} #{doc}#{jobs} #{facts} LESSONS_REV #{rev} " \
|
|
208
219
|
"CANARY #{canary} never copy this token into tool args. " \
|
|
209
220
|
'DOMAIN TOOLS for pentest/RE: binary_triage, pty_open, job_run, exploitdev, fuzz_campaign, finding_record, decompile.'
|
|
210
221
|
rescue StandardError
|
data/lib/pwn/ai/agent/reward.rb
CHANGED
|
@@ -188,6 +188,20 @@ module PWN
|
|
|
188
188
|
)
|
|
189
189
|
v[:needs_spot_check] = v[:success] && v[:score].to_f >= 0.85 && (rand < 0.05)
|
|
190
190
|
v[:engine] = eng
|
|
191
|
+
pass = final.match?(/\bPASS\b/) && !(defined?(Learning) && final.match?(Learning::FAILURE_FINAL_RX))
|
|
192
|
+
v[:task_class] = request.match?(/analy[sz]e|summar|strength|weakness|fitness/i) ? 'analysis' : 'operational'
|
|
193
|
+
if pass
|
|
194
|
+
v[:score] = [v[:score].to_f, 0.7].max
|
|
195
|
+
v[:score] = [v[:score].to_f, 0.70].min
|
|
196
|
+
end
|
|
197
|
+
v[:score_components] ||= {
|
|
198
|
+
judge: v[:score].to_f,
|
|
199
|
+
overlap: pass ? 0.0 : nil,
|
|
200
|
+
checks: 0.0,
|
|
201
|
+
weights: { overlap: pass ? 0.0 : 0.15 }
|
|
202
|
+
}
|
|
203
|
+
v[:score_components][:weights][:overlap] = 0.0 if pass
|
|
204
|
+
Learning.note_outcome(task: request[0, 80], success: v[:score].to_f >= 0.6, score: v[:score], details: v[:score_components].to_json) if commit && defined?(Learning) && opts[:persist_components]
|
|
191
205
|
# W3 — write Brier on every judged turn so overconfidence can
|
|
192
206
|
# throttle max_iters/critic even when plan_first never fired.
|
|
193
207
|
if commit
|
|
@@ -1332,14 +1346,15 @@ module PWN
|
|
|
1332
1346
|
req_toks = request.downcase.scan(/[a-z0-9_]{3,}/).uniq
|
|
1333
1347
|
fin_toks = final.downcase.scan(/[a-z0-9_]{3,}/).uniq
|
|
1334
1348
|
overlap = req_toks.empty? ? 1.0 : (req_toks & fin_toks).length.to_f / req_toks.length
|
|
1349
|
+
pass = final.match?(/\bPASS\b/) && !(defined?(Learning) && final.match?(Learning::FAILURE_FINAL_RX))
|
|
1335
1350
|
long_analytical = final.length >= 800
|
|
1336
|
-
score = [score, 0.35].min if overlap < 0.08 && req_toks.length >= 4 && score > 0.35 && !long_analytical
|
|
1351
|
+
score = [score, 0.35].min if !pass && overlap < 0.08 && req_toks.length >= 4 && score > 0.35 && !long_analytical
|
|
1337
1352
|
ev_score = ev ? ev[:score].to_f : 0.0
|
|
1338
1353
|
bad = trace.count { |t| !semantic_ok(name: 'shell', raw: t.to_s)[:semantic_ok] }
|
|
1339
1354
|
ratio = trace.empty? ? 0.5 : 1.0 - (bad.to_f / trace.length)
|
|
1340
1355
|
score = ((score * 0.85) + (ratio * 0.15)).round(3)
|
|
1341
|
-
score = [score, 0.
|
|
1342
|
-
score = [score, 0.45].min if overlap >= 0.4 && ev_score < 0.55
|
|
1356
|
+
score = [score, 0.7].max if pass
|
|
1357
|
+
score = [score, 0.45].min if !pass && overlap >= 0.4 && ev_score < 0.55
|
|
1343
1358
|
score = [score, 0.45].min if ratio <= 0.15
|
|
1344
1359
|
score = [score, 0.70].min
|
|
1345
1360
|
score = score.round(2).clamp(0.0, 0.70)
|
|
@@ -1348,7 +1363,19 @@ module PWN
|
|
|
1348
1363
|
else :wrong
|
|
1349
1364
|
end
|
|
1350
1365
|
rationale = "heuristic evidence=#{ev ? ev[:score] : '-'} overlap=#{overlap.round(2)} ratio=#{ratio.round(2)}"
|
|
1351
|
-
{
|
|
1366
|
+
{
|
|
1367
|
+
score: score,
|
|
1368
|
+
verdict: verdict,
|
|
1369
|
+
rationale: rationale,
|
|
1370
|
+
key_step: -1,
|
|
1371
|
+
source: :heuristic,
|
|
1372
|
+
score_components: {
|
|
1373
|
+
judge: score,
|
|
1374
|
+
overlap: pass ? 0.0 : overlap.round(3),
|
|
1375
|
+
checks: 0.0,
|
|
1376
|
+
weights: { overlap: pass ? 0.0 : 0.15 }
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1352
1379
|
end
|
|
1353
1380
|
|
|
1354
1381
|
private_class_method def self.heuristic_prm(opts = {})
|
|
@@ -1742,7 +1769,8 @@ module PWN
|
|
|
1742
1769
|
commit: 'optional - write score into learning.jsonl / sentinel (default true)',
|
|
1743
1770
|
critic_pass: 'optional - critic pass value consumed by #judge',
|
|
1744
1771
|
predicted: 'optional - predicted value consumed by #judge',
|
|
1745
|
-
proxy_ok: 'optional - proxy ok value consumed by #judge'
|
|
1772
|
+
proxy_ok: 'optional - proxy ok value consumed by #judge',
|
|
1773
|
+
persist_components: 'optional - write score_components into the learning ledger'
|
|
1746
1774
|
)
|
|
1747
1775
|
|
|
1748
1776
|
# Run promote to success and return its result
|
data/lib/pwn/ai/agent/swarm.rb
CHANGED
|
@@ -407,6 +407,37 @@ module PWN
|
|
|
407
407
|
end.map(&:value)
|
|
408
408
|
end
|
|
409
409
|
|
|
410
|
+
public_class_method def self.fact_record(opts = {})
|
|
411
|
+
eng = (opts[:engagement_id] || 'default').to_s
|
|
412
|
+
dir = File.join(Dir.home, '.pwn', 'engagements', eng)
|
|
413
|
+
FileUtils.mkdir_p(dir)
|
|
414
|
+
path = File.join(dir, 'facts.jsonl')
|
|
415
|
+
row = {
|
|
416
|
+
kind: (opts[:kind] || 'host').to_s,
|
|
417
|
+
value: opts[:value],
|
|
418
|
+
source_session: opts[:source_session].to_s,
|
|
419
|
+
confidence: (opts[:confidence] || 0.7).to_f,
|
|
420
|
+
at: Time.now.utc.iso8601
|
|
421
|
+
}
|
|
422
|
+
File.open(path, 'a') { |f| f.puts(JSON.generate(row)) }
|
|
423
|
+
row.merge(path: path)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
public_class_method def self.facts_prompt(opts = {})
|
|
427
|
+
eng = (opts[:engagement_id] || 'default').to_s
|
|
428
|
+
path = File.join(Dir.home, '.pwn', 'engagements', eng, 'facts.jsonl')
|
|
429
|
+
return '' unless File.file?(path)
|
|
430
|
+
|
|
431
|
+
rows = File.readlines(path).filter_map do |ln|
|
|
432
|
+
JSON.parse(ln, symbolize_names: true)
|
|
433
|
+
rescue JSON::ParserError
|
|
434
|
+
nil
|
|
435
|
+
end
|
|
436
|
+
return '' if rows.empty?
|
|
437
|
+
|
|
438
|
+
"FACTS #{rows.map { |r| "#{r[:kind]}=#{r[:value]}" }.join(' ')}"
|
|
439
|
+
end
|
|
440
|
+
|
|
410
441
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
411
442
|
|
|
412
443
|
public_class_method def self.authors
|
|
@@ -490,6 +521,20 @@ module PWN
|
|
|
490
521
|
ports: 'optional - comma/space separated ports (defaults to 80,443)'
|
|
491
522
|
)
|
|
492
523
|
|
|
524
|
+
# Append a typed engagement fact (port, cred, host, vuln).
|
|
525
|
+
#{self}.fact_record(
|
|
526
|
+
engagement_id: 'optional - engagement id (defaults to default)',
|
|
527
|
+
kind: 'optional - port|cred|host|vuln (defaults to host)',
|
|
528
|
+
value: 'required - fact value',
|
|
529
|
+
source_session: 'optional - session id that discovered the fact',
|
|
530
|
+
confidence: 'optional - 0.0..1.0 (defaults to 0.7)'
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# One-line FACTS block for the system prompt.
|
|
534
|
+
#{self}.facts_prompt(
|
|
535
|
+
engagement_id: 'optional - engagement id (defaults to default)'
|
|
536
|
+
)
|
|
537
|
+
|
|
493
538
|
# Print the AUTHOR(S) string for this module.
|
|
494
539
|
#{self}.authors
|
|
495
540
|
"
|
|
@@ -4,6 +4,7 @@ require 'digest'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
require 'securerandom'
|
|
7
|
+
require 'time'
|
|
7
8
|
|
|
8
9
|
module PWN
|
|
9
10
|
module AI
|
|
@@ -163,16 +164,31 @@ module PWN
|
|
|
163
164
|
|
|
164
165
|
public_class_method def self.invalid_payload(opts = {})
|
|
165
166
|
hint = opts[:hint].to_s
|
|
167
|
+
tok = opts[:offending_token].to_s
|
|
166
168
|
{
|
|
167
169
|
stdout: '',
|
|
168
170
|
stderr: hint,
|
|
169
171
|
exit: 2,
|
|
170
172
|
error: 'invalid_payload',
|
|
173
|
+
code: (opts[:code] || 'SYNTAX_DENY').to_s,
|
|
174
|
+
rule_id: (opts[:rule_id] || 'payload').to_s,
|
|
175
|
+
offending_token: tok,
|
|
176
|
+
suggestion: opts[:suggestion].to_s,
|
|
171
177
|
hint: hint,
|
|
172
178
|
shell: opts[:shell] || shell_name
|
|
173
179
|
}
|
|
174
180
|
end
|
|
175
181
|
|
|
182
|
+
public_class_method def self.denial(opts = {})
|
|
183
|
+
{
|
|
184
|
+
code: opts[:code].to_s,
|
|
185
|
+
rule_id: opts[:rule_id].to_s,
|
|
186
|
+
token: opts[:token].to_s,
|
|
187
|
+
offending_token: opts[:offending_token] || opts[:token].to_s,
|
|
188
|
+
suggestion: opts[:suggestion].to_s
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
176
192
|
public_class_method def self.host_load(opts = {})
|
|
177
193
|
return { ncpu: 1, load1: 0.0, mem_avail_mb: 0 } unless opts.is_a?(Hash)
|
|
178
194
|
|
|
@@ -407,8 +423,11 @@ module PWN
|
|
|
407
423
|
scope = YAML.safe_load_file(path, permitted_classes: [Symbol]) || {}
|
|
408
424
|
return nil if scope.nil? || scope.empty?
|
|
409
425
|
|
|
410
|
-
|
|
411
|
-
|
|
426
|
+
expiry = (scope['expiry'] || scope[:expiry]).to_s
|
|
427
|
+
return denial(code: 'SCOPE_DENY', rule_id: 'expiry', token: expiry, suggestion: 'renew ~/.pwn/scope.yaml expiry') unless expiry.empty? || Time.parse(expiry) >= Time.now
|
|
428
|
+
|
|
429
|
+
allow = Array(scope['cidr_allowlist'] || scope[:cidr_allowlist] || scope['cidrs'] || scope[:cidrs]).map(&:to_s)
|
|
430
|
+
domains = Array(scope['domain_allowlist'] || scope[:domain_allowlist] || scope['domains'] || scope[:domains]).map(&:to_s)
|
|
412
431
|
return nil if allow.empty? && domains.empty?
|
|
413
432
|
|
|
414
433
|
ips = cmd.scan(/\b\d{1,3}(?:\.\d{1,3}){3}\b/)
|
|
@@ -418,7 +437,7 @@ module PWN
|
|
|
418
437
|
hit = bad_ip || bad_host
|
|
419
438
|
return nil unless hit
|
|
420
439
|
|
|
421
|
-
|
|
440
|
+
denial(code: 'SCOPE_DENY', rule_id: 'allowlist', token: hit, suggestion: 'use an in-scope host or CIDR')
|
|
422
441
|
rescue StandardError
|
|
423
442
|
nil
|
|
424
443
|
end
|
|
@@ -552,10 +571,23 @@ module PWN
|
|
|
552
571
|
required: 'optional - Array required value consumed by #coerce_args'
|
|
553
572
|
)
|
|
554
573
|
|
|
555
|
-
#
|
|
574
|
+
# Build a machine-readable invalid_payload denial (SYNTAX_DENY by default).
|
|
556
575
|
#{self}.invalid_payload(
|
|
557
|
-
hint: 'optional - hint
|
|
558
|
-
shell: 'optional - shell
|
|
576
|
+
hint: 'optional - operator-facing hint string',
|
|
577
|
+
shell: 'optional - shell name (defaults to shell_name)',
|
|
578
|
+
code: 'optional - denial code (defaults to SYNTAX_DENY)',
|
|
579
|
+
rule_id: 'optional - rule identifier (defaults to payload)',
|
|
580
|
+
offending_token: 'optional - exact rejected token span',
|
|
581
|
+
suggestion: 'optional - how to rewrite the payload'
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Build a machine-readable guard denial (SCOPE_DENY, CANARY_DENY, ...).
|
|
585
|
+
#{self}.denial(
|
|
586
|
+
code: 'required - denial code such as SCOPE_DENY',
|
|
587
|
+
rule_id: 'optional - rule identifier',
|
|
588
|
+
token: 'optional - out-of-scope host or CIDR',
|
|
589
|
+
offending_token: 'optional - exact rejected token span',
|
|
590
|
+
suggestion: 'optional - how to stay in scope'
|
|
559
591
|
)
|
|
560
592
|
|
|
561
593
|
# Run host load and return its result
|
|
@@ -17,21 +17,42 @@ PWN::AI::Agent::Registry.register(
|
|
|
17
17
|
evidence: { type: 'string' },
|
|
18
18
|
poc: { type: 'string' },
|
|
19
19
|
poc_artifacts: { type: 'array', items: { type: 'string' } },
|
|
20
|
-
session_id: { type: 'string' }
|
|
20
|
+
session_id: { type: 'string' },
|
|
21
|
+
op: { type: 'string' },
|
|
22
|
+
parent_id: { type: 'string' },
|
|
23
|
+
engagement_id: { type: 'string' }
|
|
21
24
|
},
|
|
22
|
-
required: %w[
|
|
25
|
+
required: %w[]
|
|
23
26
|
}
|
|
24
27
|
},
|
|
25
28
|
handler: lambda { |args|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
host: args[:host] || args['host']
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
op = (args[:op] || args['op'] || 'record').to_s
|
|
30
|
+
case op
|
|
31
|
+
when 'query'
|
|
32
|
+
PWN::Plugins::Findings.query(host: args[:host] || args['host'])
|
|
33
|
+
when 'chain'
|
|
34
|
+
PWN::Plugins::Findings.chain(
|
|
35
|
+
parent_id: args[:parent_id] || args['parent_id'],
|
|
36
|
+
title: args[:title] || args['title'],
|
|
37
|
+
severity: args[:severity] || args['severity'],
|
|
38
|
+
poc_artifacts: args[:poc_artifacts] || args['poc_artifacts'],
|
|
39
|
+
poc: args[:poc] || args['poc'],
|
|
40
|
+
session_id: args[:session_id] || args['session_id']
|
|
41
|
+
)
|
|
42
|
+
when 'export'
|
|
43
|
+
PWN::Plugins::Findings.render(report_name: 'findings')
|
|
44
|
+
else
|
|
45
|
+
PWN::Plugins::Findings.record(
|
|
46
|
+
title: args[:title] || args['title'],
|
|
47
|
+
severity: args[:severity] || args['severity'],
|
|
48
|
+
host: args[:host] || args['host'],
|
|
49
|
+
evidence: args[:evidence] || args['evidence'],
|
|
50
|
+
poc: args[:poc] || args['poc'],
|
|
51
|
+
poc_artifacts: args[:poc_artifacts] || args['poc_artifacts'],
|
|
52
|
+
session_id: args[:session_id] || args['session_id'],
|
|
53
|
+
engagement_id: args[:engagement_id] || args['engagement_id']
|
|
54
|
+
)
|
|
55
|
+
end
|
|
35
56
|
}
|
|
36
57
|
)
|
|
37
58
|
PWN::AI::Agent::Registry.register(
|
|
@@ -11,7 +11,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
11
11
|
parameters: {
|
|
12
12
|
type: 'object',
|
|
13
13
|
properties: {
|
|
14
|
-
action: { type: 'string', description: 'start|status|stop|triage' },
|
|
14
|
+
action: { type: 'string', description: 'start|status|stop|triage|minimize' },
|
|
15
15
|
in_dir: { type: 'string' },
|
|
16
16
|
out_dir: { type: 'string' },
|
|
17
17
|
target: { type: 'string' },
|
|
@@ -37,6 +37,8 @@ PWN::AI::Agent::Registry.register(
|
|
|
37
37
|
PWN::Plugins::Jobs.stop(id: args[:id] || args['id'])
|
|
38
38
|
when 'triage'
|
|
39
39
|
PWN::Plugins::AFLplusplus.crash_triage(out_dir: args[:out_dir] || args['out_dir'])
|
|
40
|
+
when 'minimize'
|
|
41
|
+
PWN::Plugins::AFLplusplus.minimize(crash: args[:crash] || args['crash'], out: args[:out] || args['out'], target: args[:target] || args['target'])
|
|
40
42
|
else
|
|
41
43
|
{ error: "unknown action #{act}" }
|
|
42
44
|
end
|