pwn 0.5.615 → 0.5.618
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/README.md +109 -85
- data/README.md.bak +200 -0
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +42 -26
- data/documentation/AWS.md +57 -0
- data/documentation/Agent-Tool-Registry.md +56 -0
- data/documentation/Banner.md +17 -0
- data/documentation/Blockchain.md +18 -0
- data/documentation/Bounty.md +21 -0
- data/documentation/BurpSuite.md +41 -16
- data/documentation/CLI-Drivers.md +58 -0
- data/documentation/Configuration.md +66 -0
- data/documentation/Contributing.md +33 -19
- data/documentation/Cron.md +47 -0
- data/documentation/Diagrams.md +123 -40
- data/documentation/Drivers.md +43 -16
- data/documentation/Extrospection.md +54 -0
- data/documentation/FFI.md +14 -0
- data/documentation/Fuzzing.md +36 -0
- data/documentation/General-PWN-Usage.md +50 -30
- data/documentation/Hardware.md +40 -0
- data/documentation/Home.md +78 -51
- data/documentation/How-PWN-Works.md +59 -57
- data/documentation/Installation.md +53 -28
- data/documentation/Metasploit.md +34 -0
- data/documentation/NmapIt.md +18 -12
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/Persistence.md +38 -0
- data/documentation/Plugins.md +97 -47
- data/documentation/Reporting.md +25 -18
- data/documentation/SAST.md +39 -22
- data/documentation/SDR.md +40 -0
- data/documentation/Sessions.md +39 -0
- data/documentation/Skills-Memory-Learning.md +49 -29
- data/documentation/Swarm.md +71 -0
- data/documentation/Transparent-Browser.md +26 -22
- data/documentation/Troubleshooting.md +44 -25
- data/documentation/WWW.md +32 -0
- data/documentation/What-is-PWN.md +51 -32
- data/documentation/Why-PWN.md +41 -11
- data/documentation/diagrams/agent-tool-registry.svg +284 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +141 -103
- data/documentation/diagrams/aws-cloud-security.svg +166 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +47 -62
- data/documentation/diagrams/code-scanning-sast.svg +139 -79
- data/documentation/diagrams/cron-scheduling.svg +148 -0
- data/documentation/diagrams/dot/_THEME.md +27 -0
- data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
- data/documentation/diagrams/dot/ai-integration-tool-calling.dot +47 -20
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +23 -12
- data/documentation/diagrams/dot/code-scanning-sast.dot +46 -18
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +36 -14
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +44 -24
- data/documentation/diagrams/dot/hardware-hacking.dot +48 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +31 -17
- data/documentation/diagrams/dot/memory-skills-detailed.dot +57 -25
- data/documentation/diagrams/dot/network-infra-testing.dot +51 -19
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +110 -44
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +56 -25
- data/documentation/diagrams/dot/persistence-filesystem.dot +29 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +41 -27
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +87 -46
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +49 -30
- data/documentation/diagrams/dot/reporting-pipeline.dot +43 -14
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +40 -18
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +41 -14
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +45 -23
- data/documentation/diagrams/dot/zero-day-research-flow.dot +46 -24
- data/documentation/diagrams/driver-framework.svg +87 -66
- data/documentation/diagrams/extrospection-world-awareness.svg +185 -0
- data/documentation/diagrams/fuzzing-workflow.svg +128 -115
- data/documentation/diagrams/hardware-hacking.svg +163 -0
- data/documentation/diagrams/history-to-drivers.svg +84 -82
- data/documentation/diagrams/memory-skills-detailed.svg +167 -119
- data/documentation/diagrams/network-infra-testing.svg +146 -84
- data/documentation/diagrams/overall-pwn-architecture.svg +356 -185
- data/documentation/diagrams/penetration-testing-workflow.svg +161 -108
- data/documentation/diagrams/persistence-filesystem.svg +174 -0
- data/documentation/diagrams/plugin-ecosystem.svg +225 -121
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +257 -185
- data/documentation/diagrams/pwn-repl-prototyping.svg +126 -137
- data/documentation/diagrams/reporting-pipeline.svg +128 -68
- data/documentation/diagrams/reverse-engineering-flow.svg +100 -80
- data/documentation/diagrams/sdr-radio-flow.svg +146 -0
- data/documentation/diagrams/sessions-cron-automation.svg +112 -68
- data/documentation/diagrams/swarm-multi-agent.svg +225 -0
- data/documentation/diagrams/web-application-testing.svg +136 -100
- data/documentation/diagrams/zero-day-research-flow.svg +116 -112
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/pwn-REPL.md +40 -24
- data/documentation/pwn-ai-Agent.md +59 -30
- data/documentation/pwn_android_war_dialer_session.png +0 -0
- data/documentation/pwn_install.png +0 -0
- data/documentation/pwn_wallpaper.jpg +0 -0
- data/documentation/ringing-spectrogram.png +0 -0
- data/documentation/ringing-waveform.png +0 -0
- data/git_commit.sh +1 -1
- data/lib/pwn/ai/agent/assembly.rb +1 -1
- data/lib/pwn/ai/agent/btc.rb +1 -1
- data/lib/pwn/ai/agent/burp_suite.rb +1 -1
- data/lib/pwn/ai/agent/extrospection.rb +618 -0
- data/lib/pwn/ai/agent/gqrx.rb +2 -2
- data/lib/pwn/ai/agent/hacker_one.rb +1 -1
- data/lib/pwn/ai/agent/introspection.rb +91 -0
- data/lib/pwn/ai/agent/learning.rb +6 -4
- data/lib/pwn/ai/agent/loop.rb +15 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +10 -1
- data/lib/pwn/ai/agent/sast.rb +1 -1
- data/lib/pwn/ai/agent/swarm.rb +437 -0
- data/lib/pwn/ai/agent/tools/cron.rb +163 -0
- data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
- data/lib/pwn/ai/agent/tools/learning.rb +108 -0
- data/lib/pwn/ai/agent/tools/memory.rb +27 -0
- data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
- data/lib/pwn/ai/agent/tools/skills.rb +30 -0
- data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
- data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
- data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
- data/lib/pwn/ai/agent.rb +3 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +10 -23
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/repl.rb +90 -281
- data/lib/pwn/sdr/decoder/base.rb +251 -0
- data/lib/pwn/sdr/decoder/gsm.rb +84 -185
- data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
- data/lib/pwn/sdr/decoder.rb +1 -0
- data/lib/pwn/sdr/gqrx.rb +446 -65
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
- data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
- data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
- data/third_party/pwn_rdoc.jsonl +199 -37
- metadata +65 -3
- data/lib/pwn/ai/introspection.rb +0 -76
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pwn/ai/agent/registry'
|
|
4
|
+
require 'pwn/sessions'
|
|
5
|
+
|
|
6
|
+
# Thin wrappers around PWN::Sessions so the model can DISCOVER and INSPECT
|
|
7
|
+
# the JSONL transcripts that learning_reflect / learning_distill_skill
|
|
8
|
+
# consume. Without these the model has to blindly `shell("ls ~/.pwn/sessions")`
|
|
9
|
+
# and guess a session_id — this closes that gap in the learning loop.
|
|
10
|
+
|
|
11
|
+
PWN::AI::Agent::Registry.register(
|
|
12
|
+
name: 'sessions_list',
|
|
13
|
+
toolset: 'sessions',
|
|
14
|
+
schema: {
|
|
15
|
+
name: 'sessions_list',
|
|
16
|
+
description: 'List every pwn-ai session transcript in ~/.pwn/sessions ' \
|
|
17
|
+
'(id, path, size_bytes, mtime, line_count) newest-first. ' \
|
|
18
|
+
'Use to discover a session_id for learning_reflect / ' \
|
|
19
|
+
'learning_distill_skill / sessions_view.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
limit: { type: 'integer', default: 25, description: 'Max sessions to return (newest first).' }
|
|
24
|
+
},
|
|
25
|
+
required: []
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
check: -> { defined?(PWN::Sessions) },
|
|
29
|
+
handler: lambda { |args|
|
|
30
|
+
limit = (args[:limit] || 25).to_i
|
|
31
|
+
PWN::Sessions.list.sort_by { |s| s[:mtime].to_s }.reverse.first(limit)
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
PWN::AI::Agent::Registry.register(
|
|
36
|
+
name: 'sessions_view',
|
|
37
|
+
toolset: 'sessions',
|
|
38
|
+
schema: {
|
|
39
|
+
name: 'sessions_view',
|
|
40
|
+
description: 'Load a session transcript by id and return its entries ' \
|
|
41
|
+
'(role, timestamp, truncated content). Inspect BEFORE ' \
|
|
42
|
+
'calling learning_reflect / learning_distill_skill on it.',
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
session_id: { type: 'string', description: 'PWN::Sessions id (basename without .jsonl).' },
|
|
47
|
+
max_entries: { type: 'integer', default: 200, description: 'Cap on transcript entries returned.' },
|
|
48
|
+
truncate: { type: 'integer', default: 400, description: 'Chars to keep per entry content.' }
|
|
49
|
+
},
|
|
50
|
+
required: %w[session_id]
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
check: -> { defined?(PWN::Sessions) },
|
|
54
|
+
handler: lambda { |args|
|
|
55
|
+
sid = args[:session_id].to_s
|
|
56
|
+
cap = (args[:max_entries] || 200).to_i
|
|
57
|
+
trunc = (args[:truncate] || 400).to_i
|
|
58
|
+
rows = PWN::Sessions.load(session_id: sid)
|
|
59
|
+
raise ArgumentError, "no such session: #{sid}" if rows.empty?
|
|
60
|
+
|
|
61
|
+
entries = rows.first(cap).map do |e|
|
|
62
|
+
{
|
|
63
|
+
role: e[:role],
|
|
64
|
+
timestamp: e[:timestamp],
|
|
65
|
+
content: e[:content].to_s[0, trunc]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
{ session_id: sid, total_entries: rows.length, returned: entries.length, entries: entries }
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
PWN::AI::Agent::Registry.register(
|
|
73
|
+
name: 'sessions_delete',
|
|
74
|
+
toolset: 'sessions',
|
|
75
|
+
schema: {
|
|
76
|
+
name: 'sessions_delete',
|
|
77
|
+
description: 'Delete a session transcript (~/.pwn/sessions/<id>.jsonl). ' \
|
|
78
|
+
'Use to prune noisy / failed / dev-experiment transcripts ' \
|
|
79
|
+
'so the reflect() corpus stays high-signal. Irreversible.',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
session_id: { type: 'string' }
|
|
84
|
+
},
|
|
85
|
+
required: %w[session_id]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
check: -> { defined?(PWN::Sessions) },
|
|
89
|
+
handler: lambda { |args|
|
|
90
|
+
sid = args[:session_id].to_s
|
|
91
|
+
raise ArgumentError, 'session_id is required' if sid.empty?
|
|
92
|
+
|
|
93
|
+
{ session_id: sid, deleted: PWN::Sessions.delete(session_id: sid) }
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
PWN::AI::Agent::Registry.register(
|
|
98
|
+
name: 'sessions_stats',
|
|
99
|
+
toolset: 'sessions',
|
|
100
|
+
schema: {
|
|
101
|
+
name: 'sessions_stats',
|
|
102
|
+
description: 'Aggregate stats over all session transcripts: total_sessions, ' \
|
|
103
|
+
'total_lines, oldest, newest, disk_bytes.',
|
|
104
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
105
|
+
},
|
|
106
|
+
check: -> { defined?(PWN::Sessions) },
|
|
107
|
+
handler: lambda { |_args|
|
|
108
|
+
listing = PWN::Sessions.list
|
|
109
|
+
PWN::Sessions.stats.merge(
|
|
110
|
+
disk_bytes: listing.sum { |s| s[:size].to_i },
|
|
111
|
+
dir: PWN::Sessions.sessions_dir
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
PWN::AI::Agent::Registry.register(
|
|
117
|
+
name: 'sessions_current',
|
|
118
|
+
toolset: 'sessions',
|
|
119
|
+
schema: {
|
|
120
|
+
name: 'sessions_current',
|
|
121
|
+
description: 'Return the ACTIVE pwn-ai session_id (the one this ' \
|
|
122
|
+
'conversation is being appended to). Pass it directly to ' \
|
|
123
|
+
'learning_reflect / learning_distill_skill / sessions_view ' \
|
|
124
|
+
'without copy-pasting from the system-prompt banner.',
|
|
125
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
126
|
+
},
|
|
127
|
+
check: -> { defined?(PWN::Sessions) },
|
|
128
|
+
handler: lambda { |_args|
|
|
129
|
+
sid = nil
|
|
130
|
+
sid = PWN::Env.dig(:ai, :session_id) if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
131
|
+
sid ||= (Pry.config.pwn_ai_session_id if defined?(Pry) && Pry.respond_to?(:config) && Pry.config.respond_to?(:pwn_ai_session_id))
|
|
132
|
+
path = sid ? File.join(PWN::Sessions.sessions_dir, "#{sid}.jsonl") : nil
|
|
133
|
+
{
|
|
134
|
+
session_id: sid,
|
|
135
|
+
path: path,
|
|
136
|
+
exists: (path && File.exist?(path)) || false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
@@ -160,3 +160,33 @@ PWN::AI::Agent::Registry.register(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
)
|
|
163
|
+
|
|
164
|
+
PWN::AI::Agent::Registry.register(
|
|
165
|
+
name: 'skill_delete',
|
|
166
|
+
toolset: 'skills',
|
|
167
|
+
schema: {
|
|
168
|
+
name: 'skill_delete',
|
|
169
|
+
description: 'Delete a skill from ~/.pwn/skills/ by name. Use to prune ' \
|
|
170
|
+
'low-quality auto-distilled skills so they stop appearing ' \
|
|
171
|
+
'in every future system prompt. Irreversible.',
|
|
172
|
+
parameters: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
name: { type: 'string', description: 'Existing skill name (basename without extension).' }
|
|
176
|
+
},
|
|
177
|
+
required: %w[name]
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
181
|
+
handler: lambda { |args|
|
|
182
|
+
key = args[:name].to_s.to_sym
|
|
183
|
+
meta = PWN::Skills[key]
|
|
184
|
+
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
185
|
+
|
|
186
|
+
path = meta[:path]
|
|
187
|
+
dir = File.dirname(path)
|
|
188
|
+
FileUtils.rm_f(path)
|
|
189
|
+
PWN::Config.load_skills(pwn_skills_path: dir)
|
|
190
|
+
{ deleted: true, name: key, path: path, remaining: PWN::Skills.keys.length }
|
|
191
|
+
}
|
|
192
|
+
)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pwn/ai/agent/registry'
|
|
4
|
+
require 'pwn/ai/agent/swarm'
|
|
5
|
+
|
|
6
|
+
# Multi-agent orchestration tools. These let the PRIMARY pwn-ai agent
|
|
7
|
+
# spawn/ask/debate SUB-agents (personas from ~/.pwn/agents.yml), each of
|
|
8
|
+
# which is a full PWN::AI::Agent::Loop.run — tool-calling, memory-aware,
|
|
9
|
+
# metrics-recorded, learning-reflected. This replaces the legacy pwn-irc
|
|
10
|
+
# "N chatbots on inspircd" mechanism with an in-process JSONL bus under
|
|
11
|
+
# ~/.pwn/swarm/<id>/, so conversations survive the process and can be
|
|
12
|
+
# resumed cross-session by any pwn-ai / PWN::Cron job.
|
|
13
|
+
|
|
14
|
+
PWN::AI::Agent::Registry.register(
|
|
15
|
+
name: 'agent_list',
|
|
16
|
+
toolset: 'swarm',
|
|
17
|
+
schema: {
|
|
18
|
+
name: 'agent_list',
|
|
19
|
+
description: 'List defined multi-agent personas from ~/.pwn/agents.yml ' \
|
|
20
|
+
'(name, role summary, engine, toolsets, max_iters). Use ' \
|
|
21
|
+
'before agent_ask / agent_debate to see who you can ' \
|
|
22
|
+
'delegate to. Define new ones with agent_spawn.',
|
|
23
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
24
|
+
},
|
|
25
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
26
|
+
handler: lambda { |_args|
|
|
27
|
+
PWN::AI::Agent::Swarm.personas.map do |name, p|
|
|
28
|
+
{
|
|
29
|
+
name: name,
|
|
30
|
+
role: p[:role].to_s[0, 200],
|
|
31
|
+
engine: p[:engine],
|
|
32
|
+
toolsets: p[:toolsets],
|
|
33
|
+
max_iters: p[:max_iters]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
PWN::AI::Agent::Registry.register(
|
|
40
|
+
name: 'agent_spawn',
|
|
41
|
+
toolset: 'swarm',
|
|
42
|
+
schema: {
|
|
43
|
+
name: 'agent_spawn',
|
|
44
|
+
description: 'Define (or overwrite) a persona in ~/.pwn/agents.yml so ' \
|
|
45
|
+
'it can be used with agent_ask / agent_debate. A persona ' \
|
|
46
|
+
'is a system-role overlay + toolset allow-list + engine ' \
|
|
47
|
+
'override. Omit "swarm" from toolsets to prevent that ' \
|
|
48
|
+
'persona from recursively spawning further sub-agents.',
|
|
49
|
+
parameters: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
name: { type: 'string', description: 'snake_case persona name.' },
|
|
53
|
+
role: { type: 'string', description: 'System-role overlay describing this persona.' },
|
|
54
|
+
toolsets: {
|
|
55
|
+
type: 'array', items: { type: 'string' },
|
|
56
|
+
description: 'Registry toolset names this persona may use ' \
|
|
57
|
+
'(e.g. terminal, pwn, memory, skills, extrospection). ' \
|
|
58
|
+
'Default: everything except swarm/cron.'
|
|
59
|
+
},
|
|
60
|
+
engine: {
|
|
61
|
+
type: 'string', enum: %w[openai anthropic grok gemini ollama],
|
|
62
|
+
description: 'Override AI engine for this persona (model diversity ' \
|
|
63
|
+
'= real antagonism). Default: inherit active engine.'
|
|
64
|
+
},
|
|
65
|
+
max_iters: {
|
|
66
|
+
type: 'integer',
|
|
67
|
+
description: 'Per-turn tool-loop cap for this persona (default 25).'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
required: %w[name role]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
74
|
+
handler: lambda { |args|
|
|
75
|
+
PWN::AI::Agent::Swarm.spawn(
|
|
76
|
+
name: args[:name],
|
|
77
|
+
role: args[:role],
|
|
78
|
+
toolsets: args[:toolsets],
|
|
79
|
+
engine: args[:engine],
|
|
80
|
+
max_iters: args[:max_iters]
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
PWN::AI::Agent::Registry.register(
|
|
86
|
+
name: 'agent_ask',
|
|
87
|
+
toolset: 'swarm',
|
|
88
|
+
schema: {
|
|
89
|
+
name: 'agent_ask',
|
|
90
|
+
description: 'Run ONE turn of a named persona as a full tool-calling ' \
|
|
91
|
+
'sub-agent (PWN::AI::Agent::Loop.run under that persona). ' \
|
|
92
|
+
'The reply comes back to YOU as a tool result. Request + ' \
|
|
93
|
+
'reply are appended to ~/.pwn/swarm/<swarm_id>/bus.jsonl so ' \
|
|
94
|
+
'other personas (and future sessions) can see them. ' \
|
|
95
|
+
'Recursion is depth-capped by PWN::Env[:ai][:agent][:max_depth].',
|
|
96
|
+
parameters: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
name: { type: 'string', description: 'Persona name (see agent_list).' },
|
|
100
|
+
request: { type: 'string', description: 'What to ask / instruct the persona.' },
|
|
101
|
+
swarm_id: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'Existing swarm to join (from swarm_list / a prior ' \
|
|
104
|
+
'agent_ask). Omit to auto-create a new swarm.'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: %w[name request]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
max_chars: 32_000,
|
|
111
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
112
|
+
handler: lambda { |args|
|
|
113
|
+
PWN::AI::Agent::Swarm.ask(
|
|
114
|
+
name: args[:name],
|
|
115
|
+
request: args[:request],
|
|
116
|
+
swarm_id: args[:swarm_id]
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
PWN::AI::Agent::Registry.register(
|
|
122
|
+
name: 'agent_debate',
|
|
123
|
+
toolset: 'swarm',
|
|
124
|
+
schema: {
|
|
125
|
+
name: 'agent_debate',
|
|
126
|
+
description: 'Round-robin an antagonistic debate between 2+ personas. ' \
|
|
127
|
+
'Each persona sees the swarm bus tail (prior turns) and is ' \
|
|
128
|
+
'asked to respond/critique/advance. Returns the full ' \
|
|
129
|
+
'transcript plus swarm_id so you can continue it later. ' \
|
|
130
|
+
'This is the native replacement for the legacy pwn-irc ' \
|
|
131
|
+
'multi-agent chat.',
|
|
132
|
+
parameters: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
names: {
|
|
136
|
+
type: 'array', items: { type: 'string' },
|
|
137
|
+
description: '>=2 persona names in speaking order.'
|
|
138
|
+
},
|
|
139
|
+
topic: { type: 'string', description: 'Opening question / claim / target.' },
|
|
140
|
+
rounds: { type: 'integer', default: 2, description: 'Full passes over names.' },
|
|
141
|
+
swarm_id: { type: 'string', description: 'Existing swarm to continue.' }
|
|
142
|
+
},
|
|
143
|
+
required: %w[names topic]
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
max_chars: 48_000,
|
|
147
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
148
|
+
handler: lambda { |args|
|
|
149
|
+
PWN::AI::Agent::Swarm.debate(
|
|
150
|
+
names: args[:names],
|
|
151
|
+
topic: args[:topic],
|
|
152
|
+
rounds: args[:rounds],
|
|
153
|
+
swarm_id: args[:swarm_id]
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
PWN::AI::Agent::Registry.register(
|
|
159
|
+
name: 'agent_broadcast',
|
|
160
|
+
toolset: 'swarm',
|
|
161
|
+
schema: {
|
|
162
|
+
name: 'agent_broadcast',
|
|
163
|
+
description: 'Fan the same request out to multiple personas (default: ' \
|
|
164
|
+
'all defined) and return {name => reply}. Useful for ' \
|
|
165
|
+
'ensemble opinions / voting on an approach.',
|
|
166
|
+
parameters: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
request: { type: 'string' },
|
|
170
|
+
names: { type: 'array', items: { type: 'string' } },
|
|
171
|
+
swarm_id: { type: 'string' }
|
|
172
|
+
},
|
|
173
|
+
required: %w[request]
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
max_chars: 48_000,
|
|
177
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
178
|
+
handler: lambda { |args|
|
|
179
|
+
PWN::AI::Agent::Swarm.broadcast(
|
|
180
|
+
request: args[:request],
|
|
181
|
+
names: args[:names],
|
|
182
|
+
swarm_id: args[:swarm_id]
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
PWN::AI::Agent::Registry.register(
|
|
188
|
+
name: 'swarm_bus',
|
|
189
|
+
toolset: 'swarm',
|
|
190
|
+
schema: {
|
|
191
|
+
name: 'swarm_bus',
|
|
192
|
+
description: 'Tail the JSONL message bus for a swarm ' \
|
|
193
|
+
'(~/.pwn/swarm/<id>/bus.jsonl). Read-side of agent_ask / ' \
|
|
194
|
+
'agent_debate — use to inspect what personas said in a ' \
|
|
195
|
+
'prior (or concurrent) session before continuing it.',
|
|
196
|
+
parameters: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
swarm_id: { type: 'string' },
|
|
200
|
+
limit: { type: 'integer', default: 25 }
|
|
201
|
+
},
|
|
202
|
+
required: %w[swarm_id]
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
206
|
+
handler: lambda { |args|
|
|
207
|
+
PWN::AI::Agent::Swarm.bus_tail(
|
|
208
|
+
swarm_id: args[:swarm_id],
|
|
209
|
+
limit: args[:limit] || 25
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
PWN::AI::Agent::Registry.register(
|
|
215
|
+
name: 'swarm_list',
|
|
216
|
+
toolset: 'swarm',
|
|
217
|
+
schema: {
|
|
218
|
+
name: 'swarm_list',
|
|
219
|
+
description: 'List all swarms under ~/.pwn/swarm/ (swarm_id, message ' \
|
|
220
|
+
'count, mtime). Pick one to resume with agent_ask / ' \
|
|
221
|
+
'agent_debate(swarm_id:) — this is how cross-session ' \
|
|
222
|
+
'multi-agent conversations continue without a daemon.',
|
|
223
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
224
|
+
},
|
|
225
|
+
check: -> { defined?(PWN::AI::Agent::Swarm) },
|
|
226
|
+
handler: lambda { |_args|
|
|
227
|
+
PWN::AI::Agent::Swarm.list
|
|
228
|
+
}
|
|
229
|
+
)
|
|
@@ -20,7 +20,7 @@ module PWN
|
|
|
20
20
|
|
|
21
21
|
system_role_content = "Being an expert penetration tester skilled in code analysis, debugging, and exploitation while stepping through JavaScript in a Chrome DevTools debugging session: 1. Your sole purpose is to analyze each JavaScript step and generate an Exploit Prediction Scoring System (EPSS) score between 0% - 100%. The step currently resides in this block of JavaScript:\n```\n#{source_to_review}\n```\n2. If the score is >= 75%, generate a JavaScript proof-of-concept that would allow a threat actor to directly exploit or target a user for exploitation (i.e. no self-exploit). 3. If the EPSS score is >= 75% also provide a code fix. *** If the EPSS score is < 75%, no explanations or summaries - just the EPSS score."
|
|
22
22
|
|
|
23
|
-
PWN::AI::Introspection.reflect_on(
|
|
23
|
+
PWN::AI::Agent::Introspection.reflect_on(
|
|
24
24
|
system_role_content: system_role_content,
|
|
25
25
|
request: request,
|
|
26
26
|
suppress_pii_warning: true
|
|
@@ -12,7 +12,7 @@ module PWN
|
|
|
12
12
|
# 4. CVSS score, vector string, and first.org calculator URI
|
|
13
13
|
# 5. CWE category, brief description, and CWE URI
|
|
14
14
|
# 6. Relevant NIST 800-53 control
|
|
15
|
-
# It leverages the PWN::AI::Introspection.reflect_on method. Defaults to Jira for existing workflow compatibility.
|
|
15
|
+
# It leverages the PWN::AI::Agent::Introspection.reflect_on method. Defaults to Jira for existing workflow compatibility.
|
|
16
16
|
module VulnGen
|
|
17
17
|
# Supported Method Parameters::
|
|
18
18
|
# ai_analysis = PWN::AI::Agent::VulnGen.analyze(
|
|
@@ -60,7 +60,7 @@ module PWN
|
|
|
60
60
|
6. NIST 800-53 Security Control that is impacted by this vulnerability.
|
|
61
61
|
"
|
|
62
62
|
|
|
63
|
-
analysis = PWN::AI::Introspection.reflect_on(
|
|
63
|
+
analysis = PWN::AI::Agent::Introspection.reflect_on(
|
|
64
64
|
system_role_content: system_role_content,
|
|
65
65
|
request: request,
|
|
66
66
|
suppress_pii_warning: true
|
data/lib/pwn/ai/agent.rb
CHANGED
|
@@ -25,6 +25,9 @@ module PWN
|
|
|
25
25
|
autoload :Loop, 'pwn/ai/agent/loop'
|
|
26
26
|
autoload :Metrics, 'pwn/ai/agent/metrics'
|
|
27
27
|
autoload :Learning, 'pwn/ai/agent/learning'
|
|
28
|
+
autoload :Extrospection, 'pwn/ai/agent/extrospection'
|
|
29
|
+
autoload :Introspection, 'pwn/ai/agent/introspection'
|
|
30
|
+
autoload :Swarm, 'pwn/ai/agent/swarm'
|
|
28
31
|
|
|
29
32
|
# Display a List of Every PWN::AI Module
|
|
30
33
|
|
data/lib/pwn/ai/anthropic.rb
CHANGED
|
@@ -155,7 +155,7 @@ module PWN
|
|
|
155
155
|
# tool_choice: 'optional - "auto" | "none" | "required" | {type:"function", function:{name:..}}',
|
|
156
156
|
# model: 'optional - overrides PWN::Env[:ai][:anthropic][:model]',
|
|
157
157
|
# temp: 'optional - temperature (defaults to PWN::Env[:ai][:anthropic][:temp] || 1)',
|
|
158
|
-
# max_tokens: 'optional - defaults to
|
|
158
|
+
# max_tokens: 'optional - defaults to PWN::Env[:ai][:anthropic][:max_tokens] || 128_000',
|
|
159
159
|
# timeout: 'optional - seconds (default 900)',
|
|
160
160
|
# spinner: 'optional - display spinner (default false)'
|
|
161
161
|
# )
|
|
@@ -175,7 +175,7 @@ module PWN
|
|
|
175
175
|
|
|
176
176
|
http_body = {
|
|
177
177
|
model: model,
|
|
178
|
-
max_tokens: opts[:max_tokens] ||
|
|
178
|
+
max_tokens: opts[:max_tokens] || engine[:max_tokens] || 128_000,
|
|
179
179
|
temperature: temp,
|
|
180
180
|
messages: anth_messages
|
|
181
181
|
}
|
|
@@ -284,7 +284,22 @@ module PWN
|
|
|
284
284
|
# Anthropic /v1/messages response -> OpenAI chat/completions shape
|
|
285
285
|
private_class_method def self.anthropic_resp_to_oa(opts = {})
|
|
286
286
|
resp = opts[:response] ||= {}
|
|
287
|
-
blocks
|
|
287
|
+
blocks = Array(resp[:content])
|
|
288
|
+
# When Anthropic hits max_tokens mid tool_use emission it returns the
|
|
289
|
+
# block with input:{} and stop_reason:"max_tokens". Dispatching that
|
|
290
|
+
# yields "ArgumentError: <param> is required" from every tool handler,
|
|
291
|
+
# and round-tripping it in _native_content without a matching
|
|
292
|
+
# tool_result 400s the next turn. Strip those artifacts here.
|
|
293
|
+
if resp[:stop_reason] == 'max_tokens'
|
|
294
|
+
truncated, blocks = blocks.partition do |b|
|
|
295
|
+
b[:type] == 'tool_use' && (b[:input].nil? || (b[:input].respond_to?(:empty?) && b[:input].empty?))
|
|
296
|
+
end
|
|
297
|
+
unless truncated.empty?
|
|
298
|
+
names = truncated.map { |b| b[:name] }.join(', ')
|
|
299
|
+
warn "[pwn-ai] Anthropic hit max_tokens mid tool_use (#{names}); " \
|
|
300
|
+
'dropping partial call. Raise ai.anthropic.max_tokens in PWN::Env.'
|
|
301
|
+
end
|
|
302
|
+
end
|
|
288
303
|
text = blocks.select { |b| b[:type] == 'text' }.map { |b| b[:text] }.join
|
|
289
304
|
tool_calls = blocks.select { |b| b[:type] == 'tool_use' }.map do |b|
|
|
290
305
|
{
|
|
@@ -375,7 +390,7 @@ module PWN
|
|
|
375
390
|
|
|
376
391
|
http_body = {
|
|
377
392
|
model: model,
|
|
378
|
-
max_tokens:
|
|
393
|
+
max_tokens: engine[:max_tokens] || 128_000,
|
|
379
394
|
temperature: temp,
|
|
380
395
|
system: system_role_content,
|
|
381
396
|
messages: []
|
data/lib/pwn/ai.rb
CHANGED
data/lib/pwn/config.rb
CHANGED
|
@@ -74,6 +74,7 @@ module PWN
|
|
|
74
74
|
model: 'optional - Anthropic model to use (e.g. claude-3-5-sonnet-20240620)',
|
|
75
75
|
system_role_content: 'You are an ethically hacking Anthropic agent.',
|
|
76
76
|
temp: 'optional - Anthropic temperature',
|
|
77
|
+
max_tokens: 'optional - Max output tokens per response (default 8192). Raise if tool calls truncate.',
|
|
77
78
|
max_prompt_length: 200_000
|
|
78
79
|
},
|
|
79
80
|
gemini: {
|
|
@@ -87,7 +88,15 @@ module PWN
|
|
|
87
88
|
agent: {
|
|
88
89
|
native_tools: true,
|
|
89
90
|
max_iters: 25,
|
|
91
|
+
# Swarm (agent_ask/agent_debate) sub-agent recursion cap
|
|
92
|
+
max_depth: 3,
|
|
93
|
+
# run PWN::AI::Agent::Learning.auto_reflect after every final answer
|
|
94
|
+
auto_reflect: true,
|
|
95
|
+
# also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_reflect
|
|
96
|
+
auto_extrospect: false,
|
|
90
97
|
toolsets: nil
|
|
98
|
+
# multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
|
|
99
|
+
# swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
|
|
91
100
|
}
|
|
92
101
|
},
|
|
93
102
|
plugins: {
|
|
@@ -100,28 +109,6 @@ module PWN
|
|
|
100
109
|
rpc_pass: 'bitcoin RPC Password'
|
|
101
110
|
}
|
|
102
111
|
},
|
|
103
|
-
irc: {
|
|
104
|
-
ui_nick: '_human_',
|
|
105
|
-
shared_chan: '#pwn',
|
|
106
|
-
ai_agent_nicks: {
|
|
107
|
-
browser: {
|
|
108
|
-
pwn_rb: '/opt/pwn/lib/pwn/plugins/transparent_browser.rb',
|
|
109
|
-
system_role_content: 'You are a browser. You are a web browser that can be controlled by a human or AI agent'
|
|
110
|
-
},
|
|
111
|
-
nimjeh: {
|
|
112
|
-
pwn_rb: '',
|
|
113
|
-
system_role_content: 'You are a sarcastic hacker. You find software zero day vulnerabilities. This involves analyzing source code, race conditions, application binaries, and network protocols from an offensive security perspective.'
|
|
114
|
-
},
|
|
115
|
-
nmap: {
|
|
116
|
-
pwn_rb: '/opt/pwn/lib/pwn/plugins/nmap_it.rb',
|
|
117
|
-
system_role_content: 'You are a network scanner. You are a network scanner that can be controlled by a human or AI agent'
|
|
118
|
-
},
|
|
119
|
-
shodan: {
|
|
120
|
-
pwn_rb: '/opt/pwn/lib/pwn/plugins/shodan.rb',
|
|
121
|
-
system_role_content: 'You are a passive reconnaissance agent. You are a passive reconnaissance agent that can be controlled by a human or AI agent'
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
112
|
hunter: { api_key: 'hunter.how API Key' },
|
|
126
113
|
jira_data_center: {
|
|
127
114
|
base_uri: 'Jira Server Base API URI (e.g. https://jira.company.com/rest/api/latest)',
|
|
@@ -300,7 +287,7 @@ module PWN
|
|
|
300
287
|
iv: iv
|
|
301
288
|
)
|
|
302
289
|
|
|
303
|
-
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :
|
|
290
|
+
valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
|
|
304
291
|
|
|
305
292
|
engine = env[:ai][:active].to_s.downcase.to_sym
|
|
306
293
|
raise "ERROR: Unsupported AI Engine: #{engine} in #{pwn_env_path}. Supported AI Engines:\n#{valid_ai_engines.inspect}" unless valid_ai_engines.include?(engine)
|
data/lib/pwn/cron.rb
CHANGED
|
@@ -71,7 +71,7 @@ module PWN
|
|
|
71
71
|
# Executes the job (for pwn-ai prompt it will use current active AI engine
|
|
72
72
|
# via PWN::AI::* but without full REPL hook unless in pwn-ai).
|
|
73
73
|
public_class_method def self.run(opts = {})
|
|
74
|
-
id = opts[:id]
|
|
74
|
+
id = opts[:id].to_s
|
|
75
75
|
jobs = load_jobs
|
|
76
76
|
job = jobs[id] || jobs.values.find { |j| j[:name] == id || j[:id] == id }
|
|
77
77
|
raise "Job #{id} not found" unless job
|
|
@@ -134,7 +134,7 @@ module PWN
|
|
|
134
134
|
# Supported Method Parameters::
|
|
135
135
|
# PWN::Cron.remove(id:)
|
|
136
136
|
public_class_method def self.remove(opts = {}) # rubocop:disable Naming/PredicateMethod
|
|
137
|
-
id = opts[:id]
|
|
137
|
+
id = opts[:id].to_s
|
|
138
138
|
jobs = load_jobs
|
|
139
139
|
jobs.delete(id)
|
|
140
140
|
save_jobs(jobs: jobs)
|
|
@@ -152,10 +152,10 @@ module PWN
|
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Install a crontab line that invokes this job via pwn
|
|
155
|
-
# (assumes /opt/pwn and rvm ruby
|
|
155
|
+
# (assumes /opt/pwn and the active rvm ruby@pwn gemset - user can edit crontab)
|
|
156
156
|
public_class_method def self.install_crontab_entry(opts = {})
|
|
157
157
|
job = opts[:job]
|
|
158
|
-
cron_line = "#{job[:schedule]} cd /opt/pwn && /usr/local/rvm/bin/rvm ruby
|
|
158
|
+
cron_line = "#{job[:schedule]} cd /opt/pwn && /usr/local/rvm/bin/rvm ruby-#{RUBY_VERSION}@pwn do ruby -I lib -e 'require \"pwn\"; PWN::Cron.run(id: \"#{job[:id]}\")' >> #{File.join(cron_dir, 'cron.log')} 2>&1"
|
|
159
159
|
# Append to user's crontab (non-destructive)
|
|
160
160
|
existing = `crontab -l 2>/dev/null || true`
|
|
161
161
|
unless existing.include?(job[:id])
|
|
@@ -169,8 +169,17 @@ module PWN
|
|
|
169
169
|
FileUtils.mkdir_p(cron_dir)
|
|
170
170
|
return {} unless File.exist?(JOBS_FILE)
|
|
171
171
|
|
|
172
|
-
YAML.safe_load_file(
|
|
173
|
-
|
|
172
|
+
raw = YAML.safe_load_file(
|
|
173
|
+
JOBS_FILE,
|
|
174
|
+
permitted_classes: [Symbol, Time],
|
|
175
|
+
symbolize_names: true
|
|
176
|
+
) || {}
|
|
177
|
+
# Normalize outer job-id keys to String (create() uses String ids;
|
|
178
|
+
# symbolize_names would otherwise turn them into Symbols on reload
|
|
179
|
+
# and break jobs[id] / jobs.delete(id) / toggle lookups).
|
|
180
|
+
raw.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
warn("[PWN::Cron] load_jobs failed: #{e.class}: #{e.message}")
|
|
174
183
|
{}
|
|
175
184
|
end
|
|
176
185
|
|
|
@@ -180,7 +189,7 @@ module PWN
|
|
|
180
189
|
end
|
|
181
190
|
|
|
182
191
|
private_class_method def self.toggle(opts = {})
|
|
183
|
-
id = opts[:id]
|
|
192
|
+
id = opts[:id].to_s
|
|
184
193
|
enabled = opts[:enabled]
|
|
185
194
|
jobs = load_jobs
|
|
186
195
|
if jobs[id]
|