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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/Session-Workflow.md +2 -1
  3. data/etc/default_skills/pwn/ai/agent/loop/SKILL.md +1 -1
  4. data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +5 -0
  5. data/etc/default_skills/pwn/ai/red_team/SKILL.md +1 -1
  6. data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/SKILL.md +53 -0
  7. data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/references/security.md +3 -0
  8. data/etc/default_skills/pwn/ai/red_team/agent_protocol_abuse/references/urls.md +6 -0
  9. data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/SKILL.md +53 -0
  10. data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/references/security.md +3 -0
  11. data/etc/default_skills/pwn/ai/red_team/data_and_model_poisoning/references/urls.md +6 -0
  12. data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/SKILL.md +53 -0
  13. data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/references/security.md +3 -0
  14. data/etc/default_skills/pwn/ai/red_team/hidden_context_exposure/references/urls.md +6 -0
  15. data/etc/default_skills/pwn/ai/red_team/memory_poisoning/SKILL.md +53 -0
  16. data/etc/default_skills/pwn/ai/red_team/memory_poisoning/references/security.md +3 -0
  17. data/etc/default_skills/pwn/ai/red_team/memory_poisoning/references/urls.md +6 -0
  18. data/etc/default_skills/pwn/ai/red_team/multimodal_injection/SKILL.md +53 -0
  19. data/etc/default_skills/pwn/ai/red_team/multimodal_injection/references/security.md +3 -0
  20. data/etc/default_skills/pwn/ai/red_team/multimodal_injection/references/urls.md +6 -0
  21. data/etc/default_skills/pwn/ai/red_team/rag_poisoning/SKILL.md +53 -0
  22. data/etc/default_skills/pwn/ai/red_team/rag_poisoning/references/security.md +3 -0
  23. data/etc/default_skills/pwn/ai/red_team/rag_poisoning/references/urls.md +6 -0
  24. data/etc/default_skills/pwn/ai/red_team/supply_chain/SKILL.md +53 -0
  25. data/etc/default_skills/pwn/ai/red_team/supply_chain/references/security.md +3 -0
  26. data/etc/default_skills/pwn/ai/red_team/supply_chain/references/urls.md +6 -0
  27. data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/SKILL.md +53 -0
  28. data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/references/security.md +3 -0
  29. data/etc/default_skills/pwn/ai/red_team/unbounded_consumption/references/urls.md +6 -0
  30. data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/SKILL.md +53 -0
  31. data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/references/security.md +3 -0
  32. data/etc/default_skills/pwn/ai/red_team/vector_and_embedding_weaknesses/references/urls.md +6 -0
  33. data/etc/default_skills/pwn/plugins/repl/SKILL.md +7 -0
  34. data/lib/pwn/ai/agent/loop.rb +66 -8
  35. data/lib/pwn/ai/agent/prompt_builder.rb +9 -5
  36. data/lib/pwn/ai/agent/tool_guard.rb +129 -33
  37. data/lib/pwn/ai/agent/tools/ruby_eval.rb +9 -11
  38. data/lib/pwn/ai/agent/tools/shell.rb +6 -3
  39. data/lib/pwn/ai/red_team/agent_protocol_abuse.rb +91 -0
  40. data/lib/pwn/ai/red_team/data_and_model_poisoning.rb +91 -0
  41. data/lib/pwn/ai/red_team/hidden_context_exposure.rb +91 -0
  42. data/lib/pwn/ai/red_team/memory_poisoning.rb +91 -0
  43. data/lib/pwn/ai/red_team/multimodal_injection.rb +91 -0
  44. data/lib/pwn/ai/red_team/rag_poisoning.rb +91 -0
  45. data/lib/pwn/ai/red_team/supply_chain.rb +91 -0
  46. data/lib/pwn/ai/red_team/unbounded_consumption.rb +91 -0
  47. data/lib/pwn/ai/red_team/vector_and_embedding_weaknesses.rb +91 -0
  48. data/lib/pwn/ai/red_team.rb +15 -4
  49. data/lib/pwn/plugins/nmap_it.rb +47 -4
  50. data/lib/pwn/plugins/repl.rb +183 -1
  51. data/lib/pwn/version.rb +1 -1
  52. data/spec/lib/pwn/ai/agent/loop_spec.rb +23 -3
  53. data/spec/lib/pwn/ai/agent/prompt_builder_spec.rb +1 -0
  54. data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +80 -19
  55. data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +2 -2
  56. data/spec/lib/pwn/ai/agent/tools/shell_spec.rb +3 -3
  57. data/spec/lib/pwn/ai/red_team/agent_protocol_abuse_spec.rb +40 -0
  58. data/spec/lib/pwn/ai/red_team/data_and_model_poisoning_spec.rb +40 -0
  59. data/spec/lib/pwn/ai/red_team/hidden_context_exposure_spec.rb +40 -0
  60. data/spec/lib/pwn/ai/red_team/memory_poisoning_spec.rb +40 -0
  61. data/spec/lib/pwn/ai/red_team/multimodal_injection_spec.rb +40 -0
  62. data/spec/lib/pwn/ai/red_team/rag_poisoning_spec.rb +40 -0
  63. data/spec/lib/pwn/ai/red_team/supply_chain_spec.rb +40 -0
  64. data/spec/lib/pwn/ai/red_team/unbounded_consumption_spec.rb +40 -0
  65. data/spec/lib/pwn/ai/red_team/vector_and_embedding_weaknesses_spec.rb +40 -0
  66. data/spec/lib/pwn/plugins/nmap_it_spec.rb +7 -0
  67. data/spec/lib/pwn/plugins/repl_spec.rb +37 -1
  68. data/third_party/pwn_rdoc.jsonl +61 -1
  69. metadata +46 -1
