pwn 0.5.702 → 0.5.704
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/Rakefile +7 -0
- data/build_gem.sh +1 -1
- data/documentation/General-PWN-Usage.md +2 -1
- data/documentation/Installation.md +5 -3
- data/documentation/pwn-REPL.md +2 -1
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +4 -0
- data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +4 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +4 -0
- data/etc/default_skills/pwn/ai/agent/open_goal/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/policy/SKILL.md +11 -0
- data/etc/default_skills/pwn/ai/agent/prompt_cache/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/reward/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/task_summarizer/SKILL.md +12 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +9 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +5 -0
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
- data/etc/default_skills/pwn/banner/matrix/SKILL.md +1 -0
- data/etc/default_skills/pwn/cron/SKILL.md +7 -0
- data/etc/default_skills/pwn/ffi/SKILL.md +1 -0
- data/etc/default_skills/pwn/ffi/adalm_pluto/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/fftw/SKILL.md +10 -0
- data/etc/default_skills/pwn/ffi/hack_rf/SKILL.md +22 -0
- data/etc/default_skills/pwn/ffi/liquid/SKILL.md +33 -0
- data/etc/default_skills/pwn/ffi/rtl_sdr/SKILL.md +18 -0
- data/etc/default_skills/pwn/ffi/soapy_sdr/SKILL.md +24 -0
- data/etc/default_skills/pwn/ffi/stdio/SKILL.md +4 -0
- data/etc/default_skills/pwn/ffi/volk/SKILL.md +17 -0
- data/etc/default_skills/pwn/memory/SKILL.md +4 -0
- data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
- data/etc/default_skills/pwn/migrate/SKILL.md +1 -0
- data/etc/default_skills/pwn/module_skills/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/log/SKILL.md +16 -0
- data/etc/default_skills/pwn/plugins/pony/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +4 -0
- data/etc/default_skills/pwn/plugins/tty_spinner/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +1 -0
- data/etc/default_skills/pwn/sdr/decoder/adsb/SKILL.md +1 -0
- data/etc/default_skills/pwn/sdr/decoder/base/SKILL.md +1 -0
- data/etc/default_skills/pwn/sdr/decoder/dsp/SKILL.md +3 -0
- data/etc/default_skills/pwn/sdr/decoder/flex/SKILL.md +1 -0
- data/etc/default_skills/pwn/sessions/SKILL.md +1 -0
- data/etc/default_skills/pwn/version/SKILL.md +1 -1
- data/lib/pwn/ai/agent/loop.rb +31 -10
- data/lib/pwn/ai/agent/tool_guard.rb +24 -13
- data/lib/pwn/config.rb +90 -91
- data/lib/pwn/module_skills.rb +71 -1
- data/lib/pwn/plugins/github.rb +1 -1
- data/lib/pwn/plugins/log.rb +71 -8
- data/lib/pwn/plugins/repl.rb +35 -3
- data/lib/pwn/plugins/tty_spinner.rb +1 -1
- data/lib/pwn/plugins/zaproxy.rb +1 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/config_spec.rb +29 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +12 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +13 -0
- data/spec/lib/pwn/module_skills_spec.rb +12 -0
- data/spec/lib/pwn/plugins/log_spec.rb +88 -9
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +37 -1
- data/spec/lib/pwn/plugins/repl_spec.rb +16 -1
- data/third_party/pwn_rdoc.jsonl +15 -2
- metadata +1 -1
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -144,6 +144,15 @@ module PWN
|
|
|
144
144
|
)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
private_class_method def self.wait_trace_step!(opts = {})
|
|
148
|
+
return unless defined?(PWN::Plugins::Log) && PWN::Plugins::Log.respond_to?(:wait_trace_step!)
|
|
149
|
+
|
|
150
|
+
PWN::Plugins::Log.wait_trace_step!(
|
|
151
|
+
label: opts[:label],
|
|
152
|
+
nested: opts[:nested]
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
147
156
|
private_class_method def self.start_debug_session(opts = {})
|
|
148
157
|
return unless debug_on?(opts)
|
|
149
158
|
return unless defined?(PWN::Plugins::Log)
|
|
@@ -153,13 +162,14 @@ module PWN
|
|
|
153
162
|
return
|
|
154
163
|
end
|
|
155
164
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
want_trace = opts[:trace] == true
|
|
166
|
+
begin
|
|
167
|
+
want_trace ||= defined?(Pry) && Pry.config.respond_to?(:pwn_ai_trace) && Pry.config.pwn_ai_trace
|
|
168
|
+
rescue StandardError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
want_trace ||= PWN::Plugins::Log.trace_enabled? if PWN::Plugins::Log.respond_to?(:trace_enabled?)
|
|
172
|
+
if !PWN::Plugins::Log.debug_enabled? || want_trace
|
|
163
173
|
PWN::Plugins::Log.start_debug(
|
|
164
174
|
tee: opts[:debug_tee] || $stdout,
|
|
165
175
|
session_id: opts[:session_id],
|
|
@@ -934,10 +944,14 @@ module PWN
|
|
|
934
944
|
nil
|
|
935
945
|
end
|
|
936
946
|
|
|
947
|
+
PAYLOAD_SHA256 = Digest::SHA256
|
|
948
|
+
|
|
937
949
|
private_class_method def self.payload_sig(opts = {})
|
|
938
|
-
|
|
950
|
+
blob = "#{opts[:name]}|#{opts[:args]}"
|
|
951
|
+
PAYLOAD_SHA256.hexdigest(blob)[0, 16]
|
|
939
952
|
rescue StandardError
|
|
940
|
-
"
|
|
953
|
+
key = "#{opts[:name]}|#{opts[:args]}"
|
|
954
|
+
"#{opts[:name]}-#{key.hash.abs.to_s(16)[0, 12]}"
|
|
941
955
|
end
|
|
942
956
|
|
|
943
957
|
private_class_method def self.note_same_payload!(opts = {})
|
|
@@ -1742,7 +1756,12 @@ module PWN
|
|
|
1742
1756
|
what\s+did\s+you\s+(?:just\s+)?(?:say|answer|reply|respond)|
|
|
1743
1757
|
what\s+(?:was|is)\s+your\s+(?:last|previous|prior)\s+(?:answer|response|reply)|
|
|
1744
1758
|
(?:without\s+looking\s+up).{0,40}(?:session|discussing|talking)|
|
|
1745
|
-
(?:
|
|
1759
|
+
from\s+(?:(?:your|my|the)\s+)?(?:memory|context)|
|
|
1760
|
+
in\s+your\s+memory|
|
|
1761
|
+
(?:your|my)\s+memory\s+(?:of|about)|
|
|
1762
|
+
earlier\s+in\s+(?:this\s+)?(?:session|chat|conversation|turn)|
|
|
1763
|
+
previously\s+in\s+(?:this\s+)?(?:session|chat|conversation)|
|
|
1764
|
+
prior\s+turn|
|
|
1746
1765
|
(?:do\s+you\s+)?remember\s+what\s+(?:i|you)|
|
|
1747
1766
|
recall\s+(?:what|my|your|the\s+last)|
|
|
1748
1767
|
last\s+thing\s+(?:i|you)\s+said
|
|
@@ -2619,6 +2638,7 @@ module PWN
|
|
|
2619
2638
|
end
|
|
2620
2639
|
engine_s += (Time.now - t0)
|
|
2621
2640
|
PWN::Plugins::TTYSpinner.halt_all! if defined?(PWN::Plugins::TTYSpinner)
|
|
2641
|
+
wait_trace_step!(label: 'engine', nested: nested)
|
|
2622
2642
|
if msg.nil?
|
|
2623
2643
|
task_summary_flush!(state: ts_state, on_tool: on_tool)
|
|
2624
2644
|
debug_progress(msg: 'engine returned no message')
|
|
@@ -2766,6 +2786,7 @@ module PWN
|
|
|
2766
2786
|
|
|
2767
2787
|
on_tool&.call(name, args, result)
|
|
2768
2788
|
debug_tool_io!(name: name, args: args, result: result)
|
|
2789
|
+
wait_trace_step!(label: "tool #{name}", nested: nested)
|
|
2769
2790
|
task_summary_record!(state: ts_state, name: name, args: args, result: result, on_tool: on_tool)
|
|
2770
2791
|
|
|
2771
2792
|
messages << {
|
|
@@ -60,23 +60,34 @@ module PWN
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# RestClient uses HTTP::CookieJar. pwn_eval in TOPLEVEL_BINDING can
|
|
63
|
-
# assign HTTP = "/path/http" (
|
|
64
|
-
#
|
|
63
|
+
# assign HTTP = "/path/http" or Digest = "(self.we" and then every
|
|
64
|
+
# provider hop / payload_sig TypeErrors.
|
|
65
|
+
CORE_CONSTS = %i[HTTP Digest JSON URI Timeout].freeze
|
|
66
|
+
|
|
65
67
|
public_class_method def self.protect_http!
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
protect_core_constants!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
public_class_method def self.protect_core_constants!
|
|
72
|
+
@core_mods ||= {}
|
|
73
|
+
CORE_CONSTS.each do |name|
|
|
74
|
+
if Object.const_defined?(name, false)
|
|
75
|
+
cur = Object.const_get(name, false)
|
|
76
|
+
@core_mods[name] = cur if cur.is_a?(Module) && @core_mods[name].nil?
|
|
77
|
+
next if cur.is_a?(Module)
|
|
78
|
+
|
|
79
|
+
Object.send(:remove_const, name)
|
|
80
|
+
end
|
|
81
|
+
next unless @core_mods[name].is_a?(Module)
|
|
82
|
+
next if Object.const_defined?(name, false) && Object.const_get(name, false).equal?(@core_mods[name])
|
|
70
83
|
|
|
71
|
-
Object.
|
|
84
|
+
Object.const_set(name, @core_mods[name])
|
|
72
85
|
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
unless Object.const_defined?(:HTTP, false) && Object.const_get(:HTTP, false).is_a?(Module)
|
|
87
|
+
require 'http/cookie_jar'
|
|
88
|
+
@core_mods[:HTTP] = Object.const_get(:HTTP) if Object.const_defined?(:HTTP) && Object.const_get(:HTTP).is_a?(Module)
|
|
76
89
|
end
|
|
77
|
-
|
|
78
|
-
@http_mod = Object.const_get(:HTTP) if Object.const_defined?(:HTTP) && Object.const_get(:HTTP).is_a?(Module)
|
|
79
|
-
@http_mod
|
|
90
|
+
@core_mods
|
|
80
91
|
rescue StandardError
|
|
81
92
|
nil
|
|
82
93
|
end
|
data/lib/pwn/config.rb
CHANGED
|
@@ -849,9 +849,11 @@ module PWN
|
|
|
849
849
|
end
|
|
850
850
|
|
|
851
851
|
# Seed bundled skills into ~/.pwn/skills (or pwn_skills_path:).
|
|
852
|
-
#
|
|
853
|
-
#
|
|
854
|
-
#
|
|
852
|
+
# Every SKILL.md under etc/default_skills is copied, preserving
|
|
853
|
+
# relative path. SOP SKILL.md files are left alone so operator
|
|
854
|
+
# edits survive upgrades. Generated module skills under pwn/ are
|
|
855
|
+
# overwritten by ModuleSkills.install. scripts/ are never copied
|
|
856
|
+
# from the gem templates.
|
|
855
857
|
public_class_method def self.install_default_skills(opts = {})
|
|
856
858
|
root = opts[:pwn_skills_path] || pwn_skills_path
|
|
857
859
|
src_root = opts[:source] || default_skills_dir
|
|
@@ -859,33 +861,17 @@ module PWN
|
|
|
859
861
|
|
|
860
862
|
FileUtils.mkdir_p(root)
|
|
861
863
|
seeded = []
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
dest = File.join(root, name, SKILL_ENTRY)
|
|
865
|
-
src = File.join(src_dir, SKILL_ENTRY)
|
|
866
|
-
next unless File.file?(src)
|
|
864
|
+
each_skill_md(root: src_root) do |src, rel_dir|
|
|
865
|
+
next if module_skill_rel?(rel: rel_dir)
|
|
867
866
|
|
|
867
|
+
dest_dir = File.join(root, rel_dir)
|
|
868
|
+
dest = File.join(dest_dir, SKILL_ENTRY)
|
|
868
869
|
unless File.file?(dest)
|
|
869
|
-
FileUtils.mkdir_p(
|
|
870
|
+
FileUtils.mkdir_p(dest_dir)
|
|
870
871
|
FileUtils.cp(src, dest)
|
|
871
|
-
seeded << { name:
|
|
872
|
-
end
|
|
873
|
-
|
|
874
|
-
# Copy missing reference files (never overwrite operator edits).
|
|
875
|
-
ref_src = File.join(src_dir, 'references')
|
|
876
|
-
next unless Dir.exist?(ref_src)
|
|
877
|
-
|
|
878
|
-
ref_dest = File.join(root, name, 'references')
|
|
879
|
-
FileUtils.mkdir_p(ref_dest)
|
|
880
|
-
Dir.children(ref_src).each do |base|
|
|
881
|
-
from = File.join(ref_src, base)
|
|
882
|
-
next unless File.file?(from)
|
|
883
|
-
|
|
884
|
-
to = File.join(ref_dest, base)
|
|
885
|
-
next if File.file?(to)
|
|
886
|
-
|
|
887
|
-
FileUtils.cp(from, to)
|
|
872
|
+
seeded << { name: rel_dir, path: dest }
|
|
888
873
|
end
|
|
874
|
+
copy_missing_references(source_dir: File.dirname(src), dest_dir: dest_dir)
|
|
889
875
|
end
|
|
890
876
|
if defined?(PWN::ModuleSkills) && PWN::ModuleSkills.respond_to?(:install)
|
|
891
877
|
PWN::ModuleSkills.install(
|
|
@@ -899,16 +885,82 @@ module PWN
|
|
|
899
885
|
[]
|
|
900
886
|
end
|
|
901
887
|
|
|
902
|
-
private_class_method def self.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
888
|
+
private_class_method def self.each_skill_md(opts = {})
|
|
889
|
+
root = opts[:root].to_s.chomp('/')
|
|
890
|
+
return if root.empty? || !Dir.exist?(root)
|
|
891
|
+
|
|
892
|
+
Dir.glob(File.join(root, '**', SKILL_ENTRY)).each do |path|
|
|
893
|
+
next unless File.file?(path)
|
|
894
|
+
next if path.include?('/references/')
|
|
895
|
+
next if path.include?('/scripts/')
|
|
907
896
|
|
|
908
|
-
|
|
909
|
-
|
|
897
|
+
rel_dir = File.dirname(path).delete_prefix("#{root}/")
|
|
898
|
+
next if rel_dir.empty? || rel_dir == '.'
|
|
899
|
+
|
|
900
|
+
yield path, rel_dir
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
private_class_method def self.module_skill_rel?(opts = {})
|
|
905
|
+
rel = opts[:rel].to_s
|
|
906
|
+
rel == 'pwn' || rel.start_with?('pwn/')
|
|
907
|
+
end
|
|
910
908
|
|
|
911
|
-
|
|
909
|
+
private_class_method def self.copy_missing_references(opts = {})
|
|
910
|
+
ref_src = File.join(opts[:source_dir].to_s, 'references')
|
|
911
|
+
return unless Dir.exist?(ref_src)
|
|
912
|
+
|
|
913
|
+
ref_dest = File.join(opts[:dest_dir].to_s, 'references')
|
|
914
|
+
Dir.glob(File.join(ref_src, '**', '*')).each do |from|
|
|
915
|
+
next unless File.file?(from)
|
|
916
|
+
|
|
917
|
+
rel = from.delete_prefix("#{ref_src}/")
|
|
918
|
+
to = File.join(ref_dest, rel)
|
|
919
|
+
next if File.file?(to)
|
|
920
|
+
|
|
921
|
+
FileUtils.mkdir_p(File.dirname(to))
|
|
922
|
+
FileUtils.cp(from, to)
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
private_class_method def self.ingest_skill_md(opts = {})
|
|
927
|
+
skills = opts[:skills]
|
|
928
|
+
entry = opts[:entry].to_s
|
|
929
|
+
rel_dir = opts[:rel_dir].to_s
|
|
930
|
+
return skills unless skills.is_a?(Hash)
|
|
931
|
+
return skills if entry.empty? || rel_dir.empty?
|
|
932
|
+
|
|
933
|
+
key = rel_dir.to_sym
|
|
934
|
+
return skills if skills.key?(key)
|
|
935
|
+
|
|
936
|
+
dir = File.dirname(entry)
|
|
937
|
+
content = File.read(entry)
|
|
938
|
+
parsed = parse_skill_frontmatter(content: content)
|
|
939
|
+
fm = parsed[:frontmatter]
|
|
940
|
+
desc = (fm['description'] || fm[:description]).to_s.strip
|
|
941
|
+
desc = parsed[:body].to_s.lines.first.to_s.strip.sub(/^#+\s*/, '')[0, 200] if desc.empty?
|
|
942
|
+
scripts = Dir.glob(File.join(dir, 'scripts', '*.rb'))
|
|
943
|
+
fmt = module_skill_rel?(rel: rel_dir) ? :module : :agentskills
|
|
944
|
+
meta = {
|
|
945
|
+
type: scripts.any? ? :ruby : :instruction,
|
|
946
|
+
format: fmt,
|
|
947
|
+
path: entry,
|
|
948
|
+
dir: dir,
|
|
949
|
+
content: content,
|
|
950
|
+
description: desc,
|
|
951
|
+
frontmatter: fm,
|
|
952
|
+
references: parse_skill_references(content: content),
|
|
953
|
+
allowed_tools: Array(fm['allowed-tools'] || fm[:'allowed-tools'] || fm['allowed_tools'])
|
|
954
|
+
}
|
|
955
|
+
scripts.each do |rb|
|
|
956
|
+
require rb
|
|
957
|
+
rescue StandardError => e
|
|
958
|
+
meta[:loaded] = false
|
|
959
|
+
meta[:error] = e.message
|
|
960
|
+
end
|
|
961
|
+
meta[:loaded] = true unless meta.key?(:loaded) || scripts.empty?
|
|
962
|
+
skills[key] = meta
|
|
963
|
+
skills
|
|
912
964
|
end
|
|
913
965
|
|
|
914
966
|
# Supported Method Parameters::
|
|
@@ -919,7 +971,8 @@ module PWN
|
|
|
919
971
|
# Loads skills into the PWN::Skills constant. Two on-disk shapes are
|
|
920
972
|
# accepted so upgrades are seamless:
|
|
921
973
|
#
|
|
922
|
-
# agentskills.io → <root
|
|
974
|
+
# agentskills.io → <root>/**/SKILL.md (recursive; written by write_skill)
|
|
975
|
+
# module skills → <root>/pwn/**/SKILL.md
|
|
923
976
|
# legacy flat → <root>/<name>.{md,txt,rb,skill,yml,yaml}
|
|
924
977
|
#
|
|
925
978
|
# Each entry: { type:, format:, path:, dir:, content:, description:,
|
|
@@ -931,38 +984,8 @@ module PWN
|
|
|
931
984
|
skills = {}
|
|
932
985
|
return skills unless pwn_skills_path && Dir.exist?(pwn_skills_path.to_s)
|
|
933
986
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
dir = File.dirname(entry)
|
|
937
|
-
key = File.basename(dir).to_sym
|
|
938
|
-
content = File.read(entry)
|
|
939
|
-
parsed = parse_skill_frontmatter(content: content)
|
|
940
|
-
fm = parsed[:frontmatter]
|
|
941
|
-
desc = (fm['description'] || fm[:description]).to_s.strip
|
|
942
|
-
desc = parsed[:body].to_s.lines.first.to_s.strip.sub(/^#+\s*/, '')[0, 200] if desc.empty?
|
|
943
|
-
scripts = Dir.glob(File.join(dir, 'scripts', '*.rb'))
|
|
944
|
-
|
|
945
|
-
meta = {
|
|
946
|
-
type: scripts.any? ? :ruby : :instruction,
|
|
947
|
-
format: :agentskills,
|
|
948
|
-
path: entry,
|
|
949
|
-
dir: dir,
|
|
950
|
-
content: content,
|
|
951
|
-
description: desc,
|
|
952
|
-
frontmatter: fm,
|
|
953
|
-
references: parse_skill_references(content: content),
|
|
954
|
-
allowed_tools: Array(fm['allowed-tools'] || fm[:'allowed-tools'] || fm['allowed_tools'])
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
scripts.each do |rb|
|
|
958
|
-
require rb
|
|
959
|
-
rescue StandardError => e
|
|
960
|
-
meta[:loaded] = false
|
|
961
|
-
meta[:error] = e.message
|
|
962
|
-
end
|
|
963
|
-
meta[:loaded] = true unless meta.key?(:loaded) || scripts.empty?
|
|
964
|
-
|
|
965
|
-
skills[key] = meta
|
|
987
|
+
each_skill_md(root: pwn_skills_path) do |entry, rel_dir|
|
|
988
|
+
ingest_skill_md(skills: skills, entry: entry, rel_dir: rel_dir)
|
|
966
989
|
end
|
|
967
990
|
|
|
968
991
|
# ── legacy flat files (backward-compat shim) ──────────────────────
|
|
@@ -998,30 +1021,6 @@ module PWN
|
|
|
998
1021
|
end
|
|
999
1022
|
end
|
|
1000
1023
|
|
|
1001
|
-
# ── generated module skills: ~/.pwn/skills/pwn/**/SKILL.md ──────
|
|
1002
|
-
Dir.glob(File.join(pwn_skills_path, 'pwn', '**', 'SKILL.md')).each do |skill_file|
|
|
1003
|
-
next if skill_file.include?('/references/')
|
|
1004
|
-
|
|
1005
|
-
rel = skill_file.delete_prefix("#{pwn_skills_path}/").sub(%r{/SKILL\.md\z}, '')
|
|
1006
|
-
key = rel.to_sym
|
|
1007
|
-
next if skills.key?(key)
|
|
1008
|
-
|
|
1009
|
-
content = File.read(skill_file)
|
|
1010
|
-
parsed = parse_skill_frontmatter(content: content)
|
|
1011
|
-
desc = (parsed[:frontmatter]['description'] || parsed[:frontmatter][:description]).to_s.strip
|
|
1012
|
-
desc = parsed[:body].to_s.lines.first.to_s.strip.sub(/^#+\s*/, '')[0, 200] if desc.empty?
|
|
1013
|
-
skills[key] = {
|
|
1014
|
-
type: :instruction,
|
|
1015
|
-
format: :module,
|
|
1016
|
-
path: skill_file,
|
|
1017
|
-
dir: File.dirname(skill_file),
|
|
1018
|
-
content: content,
|
|
1019
|
-
description: desc,
|
|
1020
|
-
frontmatter: parsed[:frontmatter],
|
|
1021
|
-
references: parse_skill_references(content: content)
|
|
1022
|
-
}
|
|
1023
|
-
end
|
|
1024
|
-
|
|
1025
1024
|
PWN.send(:remove_const, :Skills) if PWN.const_defined?(:Skills)
|
|
1026
1025
|
PWN.const_set(:Skills, skills.freeze)
|
|
1027
1026
|
skills
|
data/lib/pwn/module_skills.rb
CHANGED
|
@@ -22,7 +22,7 @@ module PWN
|
|
|
22
22
|
|
|
23
23
|
found.concat(parse_file(path: path, lib_root: lib))
|
|
24
24
|
end
|
|
25
|
-
found.uniq { |h| h[:source] }
|
|
25
|
+
found.uniq { |h| h[:source] }.each { |rec| enrich_live_methods!(rec: rec) }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
public_class_method def self.relpath(opts = {})
|
|
@@ -132,6 +132,48 @@ module PWN
|
|
|
132
132
|
{ modules: mods.length, written: written, removed: removed, dest: dest }
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
public_class_method def self.drift(opts = {})
|
|
136
|
+
dest = opts[:dest].to_s
|
|
137
|
+
dest = File.expand_path('../../etc/default_skills', __dir__) if dest.empty?
|
|
138
|
+
dest = File.dirname(dest) if File.basename(dest) == 'pwn'
|
|
139
|
+
lib = opts[:lib_root].to_s
|
|
140
|
+
lib = File.expand_path('..', LIB_ROOT) if lib.empty?
|
|
141
|
+
mods = enumerate(lib_root: lib)
|
|
142
|
+
missing = []
|
|
143
|
+
stale = []
|
|
144
|
+
keep = {}
|
|
145
|
+
mods.each do |rec|
|
|
146
|
+
path = skill_abs(dest: dest, module: rec)
|
|
147
|
+
dir = File.dirname(path)
|
|
148
|
+
keep[path] = true
|
|
149
|
+
if preserved_skill?(path: path)
|
|
150
|
+
Dir.glob(File.join(dir, 'references', '*.md')).each { |p| keep[p] = true }
|
|
151
|
+
next
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if File.file?(path)
|
|
155
|
+
stale << path if File.read(path) != render(module: rec)
|
|
156
|
+
else
|
|
157
|
+
missing << path
|
|
158
|
+
end
|
|
159
|
+
Array(rec[:references]).each do |ref|
|
|
160
|
+
keep[File.join(dir, 'references', ref[:file])] = true
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
orphans = []
|
|
164
|
+
root = File.join(dest, 'pwn')
|
|
165
|
+
if Dir.exist?(root)
|
|
166
|
+
Dir.glob(File.join(root, '**', '*')).each do |path|
|
|
167
|
+
next unless File.file?(path)
|
|
168
|
+
next if File.basename(path).start_with?('.')
|
|
169
|
+
next if keep[path]
|
|
170
|
+
|
|
171
|
+
orphans << path
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
{ missing: missing, stale: stale, orphans: orphans, modules: mods.length }
|
|
175
|
+
end
|
|
176
|
+
|
|
135
177
|
public_class_method def self.install(opts = {})
|
|
136
178
|
root = opts[:pwn_skills_path]
|
|
137
179
|
root = PWN::Config.pwn_skills_path if root.to_s.empty? && defined?(PWN::Config)
|
|
@@ -333,6 +375,33 @@ module PWN
|
|
|
333
375
|
.downcase
|
|
334
376
|
end
|
|
335
377
|
|
|
378
|
+
private_class_method def self.enrich_live_methods!(opts = {})
|
|
379
|
+
rec = opts[:rec]
|
|
380
|
+
return rec unless rec.is_a?(Hash)
|
|
381
|
+
|
|
382
|
+
parsed = Array(rec[:methods]).map(&:to_s)
|
|
383
|
+
extra = live_class_methods(const: rec[:const]) - parsed
|
|
384
|
+
rec[:methods] = parsed + extra.sort
|
|
385
|
+
rec
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private_class_method def self.live_class_methods(opts = {})
|
|
389
|
+
const = opts[:const].to_s
|
|
390
|
+
return [] if const.empty?
|
|
391
|
+
|
|
392
|
+
mod = const.split('::').inject(Object) do |cur, part|
|
|
393
|
+
return [] unless cur.is_a?(Module)
|
|
394
|
+
return [] unless cur.const_defined?(part)
|
|
395
|
+
|
|
396
|
+
cur.const_get(part)
|
|
397
|
+
end
|
|
398
|
+
return [] unless mod.is_a?(Module)
|
|
399
|
+
|
|
400
|
+
mod.singleton_methods(false).map(&:to_s)
|
|
401
|
+
rescue StandardError
|
|
402
|
+
[]
|
|
403
|
+
end
|
|
404
|
+
|
|
336
405
|
private_class_method def self.example_call(opts = {})
|
|
337
406
|
const = opts[:const]
|
|
338
407
|
methods = Array(opts[:methods]) - %w[help authors]
|
|
@@ -370,6 +439,7 @@ module PWN
|
|
|
370
439
|
USAGE:
|
|
371
440
|
#{self}.enumerate
|
|
372
441
|
#{self}.refresh!
|
|
442
|
+
#{self}.drift
|
|
373
443
|
#{self}.install(pwn_skills_path: '~/.pwn/skills')
|
|
374
444
|
#{self}.authors
|
|
375
445
|
USAGE
|
data/lib/pwn/plugins/github.rb
CHANGED
|
@@ -85,7 +85,7 @@ module PWN
|
|
|
85
85
|
rescue RestClient::Forbidden, RestClient::Unauthorized => e
|
|
86
86
|
@@logger.error("GitHub #{e.class}: #{e.response&.body}")
|
|
87
87
|
raise e
|
|
88
|
-
rescue
|
|
88
|
+
rescue StandardError => e
|
|
89
89
|
raise e
|
|
90
90
|
end
|
|
91
91
|
|
data/lib/pwn/plugins/log.rb
CHANGED
|
@@ -23,12 +23,8 @@ module PWN
|
|
|
23
23
|
PWN::Banner
|
|
24
24
|
].freeze
|
|
25
25
|
DEFAULT_TRACE_PREFIXES = %w[
|
|
26
|
-
PWN::AI
|
|
27
|
-
PWN::
|
|
28
|
-
PWN::Sessions
|
|
29
|
-
PWN::Config
|
|
30
|
-
PWN::Cron
|
|
31
|
-
PWN::Plugins
|
|
26
|
+
PWN::AI::Agent::Loop
|
|
27
|
+
PWN::AI::Agent::Dispatch
|
|
32
28
|
].freeze
|
|
33
29
|
SECRET_KEY_RX = /password|passwd|secret|token|api[_-]?key|authorization|bearer|cookie|session[_-]?id|private[_-]?key|decryptor|credential|ssh[_-]?key|client[_-]?secret|refresh[_-]?token|access[_-]?token|id[_-]?token|vault|csrf/i
|
|
34
30
|
SECRET_VALUE_RX = %r{
|
|
@@ -150,10 +146,13 @@ module PWN
|
|
|
150
146
|
# /tmp/pwn-ai-DEBUG-<session_id>-RN.log via next_request_log!.
|
|
151
147
|
# opts[:path] is a test override that writes to one file.
|
|
152
148
|
public_class_method def self.start_debug(opts = {})
|
|
149
|
+
want_trace = opts[:trace] == true
|
|
153
150
|
if debug_enabled? && opts[:path].to_s.empty?
|
|
154
151
|
@debug_tee = opts[:tee] if opts.key?(:tee)
|
|
155
152
|
sid = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
156
153
|
@debug_session_id = sid unless sid.empty?
|
|
154
|
+
@debug_step_io = opts[:step_io] if opts.key?(:step_io)
|
|
155
|
+
apply_trace!(trace: want_trace, prefixes: opts[:prefixes]) if want_trace
|
|
157
156
|
return @debug_path
|
|
158
157
|
end
|
|
159
158
|
|
|
@@ -165,18 +164,23 @@ module PWN
|
|
|
165
164
|
@debug_session_id = sanitize_debug_session_id(session_id: opts[:session_id])
|
|
166
165
|
@debug_req_n = 0
|
|
167
166
|
@debug_request_open = false
|
|
167
|
+
@debug_step_io = opts[:step_io]
|
|
168
168
|
if path.empty?
|
|
169
169
|
close_debug_file!
|
|
170
170
|
@debug_path = nil
|
|
171
171
|
else
|
|
172
172
|
open_debug_file!(path: path)
|
|
173
173
|
end
|
|
174
|
-
|
|
174
|
+
apply_trace!(trace: want_trace, prefixes: opts[:prefixes])
|
|
175
175
|
install_stderr_tee!
|
|
176
176
|
progress(msg: 'debug session start', which_self: self) if @debug_file
|
|
177
177
|
@debug_path
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
+
public_class_method def self.trace_enabled?
|
|
181
|
+
@debug_trace == true
|
|
182
|
+
end
|
|
183
|
+
|
|
180
184
|
public_class_method def self.next_request_log!(opts = {})
|
|
181
185
|
return unless debug_enabled?
|
|
182
186
|
return @debug_path if @debug_request_open && opts[:force] != true
|
|
@@ -226,6 +230,8 @@ module PWN
|
|
|
226
230
|
@debug_tee = nil
|
|
227
231
|
@debug_tui_quiet = false
|
|
228
232
|
@debug_enabled = false
|
|
233
|
+
@debug_step = false
|
|
234
|
+
@debug_step_io = nil
|
|
229
235
|
remove_stderr_tee!
|
|
230
236
|
path
|
|
231
237
|
end
|
|
@@ -574,6 +580,16 @@ module PWN
|
|
|
574
580
|
opts[:key].to_s.match?(SECRET_KEY_RX)
|
|
575
581
|
end
|
|
576
582
|
|
|
583
|
+
private_class_method def self.opaque_secret?(opts = {})
|
|
584
|
+
return false unless opts[:key].to_s == 'value'
|
|
585
|
+
|
|
586
|
+
val = opts[:value]
|
|
587
|
+
return false unless val.is_a?(String)
|
|
588
|
+
return false if val.length < 24
|
|
589
|
+
|
|
590
|
+
val.match?(%r{\A[A-Za-z0-9\-_/=+.]+\z})
|
|
591
|
+
end
|
|
592
|
+
|
|
577
593
|
private_class_method def self.sanitize_debug_text(opts = {})
|
|
578
594
|
text = opts[:text].to_s
|
|
579
595
|
text = text.gsub(SECRET_VALUE_RX, '[REDACTED]')
|
|
@@ -584,6 +600,7 @@ module PWN
|
|
|
584
600
|
val = opts[:value]
|
|
585
601
|
key = opts[:key]
|
|
586
602
|
return '[REDACTED]' if secret_key?(key: key)
|
|
603
|
+
return '[REDACTED]' if opaque_secret?(key: key, value: val)
|
|
587
604
|
return format_debug_hash(hash: val) if val.is_a?(Hash)
|
|
588
605
|
return '[REDACTED]' if val.is_a?(String) && val.match?(SECRET_VALUE_RX)
|
|
589
606
|
|
|
@@ -667,6 +684,7 @@ module PWN
|
|
|
667
684
|
nil
|
|
668
685
|
end
|
|
669
686
|
@debug_tp.enable
|
|
687
|
+
@debug_trace = true
|
|
670
688
|
@debug_tp
|
|
671
689
|
end
|
|
672
690
|
|
|
@@ -675,6 +693,50 @@ module PWN
|
|
|
675
693
|
|
|
676
694
|
@debug_tp&.disable
|
|
677
695
|
@debug_tp = nil
|
|
696
|
+
@debug_trace = false
|
|
697
|
+
@debug_step = false
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
public_class_method def self.wait_trace_step!(opts = {})
|
|
701
|
+
return unless debug_enabled?
|
|
702
|
+
return unless @debug_step
|
|
703
|
+
return if opts[:nested]
|
|
704
|
+
|
|
705
|
+
io = @debug_step_io || $stdin
|
|
706
|
+
return unless io.respond_to?(:gets)
|
|
707
|
+
return if io.equal?($stdin) && !$stdin.tty?
|
|
708
|
+
|
|
709
|
+
label = opts[:label].to_s
|
|
710
|
+
label = 'loop' if label.empty?
|
|
711
|
+
progress(msg: "trace step #{label} — Press ENTER to Continue...", which_self: self, tee: nil)
|
|
712
|
+
prompt_trace_enter!
|
|
713
|
+
io.gets
|
|
714
|
+
true
|
|
715
|
+
rescue StandardError
|
|
716
|
+
nil
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
private_class_method def self.prompt_trace_enter!(opts = {})
|
|
720
|
+
return if opts[:skip]
|
|
721
|
+
|
|
722
|
+
tee = @debug_tee
|
|
723
|
+
return unless tee.respond_to?(:puts)
|
|
724
|
+
|
|
725
|
+
tee.print("\e[31mPress ENTER to Continue...\e[0m")
|
|
726
|
+
tee.flush if tee.respond_to?(:flush)
|
|
727
|
+
$stdout.flush if $stdout.respond_to?(:flush)
|
|
728
|
+
rescue StandardError
|
|
729
|
+
nil
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
private_class_method def self.apply_trace!(opts = {})
|
|
733
|
+
if opts[:trace]
|
|
734
|
+
@debug_step = true
|
|
735
|
+
start_trace!(prefixes: opts[:prefixes])
|
|
736
|
+
@debug_trace = true
|
|
737
|
+
else
|
|
738
|
+
@debug_step = false
|
|
739
|
+
end
|
|
678
740
|
end
|
|
679
741
|
|
|
680
742
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
@@ -703,7 +765,8 @@ module PWN
|
|
|
703
765
|
#{self}.progress(msg: 'stage', which_self: self)
|
|
704
766
|
#{self}.finish_request_log!(iter: 1, tools_called: 0, engine_s: 0.2, final_chars: 12)
|
|
705
767
|
#{self}.stop_debug
|
|
706
|
-
# start_debug(trace: true) enables
|
|
768
|
+
# start_debug(trace: true) enables TracePoint + ENTER after each Loop step
|
|
769
|
+
# (toggle-trace). Stored on Pry.config.pwn_ai_trace, not Env.
|
|
707
770
|
"
|
|
708
771
|
end
|
|
709
772
|
end
|