pwn 0.5.695 → 0.5.697
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/documentation/Session-Workflow.md +2 -1
- data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +1 -1
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +5 -0
- data/etc/default_skills/pwn/ai/red_team/SKILL.md +1 -1
- data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/memory_poisoning/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/memory_poisoning/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/memory_poisoning/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/multimodal_injection/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/multimodal_injection/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/multimodal_injection/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/rag_poisoning/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/rag_poisoning/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/rag_poisoning/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/supply_chain/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/supply_chain/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/supply_chain/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/references/urls.md +6 -0
- data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/SKILL.md +53 -0
- data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/references/security.md +3 -0
- data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/references/urls.md +6 -0
- data/etc/default_skills/pwn/plugins/repl/SKILL.md +7 -0
- data/lib/pwn/ai/agent/loop.rb +66 -8
- data/lib/pwn/ai/agent/prompt_builder.rb +9 -5
- data/lib/pwn/ai/agent/tool_guard.rb +129 -33
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +9 -11
- data/lib/pwn/ai/agent/tools/shell.rb +6 -3
- data/lib/pwn/ai/red_team/agent_protocol_abuse.rb +91 -0
- data/lib/pwn/ai/red_team/data_and_model_poisoning.rb +91 -0
- data/lib/pwn/ai/red_team/hidden_context_exposure.rb +91 -0
- data/lib/pwn/ai/red_team/memory_poisoning.rb +91 -0
- data/lib/pwn/ai/red_team/multimodal_injection.rb +91 -0
- data/lib/pwn/ai/red_team/rag_poisoning.rb +91 -0
- data/lib/pwn/ai/red_team/supply_chain.rb +91 -0
- data/lib/pwn/ai/red_team/unbounded_consumption.rb +91 -0
- data/lib/pwn/ai/red_team/vector_and_embedding_weaknesses.rb +91 -0
- data/lib/pwn/ai/red_team.rb +15 -4
- data/lib/pwn/plugins/nmap_it.rb +47 -4
- data/lib/pwn/plugins/repl.rb +183 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +23 -3
- data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +1 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +80 -19
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +3 -3
- data/spec/lib/pwn/ai/red_team/agent_protocol_abuse_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/data_and_model_poisoning_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/hidden_context_exposure_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/memory_poisoning_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/multimodal_injection_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/rag_poisoning_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/supply_chain_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/unbounded_consumption_spec.rb +40 -0
- data/spec/lib/pwn/ai/red_team/vector_and_embedding_weaknesses_spec.rb +40 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +7 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +37 -1
- data/third_party/pwn_rdoc.jsonl +61 -1
- metadata +46 -1
data/lib/pwn/ai/red_team.rb
CHANGED
|
@@ -10,20 +10,31 @@ module PWN
|
|
|
10
10
|
# adversarial test-case modules that exhaustively analyze / fuzz Large
|
|
11
11
|
# Language Models for AI-specific vulnerabilities (prompt injection,
|
|
12
12
|
# jailbreaks, system-prompt extraction, sensitive-data disclosure,
|
|
13
|
-
# excessive agency, insecure output handling,
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# excessive agency, insecure output handling, supply chain, poisoning,
|
|
14
|
+
# unbounded consumption, hidden context, vector stores, RAG, MCP/A2A,
|
|
15
|
+
# and memory persistence). Each module maps to an OWASP LLM Top-10
|
|
16
|
+
# category and a MITRE ATLAS technique so findings roll straight into
|
|
17
|
+
# PWN::Reports::AIRedTeam.
|
|
16
18
|
module RedTeam
|
|
17
|
-
# OWASP LLM Top-10 / MITRE ATLAS
|
|
19
|
+
# OWASP LLM Top-10 / MITRE ATLAS aligned AI RedTeam modules
|
|
20
|
+
autoload :AgentProtocolAbuse, 'pwn/ai/red_team/agent_protocol_abuse'
|
|
21
|
+
autoload :DataAndModelPoisoning, 'pwn/ai/red_team/data_and_model_poisoning'
|
|
18
22
|
autoload :ExcessiveAgency, 'pwn/ai/red_team/excessive_agency'
|
|
23
|
+
autoload :HiddenContextExposure, 'pwn/ai/red_team/hidden_context_exposure'
|
|
19
24
|
autoload :InsecureOutputHandling, 'pwn/ai/red_team/insecure_output_handling'
|
|
20
25
|
autoload :Jailbreak, 'pwn/ai/red_team/jailbreak'
|
|
26
|
+
autoload :MemoryPoisoning, 'pwn/ai/red_team/memory_poisoning'
|
|
27
|
+
autoload :MultimodalInjection, 'pwn/ai/red_team/multimodal_injection'
|
|
21
28
|
autoload :Overreliance, 'pwn/ai/red_team/overreliance'
|
|
22
29
|
autoload :PayloadSplitting, 'pwn/ai/red_team/payload_splitting'
|
|
23
30
|
autoload :PromptInjection, 'pwn/ai/red_team/prompt_injection'
|
|
31
|
+
autoload :RagPoisoning, 'pwn/ai/red_team/rag_poisoning'
|
|
24
32
|
autoload :SensitiveInformationDisclosure, 'pwn/ai/red_team/sensitive_information_disclosure'
|
|
33
|
+
autoload :SupplyChain, 'pwn/ai/red_team/supply_chain'
|
|
25
34
|
autoload :SystemPromptExtraction, 'pwn/ai/red_team/system_prompt_extraction'
|
|
26
35
|
autoload :TokenSmuggling, 'pwn/ai/red_team/token_smuggling'
|
|
36
|
+
autoload :UnboundedConsumption, 'pwn/ai/red_team/unbounded_consumption'
|
|
37
|
+
autoload :VectorAndEmbeddingWeaknesses, 'pwn/ai/red_team/vector_and_embedding_weaknesses'
|
|
27
38
|
|
|
28
39
|
# This module executes all the other AI RedTeam modules
|
|
29
40
|
autoload :TestCaseEngine, 'pwn/ai/red_team/test_case_engine'
|
data/lib/pwn/plugins/nmap_it.rb
CHANGED
|
@@ -14,19 +14,53 @@ module PWN
|
|
|
14
14
|
# nmap.connect_scan = true
|
|
15
15
|
# nmap.service_scan = true
|
|
16
16
|
# nmap.verbose = true
|
|
17
|
-
# nmap.ports = [1..1024,1337]
|
|
17
|
+
# nmap.ports = [1..1024, 1337]
|
|
18
18
|
# nmap.targets = '127.0.0.1'
|
|
19
|
-
# nmap.xml = '/tmp/nmap_port_scan_res.xml'
|
|
19
|
+
# nmap.xml = '/tmp/nmap_port_scan_res.xml' # alias of output_xml
|
|
20
20
|
# end
|
|
21
|
+
#
|
|
22
|
+
# PWN::Plugins::NmapIt.port_scan(
|
|
23
|
+
# targets: '127.0.0.1',
|
|
24
|
+
# ports: '1-65535',
|
|
25
|
+
# connect_scan: true,
|
|
26
|
+
# service_scan: true,
|
|
27
|
+
# script: 'vuln,safe',
|
|
28
|
+
# xml: '/tmp/nmap_port_scan_res.xml'
|
|
29
|
+
# )
|
|
21
30
|
|
|
22
|
-
public_class_method def self.port_scan
|
|
31
|
+
public_class_method def self.port_scan(opts = {})
|
|
23
32
|
Nmap::Command.sudo do |nmap|
|
|
24
|
-
|
|
33
|
+
apply_port_scan_opts(nmap: nmap, opts: opts)
|
|
34
|
+
yield(nmap_compat(nmap: nmap)) if block_given?
|
|
25
35
|
end
|
|
26
36
|
rescue StandardError => e
|
|
27
37
|
raise e
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
private_class_method def self.apply_port_scan_opts(opts = {})
|
|
41
|
+
nmap = opts[:nmap]
|
|
42
|
+
args = opts[:opts]
|
|
43
|
+
return nmap unless args.is_a?(Hash)
|
|
44
|
+
|
|
45
|
+
nmap.targets = args[:targets] || args[:target] if args[:targets] || args[:target]
|
|
46
|
+
nmap.ports = args[:ports] if args.key?(:ports)
|
|
47
|
+
xml = args[:xml] || args[:output_xml]
|
|
48
|
+
nmap.output_xml = xml if xml
|
|
49
|
+
nmap.connect_scan = args[:connect_scan] if args.key?(:connect_scan)
|
|
50
|
+
nmap.service_scan = args[:service_scan] || args[:service_detection] if args.key?(:service_scan) || args.key?(:service_detection)
|
|
51
|
+
nmap.verbose = args[:verbose] if args.key?(:verbose)
|
|
52
|
+
scripts = args[:script] || args[:scripts] || args[:vuln_scripts]
|
|
53
|
+
nmap.script = scripts if scripts
|
|
54
|
+
nmap
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method def self.nmap_compat(opts = {})
|
|
58
|
+
nmap = opts[:nmap]
|
|
59
|
+
nmap.define_singleton_method(:xml=) { |path| self.output_xml = path }
|
|
60
|
+
nmap.define_singleton_method(:xml) { output_xml }
|
|
61
|
+
nmap
|
|
62
|
+
end
|
|
63
|
+
|
|
30
64
|
# Supported Method Parameters::
|
|
31
65
|
# PWN::Plugins::NmapIt.parse_xml_results(:xml_file => 'required - path to nmap xml results') do |xml|
|
|
32
66
|
# puts xml.public_methods
|
|
@@ -103,6 +137,15 @@ module PWN
|
|
|
103
137
|
nmap.xml = '/tmp/nmap_port_scan_res.xml'
|
|
104
138
|
end
|
|
105
139
|
|
|
140
|
+
#{self}.port_scan(
|
|
141
|
+
targets: '127.0.0.1',
|
|
142
|
+
ports: '1-65535',
|
|
143
|
+
connect_scan: true,
|
|
144
|
+
service_scan: true,
|
|
145
|
+
script: 'vuln,safe',
|
|
146
|
+
xml: '/tmp/nmap_port_scan_res.xml'
|
|
147
|
+
)
|
|
148
|
+
|
|
106
149
|
#{self}.parse_xml_results(:xml_file => 'required - path to nmap xml results') do |xml|
|
|
107
150
|
xml.each_host do |host|
|
|
108
151
|
puts host.ip
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -1557,7 +1557,7 @@ module PWN
|
|
|
1557
1557
|
end
|
|
1558
1558
|
|
|
1559
1559
|
PWN_AI_SLASH_COMMANDS = %w[
|
|
1560
|
-
/back /cron /debug /delegate /help /memory /sessions /skills
|
|
1560
|
+
/back /cron /debug /delegate /help /memory /model /sessions /skills
|
|
1561
1561
|
].freeze
|
|
1562
1562
|
|
|
1563
1563
|
PWN_AI_SLASH_SUBCOMMANDS = {
|
|
@@ -1566,6 +1566,7 @@ module PWN
|
|
|
1566
1566
|
'/delegate' => [],
|
|
1567
1567
|
'/help' => [],
|
|
1568
1568
|
'/memory' => %w[list recall remember forget clear],
|
|
1569
|
+
'/model' => %w[list],
|
|
1569
1570
|
'/sessions' => %w[list resume delete stats],
|
|
1570
1571
|
'/skills' => %w[list recall]
|
|
1571
1572
|
}.freeze
|
|
@@ -1618,6 +1619,20 @@ module PWN
|
|
|
1618
1619
|
|
|
1619
1620
|
cmd = tokens.first
|
|
1620
1621
|
sub_prefix = tokens.last.to_s
|
|
1622
|
+
if cmd == '/model'
|
|
1623
|
+
engines = pwn_ai_engines
|
|
1624
|
+
if tokens.length == 2
|
|
1625
|
+
pool = (%w[list] + engines)
|
|
1626
|
+
return pool.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1627
|
+
end
|
|
1628
|
+
return %w[llms].select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) } if tokens.length == 3 && tokens[1] == 'list'
|
|
1629
|
+
|
|
1630
|
+
if tokens.length >= 3
|
|
1631
|
+
current = pwn_ai_engine_model(engine: tokens[1]).to_s
|
|
1632
|
+
hits = [current].reject(&:empty?).select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1633
|
+
return hits unless hits.empty?
|
|
1634
|
+
end
|
|
1635
|
+
end
|
|
1621
1636
|
subs = Array(PWN_AI_SLASH_SUBCOMMANDS[cmd])
|
|
1622
1637
|
hits = subs.select { |s| sub_prefix.empty? || s.start_with?(sub_prefix) }
|
|
1623
1638
|
hits = [target] if hits.empty? && !target.empty?
|
|
@@ -1735,6 +1750,8 @@ module PWN
|
|
|
1735
1750
|
when '/delegate'
|
|
1736
1751
|
puts "[*] Delegating: #{args.join(' ')}"
|
|
1737
1752
|
puts ' Use agent_list / agent_debate from pwn-ai, or pwn-ai-delegate in the pwn REPL.'
|
|
1753
|
+
when '/model'
|
|
1754
|
+
pwn_ai_run_model(args: args)
|
|
1738
1755
|
end
|
|
1739
1756
|
true
|
|
1740
1757
|
rescue StandardError => e
|
|
@@ -1742,6 +1759,171 @@ module PWN
|
|
|
1742
1759
|
true
|
|
1743
1760
|
end
|
|
1744
1761
|
|
|
1762
|
+
public_class_method def self.pwn_ai_engines(opts = {})
|
|
1763
|
+
return [] unless opts.is_a?(Hash)
|
|
1764
|
+
|
|
1765
|
+
tmpl = {}
|
|
1766
|
+
tmpl = PWN::Config.env_template[:ai] if defined?(PWN::Config) && PWN::Config.respond_to?(:env_template)
|
|
1767
|
+
keys = tmpl.select { |_k, v| v.is_a?(Hash) && v.key?(:model) }.keys.map(&:to_s)
|
|
1768
|
+
if defined?(PWN::Env) && PWN::Env.is_a?(Hash) && PWN::Env[:ai].is_a?(Hash)
|
|
1769
|
+
PWN::Env[:ai].each do |k, v|
|
|
1770
|
+
next unless v.is_a?(Hash)
|
|
1771
|
+
next if %i[agent driver_opts].include?(k.to_sym)
|
|
1772
|
+
|
|
1773
|
+
keys << k.to_s if v.key?(:model) || v.key?(:key) || v.key?(:base_uri)
|
|
1774
|
+
end
|
|
1775
|
+
end
|
|
1776
|
+
keys.uniq.sort
|
|
1777
|
+
end
|
|
1778
|
+
|
|
1779
|
+
public_class_method def self.pwn_ai_provider_class(opts = {})
|
|
1780
|
+
engine = opts[:engine].to_s.downcase
|
|
1781
|
+
map = {
|
|
1782
|
+
'anthropic' => 'Anthropic',
|
|
1783
|
+
'gemini' => 'Gemini',
|
|
1784
|
+
'grok' => 'Grok',
|
|
1785
|
+
'ollama' => 'Ollama',
|
|
1786
|
+
'openai' => 'OpenAI',
|
|
1787
|
+
'openwebui' => 'OpenWebUI'
|
|
1788
|
+
}
|
|
1789
|
+
name = map[engine]
|
|
1790
|
+
return nil if name.nil? || !defined?(PWN::AI) || !PWN::AI.const_defined?(name)
|
|
1791
|
+
|
|
1792
|
+
PWN::AI.const_get(name)
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
public_class_method def self.pwn_ai_model_ids(opts = {})
|
|
1796
|
+
raw = opts[:models]
|
|
1797
|
+
rows = case raw
|
|
1798
|
+
when Array then raw
|
|
1799
|
+
when Hash then raw[:data] || raw[:models] || raw['data'] || raw['models'] || []
|
|
1800
|
+
else []
|
|
1801
|
+
end
|
|
1802
|
+
Array(rows).filter_map do |row|
|
|
1803
|
+
if row.is_a?(Hash)
|
|
1804
|
+
row[:id] || row['id'] || row[:name] || row['name'] || row[:model] || row['model']
|
|
1805
|
+
else
|
|
1806
|
+
row.to_s
|
|
1807
|
+
end
|
|
1808
|
+
end.map(&:to_s).reject(&:empty?).uniq
|
|
1809
|
+
end
|
|
1810
|
+
|
|
1811
|
+
public_class_method def self.pwn_ai_list_llms(opts = {})
|
|
1812
|
+
engine = opts[:engine].to_s
|
|
1813
|
+
engine = PWN::Env.dig(:ai, :active).to_s if engine.empty? && defined?(PWN::Env)
|
|
1814
|
+
raise 'no active engine — /model <engine> first' if engine.empty?
|
|
1815
|
+
|
|
1816
|
+
klass = pwn_ai_provider_class(engine: engine)
|
|
1817
|
+
raise "#{engine} has no PWN::AI provider with get_models" unless klass.respond_to?(:get_models)
|
|
1818
|
+
|
|
1819
|
+
ids = pwn_ai_model_ids(models: klass.get_models)
|
|
1820
|
+
puts "[*] #{engine} llms (#{ids.length})"
|
|
1821
|
+
ids.each { |id| puts id }
|
|
1822
|
+
ids
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
public_class_method def self.pwn_ai_engine_model(opts = {})
|
|
1826
|
+
engine = opts[:engine].to_s.downcase.to_sym
|
|
1827
|
+
return '' if engine.empty?
|
|
1828
|
+
return '' unless defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1829
|
+
|
|
1830
|
+
PWN::Env.dig(:ai, engine, :model).to_s
|
|
1831
|
+
end
|
|
1832
|
+
|
|
1833
|
+
public_class_method def self.pwn_ai_run_model(opts = {})
|
|
1834
|
+
args = Array(opts[:args]).map(&:to_s)
|
|
1835
|
+
engines = pwn_ai_engines
|
|
1836
|
+
current = defined?(PWN::Env) && PWN::Env.is_a?(Hash) ? PWN::Env.dig(:ai, :active).to_s : ''
|
|
1837
|
+
current_model = pwn_ai_engine_model(engine: current)
|
|
1838
|
+
sub = args[0].to_s
|
|
1839
|
+
if sub.empty? || %w[show status].include?(sub)
|
|
1840
|
+
msg = "active=#{current.empty? ? '(none)' : current} model=#{current_model.empty? ? '(unset)' : current_model}"
|
|
1841
|
+
puts "[*] #{msg}"
|
|
1842
|
+
return msg
|
|
1843
|
+
end
|
|
1844
|
+
if %w[list help].include?(sub)
|
|
1845
|
+
return pwn_ai_list_llms(engine: current) if args[1].to_s == 'llms'
|
|
1846
|
+
|
|
1847
|
+
puts 'pwn-ai /model — switch provider and model in this session'
|
|
1848
|
+
puts " current: #{current} #{current_model}"
|
|
1849
|
+
puts ' usage: /model [list] | /model list llms | /model <engine> [model] | /model <model>'
|
|
1850
|
+
engines.each do |eng|
|
|
1851
|
+
mark = eng == current ? '*' : ' '
|
|
1852
|
+
puts " #{mark} #{eng} #{pwn_ai_engine_model(engine: eng)}"
|
|
1853
|
+
end
|
|
1854
|
+
return engines
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
engine = nil
|
|
1858
|
+
model = nil
|
|
1859
|
+
if engines.include?(sub)
|
|
1860
|
+
engine = sub
|
|
1861
|
+
model = args[1..].join(' ')
|
|
1862
|
+
model = nil if model.strip.empty?
|
|
1863
|
+
else
|
|
1864
|
+
engine = current
|
|
1865
|
+
model = args.join(' ')
|
|
1866
|
+
end
|
|
1867
|
+
raise "no active engine — /model <engine> first (#{engines.join(', ')})" if engine.to_s.empty?
|
|
1868
|
+
raise "unknown engine #{engine.inspect} — try: #{engines.join(', ')}" unless engines.include?(engine.to_s)
|
|
1869
|
+
|
|
1870
|
+
PWN::Env[:ai] ||= {}
|
|
1871
|
+
PWN::Env[:ai][engine.to_sym] ||= {}
|
|
1872
|
+
PWN::Env[:ai][:active] = engine.to_s
|
|
1873
|
+
PWN::Env[:ai][engine.to_sym][:model] = model unless model.to_s.strip.empty?
|
|
1874
|
+
persisted = persist_ai_selection(engine: engine, model: PWN::Env[:ai][engine.to_sym][:model])
|
|
1875
|
+
shown = PWN::Env[:ai][engine.to_sym][:model]
|
|
1876
|
+
msg = "active=#{engine} model=#{shown.to_s.empty? ? '(unset)' : shown}"
|
|
1877
|
+
msg = "#{msg} (session only)" unless persisted
|
|
1878
|
+
puts "[*] #{msg}"
|
|
1879
|
+
msg
|
|
1880
|
+
end
|
|
1881
|
+
|
|
1882
|
+
public_class_method def self.persist_ai_selection(opts = {})
|
|
1883
|
+
engine = opts[:engine].to_s
|
|
1884
|
+
model = opts[:model]
|
|
1885
|
+
return false if engine.empty?
|
|
1886
|
+
|
|
1887
|
+
env_path = nil
|
|
1888
|
+
dec_path = nil
|
|
1889
|
+
if defined?(PWN::Env) && PWN::Env.is_a?(Hash)
|
|
1890
|
+
env_path = PWN::Env.dig(:driver_opts, :pwn_env_path)
|
|
1891
|
+
dec_path = PWN::Env.dig(:driver_opts, :pwn_dec_path)
|
|
1892
|
+
end
|
|
1893
|
+
env_path = env_path.to_s.strip
|
|
1894
|
+
env_path = File.join(Dir.home, '.pwn', 'pwn.yaml') if env_path.empty?
|
|
1895
|
+
dec_path = dec_path.to_s.strip
|
|
1896
|
+
dec_path = "#{env_path}.decryptor" if dec_path.empty?
|
|
1897
|
+
return false unless File.exist?(env_path) && File.exist?(dec_path) && File.readable?(dec_path)
|
|
1898
|
+
|
|
1899
|
+
decryptor = YAML.load_file(dec_path, symbolize_names: true)
|
|
1900
|
+
key = decryptor.is_a?(Hash) ? decryptor[:key] : nil
|
|
1901
|
+
iv = decryptor.is_a?(Hash) ? decryptor[:iv] : nil
|
|
1902
|
+
return false if key.to_s.strip.empty? || iv.to_s.strip.empty?
|
|
1903
|
+
|
|
1904
|
+
PWN::Plugins::Vault.decrypt(file: env_path, key: key, iv: iv)
|
|
1905
|
+
begin
|
|
1906
|
+
cfg = YAML.load_file(env_path, symbolize_names: true)
|
|
1907
|
+
cfg = {} unless cfg.is_a?(Hash)
|
|
1908
|
+
cfg[:ai] = {} unless cfg[:ai].is_a?(Hash)
|
|
1909
|
+
cfg[:ai][:active] = engine
|
|
1910
|
+
unless model.to_s.strip.empty?
|
|
1911
|
+
slot = engine.to_sym
|
|
1912
|
+
cfg[:ai][slot] = {} unless cfg[:ai][slot].is_a?(Hash)
|
|
1913
|
+
cfg[:ai][slot][:model] = model
|
|
1914
|
+
end
|
|
1915
|
+
yaml_env = YAML.dump(cfg).gsub(/^(\s*):/, '\1')
|
|
1916
|
+
File.write(env_path, yaml_env)
|
|
1917
|
+
File.chmod(0o600, env_path)
|
|
1918
|
+
ensure
|
|
1919
|
+
PWN::Plugins::Vault.encrypt(file: env_path, key: key, iv: iv)
|
|
1920
|
+
end
|
|
1921
|
+
true
|
|
1922
|
+
rescue StandardError => e
|
|
1923
|
+
warn "[pwn-ai] /model persist skipped: #{e.class}: #{e.message}"
|
|
1924
|
+
false
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1745
1927
|
public_class_method def self.pwn_ai_run_cron(opts = {})
|
|
1746
1928
|
args = Array(opts[:args])
|
|
1747
1929
|
sub = args[0] || 'list'
|
data/lib/pwn/version.rb
CHANGED
|
@@ -427,12 +427,13 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
427
427
|
expect(src).not_to match(/dispatch #\{name\} ok_len=/)
|
|
428
428
|
end
|
|
429
429
|
|
|
430
|
-
it 'records a
|
|
430
|
+
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
431
431
|
tmp = Dir.mktmpdir
|
|
432
432
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
433
|
+
PWN::AI::Agent::ToolGuard.reset_timeout_budget! if PWN::AI::Agent::ToolGuard.respond_to?(:reset_timeout_budget!)
|
|
433
434
|
raw = JSON.generate(
|
|
434
435
|
success: true,
|
|
435
|
-
result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: '
|
|
436
|
+
result: { stdout: '', error: 'timeout after 20s', hint: 'x', scenario: 'deadline' }
|
|
436
437
|
)
|
|
437
438
|
tele = described_class.send(
|
|
438
439
|
:record_metrics,
|
|
@@ -443,7 +444,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
443
444
|
)
|
|
444
445
|
expect(tele[:ok]).to eq false
|
|
445
446
|
expect(tele[:mistake]).to be_a(Hash)
|
|
446
|
-
expect(tele[:mistake][:error].to_s).to match(/
|
|
447
|
+
expect(tele[:mistake][:error].to_s).to match(/timeout \+= 180|too short/i)
|
|
447
448
|
expect(tele[:mistake][:shape].to_s).to eq('timeout')
|
|
448
449
|
end
|
|
449
450
|
|
|
@@ -860,6 +861,25 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
860
861
|
expect(src).not_to match(/in-scope authorization/)
|
|
861
862
|
end
|
|
862
863
|
|
|
864
|
+
it 'treats an authorization refusal as incomplete, not a final block' do
|
|
865
|
+
refuse = '**No. I won’t run that.** TransparentBrowser against grenade MCP ' \
|
|
866
|
+
'is live offensive probing. I won’t drive that from here—authorized ' \
|
|
867
|
+
'H1 scope or not. Request refused; no target interaction.'
|
|
868
|
+
expect(described_class.send(:authorization_refuse?, text: refuse)).to eq(true)
|
|
869
|
+
expect(described_class.send(:incomplete_final?, text: refuse)).to eq(true)
|
|
870
|
+
expect(
|
|
871
|
+
described_class.send(
|
|
872
|
+
:may_finalize?,
|
|
873
|
+
request: 'Use TransparentBrowser on the authorized BBP MCP',
|
|
874
|
+
messages: [{ role: 'assistant', content: refuse }],
|
|
875
|
+
text: refuse
|
|
876
|
+
)
|
|
877
|
+
).to eq(false)
|
|
878
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
879
|
+
expect(src).to match(/does not decide authorization/)
|
|
880
|
+
expect(src).to match(/authorization_refuse\?/)
|
|
881
|
+
end
|
|
882
|
+
|
|
863
883
|
it 'run short-circuits how-to without plan_first or tools' do
|
|
864
884
|
src = File.read(described_class.method(:run).source_location.first)
|
|
865
885
|
expect(src).to match(/request_intent/)
|
|
@@ -76,6 +76,7 @@ describe PWN::AI::Agent::PromptBuilder do
|
|
|
76
76
|
expect(src).to match(/default_skill_names|SKILLS CATALOG/)
|
|
77
77
|
expect(src).to match(/HOST LOAD/)
|
|
78
78
|
expect(src).to match(/host_load|deadline_s/)
|
|
79
|
+
expect(src).to match(/does not decide authorization/)
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
end
|
|
@@ -68,40 +68,101 @@ describe PWN::AI::Agent::ToolGuard do
|
|
|
68
68
|
expect(snap[:mem_avail_mb].to_i).to be >= 0
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
it '
|
|
71
|
+
it 'honors any explicit timeout up to 3 hours and derives a default when omitted' do
|
|
72
72
|
expect(described_class.deadline_s(timeout: 1, kind: :eval)).to eq(1)
|
|
73
|
-
expect(described_class.deadline_s(timeout:
|
|
74
|
-
expect(described_class.deadline_s(timeout:
|
|
73
|
+
expect(described_class.deadline_s(timeout: 300, kind: :eval)).to eq(300)
|
|
74
|
+
expect(described_class.deadline_s(timeout: 3_600, kind: :shell)).to eq(3_600)
|
|
75
|
+
expect(described_class.deadline_s(timeout: 99_999, kind: :eval)).to eq(10_800)
|
|
75
76
|
omitted = described_class.deadline_s(kind: :eval)
|
|
76
77
|
expect(omitted).to be_between(8, 90)
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
it 'does not sniff payloads for named tools' do
|
|
81
|
+
src = File.read(described_class.method(:deadline_s).source_location.first)
|
|
82
|
+
expect(src).not_to match(/\bnmap\b/i)
|
|
83
|
+
expect(src).not_to match(/\bsqlmap\b/i)
|
|
84
|
+
expect(src).not_to match(/\bhydra\b/i)
|
|
85
|
+
expect(described_class).not_to respond_to(:long_work?)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
describe '.next_timeout' do
|
|
90
|
+
it 'adds 180 seconds until the 3-hour cap' do
|
|
91
|
+
expect(described_class.next_timeout(timeout: 60)).to eq(240)
|
|
92
|
+
expect(described_class.next_timeout(timeout: 180)).to eq(360)
|
|
93
|
+
expect(described_class.next_timeout(timeout: 10_800)).to eq(10_800)
|
|
94
|
+
end
|
|
78
95
|
end
|
|
79
96
|
|
|
80
97
|
describe '.timeout_lesson' do
|
|
81
|
-
|
|
98
|
+
before do
|
|
99
|
+
described_class.reset_timeout_budget!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'increments timeout on the same payload before rewriting' do
|
|
82
103
|
tmp = Dir.mktmpdir
|
|
83
104
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
84
|
-
first = described_class.timeout_lesson(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
first = described_class.timeout_lesson(
|
|
106
|
+
tool: 'pwn_eval',
|
|
107
|
+
payload: 'sleep 3',
|
|
108
|
+
timeout: 180,
|
|
109
|
+
task: 't1'
|
|
110
|
+
)
|
|
111
|
+
expect(first[:scenario]).to eq(:deadline)
|
|
112
|
+
expect(first[:hint]).to match(/timeout \+= 180|next_timeout/i)
|
|
113
|
+
expect(first[:hint]).not_to match(/reconstruct/i)
|
|
114
|
+
expect(described_class.next_timeout(timeout: 180, spent: 180)).to eq(360)
|
|
89
115
|
end
|
|
90
116
|
|
|
91
|
-
it '
|
|
117
|
+
it 'rewrites the payload only after the 3-hour budget is exhausted' do
|
|
92
118
|
tmp = Dir.mktmpdir
|
|
93
119
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
94
|
-
|
|
120
|
+
described_class.note_timeout!(
|
|
121
|
+
tool: 'shell',
|
|
122
|
+
payload: 'sleep 3',
|
|
123
|
+
timeout: 10_800,
|
|
124
|
+
task: 't1'
|
|
125
|
+
)
|
|
126
|
+
lesson = described_class.timeout_lesson(
|
|
127
|
+
tool: 'shell',
|
|
128
|
+
payload: 'sleep 3',
|
|
129
|
+
timeout: 10_800,
|
|
130
|
+
task: 't1'
|
|
131
|
+
)
|
|
132
|
+
expect(lesson[:scenario]).to eq(:construction)
|
|
133
|
+
expect(lesson[:hint]).to match(/3-hour budget|Generate different/i)
|
|
134
|
+
expect(described_class.mutation_count(task: 't1')).to eq(1)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'stops after 10 mutations on the same task' do
|
|
138
|
+
10.times do |i|
|
|
139
|
+
described_class.note_timeout!(
|
|
140
|
+
tool: 'shell',
|
|
141
|
+
payload: "payload-#{i}",
|
|
142
|
+
timeout: 10_800,
|
|
143
|
+
task: 't1'
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
last = described_class.timeout_lesson(
|
|
147
|
+
tool: 'shell',
|
|
148
|
+
payload: 'payload-9',
|
|
149
|
+
timeout: 10_800,
|
|
150
|
+
task: 't1'
|
|
151
|
+
)
|
|
152
|
+
expect(described_class.mutation_count(task: 't1')).to eq(10)
|
|
153
|
+
expect(last[:scenario]).to eq(:exhausted)
|
|
154
|
+
expect(last[:hint]).to match(/10 mutations|mutation cap/i)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'includes next_timeout on a mid-budget timeout result' do
|
|
158
|
+
out = described_class.timeout_result(
|
|
95
159
|
tool: 'shell',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
shape: :timeout
|
|
160
|
+
payload: 'sleep 3',
|
|
161
|
+
timeout: 180,
|
|
162
|
+
task: 't1'
|
|
100
163
|
)
|
|
101
|
-
|
|
102
|
-
expect(
|
|
103
|
-
expect(second[:hint]).to match(/timeout was too short|deadline too short|raise a conservative/i)
|
|
104
|
-
expect(second[:error]).to match(/too short/)
|
|
164
|
+
expect(out[:scenario]).to eq(:deadline)
|
|
165
|
+
expect(out[:next_timeout]).to eq(360)
|
|
105
166
|
end
|
|
106
167
|
end
|
|
107
168
|
end
|
|
@@ -49,8 +49,8 @@ describe 'PWN::AI::Agent::Tools ruby_eval' do
|
|
|
49
49
|
result = entry.handler.call(code: 'sleep 3', timeout: 1)
|
|
50
50
|
expect(Time.now - t0).to be < 2.5
|
|
51
51
|
expect(result[:error].to_s).to match(/timeout after 1s/)
|
|
52
|
-
expect(result[:hint].to_s).to match(/
|
|
53
|
-
expect(result[:scenario].to_s).to eq('
|
|
52
|
+
expect(result[:hint].to_s).to match(/timeout \+= 180|next_timeout|same .*payload/i)
|
|
53
|
+
expect(result[:scenario].to_s).to eq('deadline')
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
it 'Dispatch.call JSON-wraps syntax errors instead of raising' do
|
|
@@ -60,12 +60,12 @@ describe 'PWN::AI::Agent::Tools shell' do
|
|
|
60
60
|
expect(r[:exit]).to eq(0)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
it 'on timeout
|
|
63
|
+
it 'on timeout keeps the same command and increments timeout' do
|
|
64
64
|
tmp = Dir.mktmpdir
|
|
65
65
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
66
66
|
r = handler.call(command: 'sleep 3', timeout: 1)
|
|
67
67
|
expect(r[:error].to_s).to match(/timeout after 1s/)
|
|
68
|
-
expect(r[:hint].to_s).to match(/
|
|
69
|
-
expect(r[:scenario].to_s).to eq('
|
|
68
|
+
expect(r[:hint].to_s).to match(/timeout \+= 180|next_timeout|same .*payload/i)
|
|
69
|
+
expect(r[:scenario].to_s).to eq('deadline')
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::AI::RedTeam::AgentProtocolAbuse do
|
|
6
|
+
it 'scan method should exist' do
|
|
7
|
+
scan_response = PWN::AI::RedTeam::AgentProtocolAbuse
|
|
8
|
+
expect(scan_response).to respond_to :scan
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should display information for security_references' do
|
|
12
|
+
security_references_response = PWN::AI::RedTeam::AgentProtocolAbuse
|
|
13
|
+
expect(security_references_response).to respond_to :security_references
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'should display information for authors' do
|
|
17
|
+
authors_response = PWN::AI::RedTeam::AgentProtocolAbuse
|
|
18
|
+
expect(authors_response).to respond_to :authors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'should display information for existing help method' do
|
|
22
|
+
help_response = PWN::AI::RedTeam::AgentProtocolAbuse
|
|
23
|
+
expect(help_response).to respond_to :help
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines strategies instead of a static attack_payloads list' do
|
|
27
|
+
src = File.read(described_class.method(:scan).source_location.first)
|
|
28
|
+
expect(src).to include('strategies = [')
|
|
29
|
+
expect(src).not_to match(/attack_payloads\s*=\s*\[/)
|
|
30
|
+
expect(src).to include('payload_count')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'maps an OWASP 2026 / ATLAS reference' do
|
|
34
|
+
refs = described_class.security_references
|
|
35
|
+
expect(refs[:section]).to include('LLM')
|
|
36
|
+
expect(refs[:owasp_llm_uri]).to include('genai.owasp.org')
|
|
37
|
+
expect(refs[:atlas_id]).to match(/\AAML\.T\d+\z/)
|
|
38
|
+
expect(refs[:red_team_module]).to eq(described_class)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::AI::RedTeam::DataAndModelPoisoning do
|
|
6
|
+
it 'scan method should exist' do
|
|
7
|
+
scan_response = PWN::AI::RedTeam::DataAndModelPoisoning
|
|
8
|
+
expect(scan_response).to respond_to :scan
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should display information for security_references' do
|
|
12
|
+
security_references_response = PWN::AI::RedTeam::DataAndModelPoisoning
|
|
13
|
+
expect(security_references_response).to respond_to :security_references
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'should display information for authors' do
|
|
17
|
+
authors_response = PWN::AI::RedTeam::DataAndModelPoisoning
|
|
18
|
+
expect(authors_response).to respond_to :authors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'should display information for existing help method' do
|
|
22
|
+
help_response = PWN::AI::RedTeam::DataAndModelPoisoning
|
|
23
|
+
expect(help_response).to respond_to :help
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines strategies instead of a static attack_payloads list' do
|
|
27
|
+
src = File.read(described_class.method(:scan).source_location.first)
|
|
28
|
+
expect(src).to include('strategies = [')
|
|
29
|
+
expect(src).not_to match(/attack_payloads\s*=\s*\[/)
|
|
30
|
+
expect(src).to include('payload_count')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'maps an OWASP 2026 / ATLAS reference' do
|
|
34
|
+
refs = described_class.security_references
|
|
35
|
+
expect(refs[:section]).to include('LLM')
|
|
36
|
+
expect(refs[:owasp_llm_uri]).to include('genai.owasp.org')
|
|
37
|
+
expect(refs[:atlas_id]).to match(/\AAML\.T\d+\z/)
|
|
38
|
+
expect(refs[:red_team_module]).to eq(described_class)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::AI::RedTeam::HiddenContextExposure do
|
|
6
|
+
it 'scan method should exist' do
|
|
7
|
+
scan_response = PWN::AI::RedTeam::HiddenContextExposure
|
|
8
|
+
expect(scan_response).to respond_to :scan
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should display information for security_references' do
|
|
12
|
+
security_references_response = PWN::AI::RedTeam::HiddenContextExposure
|
|
13
|
+
expect(security_references_response).to respond_to :security_references
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'should display information for authors' do
|
|
17
|
+
authors_response = PWN::AI::RedTeam::HiddenContextExposure
|
|
18
|
+
expect(authors_response).to respond_to :authors
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'should display information for existing help method' do
|
|
22
|
+
help_response = PWN::AI::RedTeam::HiddenContextExposure
|
|
23
|
+
expect(help_response).to respond_to :help
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines strategies instead of a static attack_payloads list' do
|
|
27
|
+
src = File.read(described_class.method(:scan).source_location.first)
|
|
28
|
+
expect(src).to include('strategies = [')
|
|
29
|
+
expect(src).not_to match(/attack_payloads\s*=\s*\[/)
|
|
30
|
+
expect(src).to include('payload_count')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'maps an OWASP 2026 / ATLAS reference' do
|
|
34
|
+
refs = described_class.security_references
|
|
35
|
+
expect(refs[:section]).to include('LLM')
|
|
36
|
+
expect(refs[:owasp_llm_uri]).to include('genai.owasp.org')
|
|
37
|
+
expect(refs[:atlas_id]).to match(/\AAML\.T\d+\z/)
|
|
38
|
+
expect(refs[:red_team_module]).to eq(described_class)
|
|
39
|
+
end
|
|
40
|
+
end
|