pwn 0.5.613 → 0.5.617
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/Gemfile +10 -10
- data/README.md +117 -17
- data/bin/pwn +47 -2
- data/bin/pwn_gqrx_scanner +75 -18
- data/documentation/AI-Integration.md +36 -0
- data/documentation/BurpSuite.md +26 -0
- data/documentation/Contributing.md +31 -0
- data/documentation/Diagrams.md +140 -0
- data/documentation/Drivers.md +25 -0
- data/documentation/General-PWN-Usage.md +60 -0
- data/documentation/Home.md +78 -0
- data/documentation/How-PWN-Works.md +62 -0
- data/documentation/Installation.md +75 -0
- data/documentation/NmapIt.md +18 -0
- data/documentation/Plugins.md +59 -0
- data/documentation/Reporting.md +28 -0
- data/documentation/SAST.md +32 -0
- data/documentation/Skills-Memory-Learning.md +44 -0
- data/documentation/Transparent-Browser.md +32 -0
- data/documentation/Troubleshooting.md +38 -0
- data/documentation/What-is-PWN.md +53 -0
- data/documentation/Why-PWN.md +48 -0
- data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
- data/documentation/diagrams/build.sh +20 -0
- data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
- data/documentation/diagrams/code-scanning-sast.svg +172 -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 +50 -0
- data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
- data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
- data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
- data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
- data/documentation/diagrams/dot/driver-framework.dot +39 -0
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
- data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
- data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
- data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
- data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
- data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
- data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
- data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
- data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
- data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
- data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
- data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
- data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
- data/documentation/diagrams/dot/web-application-testing.dot +49 -0
- data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
- data/documentation/diagrams/driver-framework.svg +111 -0
- data/documentation/diagrams/fuzzing-workflow.svg +160 -0
- data/documentation/diagrams/history-to-drivers.svg +119 -0
- data/documentation/diagrams/memory-skills-detailed.svg +202 -0
- data/documentation/diagrams/network-infra-testing.svg +183 -0
- data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
- data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
- data/documentation/diagrams/plugin-ecosystem.svg +276 -0
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
- data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
- data/documentation/diagrams/reporting-pipeline.svg +151 -0
- data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
- data/documentation/diagrams/sessions-cron-automation.svg +136 -0
- data/documentation/diagrams/web-application-testing.svg +173 -0
- data/documentation/diagrams/zero-day-research-flow.svg +153 -0
- data/documentation/pwn-REPL.md +33 -0
- data/documentation/pwn-ai-Agent.md +43 -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 +379 -0
- data/lib/pwn/ai/agent/loop.rb +33 -3
- data/lib/pwn/ai/agent/metrics.rb +155 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
- 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 +221 -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 +124 -8
- 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 +5 -0
- data/lib/pwn/ai/anthropic.rb +19 -4
- data/lib/pwn/ai/grok.rb +249 -106
- data/lib/pwn/ai.rb +0 -1
- data/lib/pwn/config.rb +96 -36
- data/lib/pwn/cron.rb +16 -7
- data/lib/pwn/plugins/monkey_patch.rb +2 -2
- data/lib/pwn/plugins/repl.rb +158 -284
- 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/learning_spec.rb +68 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -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/learning_spec.rb +30 -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 +178 -33
- metadata +110 -36
- data/documentation/PWN.png +0 -0
- data/documentation/PWN_Contributors_and_Users.png +0 -0
- data/documentation/fax-spectrogram.png +0 -0
- data/documentation/fax-waveform.png +0 -0
- data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
- data/documentation/lifecycle_authz_replay.example.yaml +0 -27
- 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/documentation/vulnerability_report_template.md +0 -37
- 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
|
+
)
|
|
@@ -6,19 +6,30 @@ require 'pwn/ai/agent/registry'
|
|
|
6
6
|
# Thin wrappers around the PWN::Skills store (~/.pwn/skills/*.md) so the
|
|
7
7
|
# model can list/read/write reusable procedures. PromptBuilder injects
|
|
8
8
|
# the index (name + first line); skill_view loads the full body on demand.
|
|
9
|
+
#
|
|
10
|
+
# Each skill may carry a :references Array (URLs, CWE/CVE/ATT&CK ids,
|
|
11
|
+
# NIST 800-53 controls, docs) parsed from YAML front-matter or a
|
|
12
|
+
# "## References" markdown section. References let the agent cite
|
|
13
|
+
# authoritative sources when applying a skill and mirror the
|
|
14
|
+
# PWN::SAST::*.security_references convention.
|
|
9
15
|
|
|
10
16
|
PWN::AI::Agent::Registry.register(
|
|
11
17
|
name: 'skill_list',
|
|
12
18
|
toolset: 'skills',
|
|
13
19
|
schema: {
|
|
14
20
|
name: 'skill_list',
|
|
15
|
-
description: 'List available pwn-ai skills (name + first line
|
|
21
|
+
description: 'List available pwn-ai skills (name + first line + reference count).',
|
|
16
22
|
parameters: { type: 'object', properties: {}, required: [] }
|
|
17
23
|
},
|
|
18
24
|
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
19
25
|
handler: lambda { |_args|
|
|
20
26
|
PWN::Skills.map do |name, meta|
|
|
21
|
-
{
|
|
27
|
+
{
|
|
28
|
+
name: name,
|
|
29
|
+
type: meta[:type],
|
|
30
|
+
summary: meta[:content].to_s.lines.first.to_s.strip[0, 120],
|
|
31
|
+
references: Array(meta[:references]).length
|
|
32
|
+
}
|
|
22
33
|
end
|
|
23
34
|
}
|
|
24
35
|
)
|
|
@@ -28,7 +39,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
28
39
|
toolset: 'skills',
|
|
29
40
|
schema: {
|
|
30
41
|
name: 'skill_view',
|
|
31
|
-
description: 'Read the full content of a named skill.',
|
|
42
|
+
description: 'Read the full content of a named skill, including its references.',
|
|
32
43
|
parameters: {
|
|
33
44
|
type: 'object',
|
|
34
45
|
properties: { name: { type: 'string' } },
|
|
@@ -41,7 +52,13 @@ PWN::AI::Agent::Registry.register(
|
|
|
41
52
|
meta = PWN::Skills[key]
|
|
42
53
|
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
43
54
|
|
|
44
|
-
{
|
|
55
|
+
{
|
|
56
|
+
name: key,
|
|
57
|
+
type: meta[:type],
|
|
58
|
+
path: meta[:path],
|
|
59
|
+
references: Array(meta[:references]),
|
|
60
|
+
content: meta[:content]
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
63
|
)
|
|
47
64
|
|
|
@@ -51,12 +68,20 @@ PWN::AI::Agent::Registry.register(
|
|
|
51
68
|
schema: {
|
|
52
69
|
name: 'skill_create',
|
|
53
70
|
description: 'Save a new reusable skill (markdown procedure) to ~/.pwn/skills/. ' \
|
|
54
|
-
'It will be listed in every future pwn-ai system prompt.'
|
|
71
|
+
'It will be listed in every future pwn-ai system prompt. Optionally ' \
|
|
72
|
+
'attach references (URLs, CWE/CVE ids, MITRE ATT&CK, NIST 800-53, docs).',
|
|
55
73
|
parameters: {
|
|
56
74
|
type: 'object',
|
|
57
75
|
properties: {
|
|
58
76
|
name: { type: 'string', description: 'lowercase, hyphens/underscores only' },
|
|
59
|
-
content: { type: 'string', description: 'Full markdown body of the skill.' }
|
|
77
|
+
content: { type: 'string', description: 'Full markdown body of the skill.' },
|
|
78
|
+
references: {
|
|
79
|
+
type: 'array',
|
|
80
|
+
items: { type: 'string' },
|
|
81
|
+
description: 'Optional list of reference URLs / identifiers (CWE-79, T1059, ' \
|
|
82
|
+
'https://..., NIST SI-3, etc.). Written as YAML front-matter and ' \
|
|
83
|
+
'a "## References" section.'
|
|
84
|
+
}
|
|
60
85
|
},
|
|
61
86
|
required: %w[name content]
|
|
62
87
|
}
|
|
@@ -66,11 +91,102 @@ PWN::AI::Agent::Registry.register(
|
|
|
66
91
|
name = args[:name].to_s.gsub(/[^a-z0-9_-]/i, '_')
|
|
67
92
|
raise ArgumentError, 'name is required' if name.empty?
|
|
68
93
|
|
|
94
|
+
body = args[:content].to_s
|
|
95
|
+
refs = Array(args[:references]).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
96
|
+
|
|
97
|
+
unless refs.empty?
|
|
98
|
+
# YAML front-matter (only if body doesn't already have one)
|
|
99
|
+
unless body.start_with?("---\n")
|
|
100
|
+
fm = "---\nreferences:\n#{refs.map { |r| " - #{r}" }.join("\n")}\n---\n"
|
|
101
|
+
body = fm + body
|
|
102
|
+
end
|
|
103
|
+
# ## References section (only if not already present)
|
|
104
|
+
body = "#{body.rstrip}\n\n## References\n#{refs.map { |r| "- #{r}" }.join("\n")}\n" unless body =~ /^\#{1,3}\s*References\s*$/i
|
|
105
|
+
end
|
|
106
|
+
|
|
69
107
|
dir = PWN::Config.pwn_skills_path
|
|
70
108
|
FileUtils.mkdir_p(dir)
|
|
71
109
|
path = File.join(dir, "#{name}.md")
|
|
72
|
-
File.write(path,
|
|
110
|
+
File.write(path, body)
|
|
73
111
|
PWN::Config.load_skills(pwn_skills_path: dir) # refresh PWN::Skills const
|
|
74
|
-
{ saved: true, path: path, total: PWN::Skills.keys.length }
|
|
112
|
+
{ saved: true, path: path, references: refs, total: PWN::Skills.keys.length }
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
PWN::AI::Agent::Registry.register(
|
|
117
|
+
name: 'skill_add_reference',
|
|
118
|
+
toolset: 'skills',
|
|
119
|
+
schema: {
|
|
120
|
+
name: 'skill_add_reference',
|
|
121
|
+
description: 'Append one or more references (URL, CWE/CVE id, MITRE ATT&CK ' \
|
|
122
|
+
'technique, NIST control, doc link) to an existing skill.',
|
|
123
|
+
parameters: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
name: { type: 'string', description: 'Existing skill name.' },
|
|
127
|
+
references: { type: 'array', items: { type: 'string' } }
|
|
128
|
+
},
|
|
129
|
+
required: %w[name references]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
check: -> { defined?(PWN::Skills) && PWN::Skills.is_a?(Hash) },
|
|
133
|
+
handler: lambda { |args|
|
|
134
|
+
key = args[:name].to_s.to_sym
|
|
135
|
+
meta = PWN::Skills[key]
|
|
136
|
+
raise ArgumentError, "no such skill: #{key}" unless meta
|
|
137
|
+
|
|
138
|
+
new_refs = Array(args[:references]).map(&:to_s).map(&:strip).reject(&:empty?)
|
|
139
|
+
raise ArgumentError, 'references must be a non-empty array' if new_refs.empty?
|
|
140
|
+
|
|
141
|
+
body = File.read(meta[:path])
|
|
142
|
+
existing = Array(meta[:references])
|
|
143
|
+
add = (new_refs - existing)
|
|
144
|
+
|
|
145
|
+
if body =~ /^(\#{1,3}\s*References\s*)$/i
|
|
146
|
+
# Append bullets right after the References heading block (end of file safest)
|
|
147
|
+
body = "#{body.rstrip}\n#{add.map { |r| "- #{r}" }.join("\n")}\n" unless add.empty?
|
|
148
|
+
else
|
|
149
|
+
body = "#{body.rstrip}\n\n## References\n#{(existing + add).uniq.map { |r| "- #{r}" }.join("\n")}\n"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
File.write(meta[:path], body)
|
|
153
|
+
PWN::Config.load_skills(pwn_skills_path: File.dirname(meta[:path]))
|
|
154
|
+
{
|
|
155
|
+
saved: true,
|
|
156
|
+
name: key,
|
|
157
|
+
path: meta[:path],
|
|
158
|
+
added: add,
|
|
159
|
+
references: Array(PWN::Skills[key][:references])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
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 }
|
|
75
191
|
}
|
|
76
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
|
@@ -23,6 +23,11 @@ module PWN
|
|
|
23
23
|
autoload :Result, 'pwn/ai/agent/result'
|
|
24
24
|
autoload :PromptBuilder, 'pwn/ai/agent/prompt_builder'
|
|
25
25
|
autoload :Loop, 'pwn/ai/agent/loop'
|
|
26
|
+
autoload :Metrics, 'pwn/ai/agent/metrics'
|
|
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'
|
|
26
31
|
|
|
27
32
|
# Display a List of Every PWN::AI Module
|
|
28
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: []
|