@@ -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, etc.). Each module maps to
14
- # an OWASP LLM Top-10 category and a MITRE ATLAS technique so findings
15
- # roll straight into PWN::Reports::AIRedTeam.
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 Aligned AI RedTeam Modules
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'
@@ -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
- yield(nmap)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.695'
4
+ VERSION = '0.5.697'
5
5
  end
@@ -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 reconstruct-first timeout mistake instead of treating success:true as ok' do
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: 'construction' }
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(/reconstruct/)
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 'clamps an explicit timeout and derives a default when omitted' do
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: 9_999, kind: :eval)).to eq(90)
74
- expect(described_class.deadline_s(timeout: 9_999, kind: :shell)).to eq(180)
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
- it 'prefers reconstruct-the-payload before raising timeout' do
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(tool: 'pwn_eval', payload: 'sleep 3', timeout: 1)
85
- expect(first[:scenario]).to eq(:construction)
86
- expect(first[:hint]).to match(/reconstruct|generated differently|improperly/i)
87
- expect(first[:hint]).to match(/do not first raise timeout/i)
88
- expect(first[:error]).to match(/reconstruct/)
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 'switches to deadline-too-short only after a prior timeout mistake' do
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
- PWN::AI::Agent::Mistakes.record(
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
- error: 'shell timeout: reconstruct payload to same goal before raising timeout',
97
- args: 'sleep 3',
98
- source: :tool,
99
- shape: :timeout
160
+ payload: 'sleep 3',
161
+ timeout: 180,
162
+ task: 't1'
100
163
  )
101
- second = described_class.timeout_lesson(tool: 'shell', payload: 'sleep 3', timeout: 1)
102
- expect(second[:scenario]).to eq(:deadline)
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(/reconstruct|generated differently|improperly/i)
53
- expect(result[:scenario].to_s).to eq('construction')
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 prefers reconstruct-the-command before raising timeout' do
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(/reconstruct|generated differently|improperly/i)
69
- expect(r[:scenario].to_s).to eq('construction')
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