pwn 0.5.613 → 0.5.617

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -10
  3. data/README.md +117 -17
  4. data/bin/pwn +47 -2
  5. data/bin/pwn_gqrx_scanner +75 -18
  6. data/documentation/AI-Integration.md +36 -0
  7. data/documentation/BurpSuite.md +26 -0
  8. data/documentation/Contributing.md +31 -0
  9. data/documentation/Diagrams.md +140 -0
  10. data/documentation/Drivers.md +25 -0
  11. data/documentation/General-PWN-Usage.md +60 -0
  12. data/documentation/Home.md +78 -0
  13. data/documentation/How-PWN-Works.md +62 -0
  14. data/documentation/Installation.md +75 -0
  15. data/documentation/NmapIt.md +18 -0
  16. data/documentation/Plugins.md +59 -0
  17. data/documentation/Reporting.md +28 -0
  18. data/documentation/SAST.md +32 -0
  19. data/documentation/Skills-Memory-Learning.md +44 -0
  20. data/documentation/Transparent-Browser.md +32 -0
  21. data/documentation/Troubleshooting.md +38 -0
  22. data/documentation/What-is-PWN.md +53 -0
  23. data/documentation/Why-PWN.md +48 -0
  24. data/documentation/diagrams/ai-integration-tool-calling.svg +172 -0
  25. data/documentation/diagrams/build.sh +20 -0
  26. data/documentation/diagrams/burp-vs-zap-preference.svg +69 -0
  27. data/documentation/diagrams/code-scanning-sast.svg +172 -0
  28. data/documentation/diagrams/dot/_THEME.md +27 -0
  29. data/documentation/diagrams/dot/agent-tool-registry.dot +59 -0
  30. data/documentation/diagrams/dot/ai-integration-tool-calling.dot +50 -0
  31. data/documentation/diagrams/dot/aws-cloud-security.dot +48 -0
  32. data/documentation/diagrams/dot/burp-vs-zap-preference.dot +26 -0
  33. data/documentation/diagrams/dot/code-scanning-sast.dot +49 -0
  34. data/documentation/diagrams/dot/cron-scheduling.dot +40 -0
  35. data/documentation/diagrams/dot/driver-framework.dot +39 -0
  36. data/documentation/diagrams/dot/extrospection-world-awareness.dot +55 -0
  37. data/documentation/diagrams/dot/fuzzing-workflow.dot +47 -0
  38. data/documentation/diagrams/dot/history-to-drivers.dot +34 -0
  39. data/documentation/diagrams/dot/memory-skills-detailed.dot +60 -0
  40. data/documentation/diagrams/dot/network-infra-testing.dot +54 -0
  41. data/documentation/diagrams/dot/overall-pwn-architecture.dot +115 -0
  42. data/documentation/diagrams/dot/penetration-testing-workflow.dot +60 -0
  43. data/documentation/diagrams/dot/plugin-ecosystem.dot +45 -0
  44. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +98 -0
  45. data/documentation/diagrams/dot/pwn-repl-prototyping.dot +56 -0
  46. data/documentation/diagrams/dot/reporting-pipeline.dot +46 -0
  47. data/documentation/diagrams/dot/reverse-engineering-flow.dot +43 -0
  48. data/documentation/diagrams/dot/sdr-radio-flow.dot +45 -0
  49. data/documentation/diagrams/dot/sessions-cron-automation.dot +44 -0
  50. data/documentation/diagrams/dot/swarm-multi-agent.dot +70 -0
  51. data/documentation/diagrams/dot/web-application-testing.dot +49 -0
  52. data/documentation/diagrams/dot/zero-day-research-flow.dot +49 -0
  53. data/documentation/diagrams/driver-framework.svg +111 -0
  54. data/documentation/diagrams/fuzzing-workflow.svg +160 -0
  55. data/documentation/diagrams/history-to-drivers.svg +119 -0
  56. data/documentation/diagrams/memory-skills-detailed.svg +202 -0
  57. data/documentation/diagrams/network-infra-testing.svg +183 -0
  58. data/documentation/diagrams/overall-pwn-architecture.svg +405 -0
  59. data/documentation/diagrams/penetration-testing-workflow.svg +201 -0
  60. data/documentation/diagrams/plugin-ecosystem.svg +276 -0
  61. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +306 -0
  62. data/documentation/diagrams/pwn-repl-prototyping.svg +168 -0
  63. data/documentation/diagrams/reporting-pipeline.svg +151 -0
  64. data/documentation/diagrams/reverse-engineering-flow.svg +134 -0
  65. data/documentation/diagrams/sessions-cron-automation.svg +136 -0
  66. data/documentation/diagrams/web-application-testing.svg +173 -0
  67. data/documentation/diagrams/zero-day-research-flow.svg +153 -0
  68. data/documentation/pwn-REPL.md +33 -0
  69. data/documentation/pwn-ai-Agent.md +43 -0
  70. data/git_commit.sh +1 -1
  71. data/lib/pwn/ai/agent/assembly.rb +1 -1
  72. data/lib/pwn/ai/agent/btc.rb +1 -1
  73. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  74. data/lib/pwn/ai/agent/extrospection.rb +618 -0
  75. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  76. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  77. data/lib/pwn/ai/agent/introspection.rb +91 -0
  78. data/lib/pwn/ai/agent/learning.rb +379 -0
  79. data/lib/pwn/ai/agent/loop.rb +33 -3
  80. data/lib/pwn/ai/agent/metrics.rb +155 -0
  81. data/lib/pwn/ai/agent/prompt_builder.rb +32 -3
  82. data/lib/pwn/ai/agent/sast.rb +1 -1
  83. data/lib/pwn/ai/agent/swarm.rb +437 -0
  84. data/lib/pwn/ai/agent/tools/cron.rb +163 -0
  85. data/lib/pwn/ai/agent/tools/extrospection.rb +280 -0
  86. data/lib/pwn/ai/agent/tools/learning.rb +221 -0
  87. data/lib/pwn/ai/agent/tools/memory.rb +27 -0
  88. data/lib/pwn/ai/agent/tools/metrics.rb +61 -0
  89. data/lib/pwn/ai/agent/tools/sessions.rb +139 -0
  90. data/lib/pwn/ai/agent/tools/skills.rb +124 -8
  91. data/lib/pwn/ai/agent/tools/swarm.rb +229 -0
  92. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  93. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  94. data/lib/pwn/ai/agent.rb +5 -0
  95. data/lib/pwn/ai/anthropic.rb +19 -4
  96. data/lib/pwn/ai/grok.rb +249 -106
  97. data/lib/pwn/ai.rb +0 -1
  98. data/lib/pwn/config.rb +96 -36
  99. data/lib/pwn/cron.rb +16 -7
  100. data/lib/pwn/plugins/monkey_patch.rb +2 -2
  101. data/lib/pwn/plugins/repl.rb +158 -284
  102. data/lib/pwn/sdr/decoder/base.rb +251 -0
  103. data/lib/pwn/sdr/decoder/gsm.rb +84 -185
  104. data/lib/pwn/sdr/decoder/pocsag.rb +62 -217
  105. data/lib/pwn/sdr/decoder.rb +1 -0
  106. data/lib/pwn/sdr/gqrx.rb +446 -65
  107. data/lib/pwn/version.rb +1 -1
  108. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +15 -0
  109. data/spec/lib/pwn/ai/agent/introspection_spec.rb +15 -0
  110. data/spec/lib/pwn/ai/agent/learning_spec.rb +68 -0
  111. data/spec/lib/pwn/ai/agent/metrics_spec.rb +27 -0
  112. data/spec/lib/pwn/ai/agent/swarm_spec.rb +34 -0
  113. data/spec/lib/pwn/ai/agent/tools/cron_spec.rb +40 -0
  114. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +40 -0
  115. data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +30 -0
  116. data/spec/lib/pwn/ai/agent/tools/metrics_spec.rb +20 -0
  117. data/spec/lib/pwn/ai/agent/tools/sessions_spec.rb +35 -0
  118. data/spec/lib/pwn/ai/agent/tools/swarm_spec.rb +17 -0
  119. data/spec/lib/pwn/{ai/introspection_spec.rb → sdr/decoder/base_spec.rb} +3 -3
  120. data/third_party/pwn_rdoc.jsonl +178 -33
  121. metadata +110 -36
  122. data/documentation/PWN.png +0 -0
  123. data/documentation/PWN_Contributors_and_Users.png +0 -0
  124. data/documentation/fax-spectrogram.png +0 -0
  125. data/documentation/fax-waveform.png +0 -0
  126. data/documentation/frozen_string_literal_files_changed_from_false_to_true.txt +0 -54
  127. data/documentation/lifecycle_authz_replay.example.yaml +0 -27
  128. data/documentation/pwn_android_war_dialer_session.png +0 -0
  129. data/documentation/pwn_install.png +0 -0
  130. data/documentation/pwn_wallpaper.jpg +0 -0
  131. data/documentation/ringing-spectrogram.png +0 -0
  132. data/documentation/ringing-waveform.png +0 -0
  133. data/documentation/vulnerability_report_template.md +0 -37
  134. data/lib/pwn/ai/introspection.rb +0 -76
data/lib/pwn/ai/grok.rb CHANGED
@@ -13,115 +13,227 @@ module PWN
13
13
  # API documentation: https://docs.x.ai/docs
14
14
  # Obtain an API key from https://x.ai/api
15
15
  module Grok
16
+ # Internal helper: true when +opts[:value]+ is a *real* configured value coming
17
+ # from PWN::Config / pwn-vault (i.e. not nil, not blank, and not one of
18
+ # the "optional - ..." / "required - ..." placeholder strings that
19
+ # PWN::Config.default_env writes into a fresh ~/.pwn/pwn.yaml).
20
+ # Used so OAuth + API key resolution behaves correctly regardless of
21
+ # whether the user edited every field.
22
+ private_class_method def self.real_config_value?(opts = {})
23
+ s = opts[:value].to_s.strip
24
+ return false if s.empty?
25
+ return false if s.match?(/\A(optional|required)\b/i)
26
+
27
+ true
28
+ end
29
+
30
+ # ------------------------------------------------------------------
31
+ # xAI Grok OAuth (SuperGrok) -- public-client OIDC, no client_secret.
32
+ #
33
+ # auth.x.ai is a full OIDC provider (/.well-known/openid-configuration).
34
+ # xAI ships a PUBLIC client for the Grok CLI; the same client_id is used
35
+ # by NousResearch/hermes-agent (`hermes auth add xai-oauth`) and is reused
36
+ # here so PWN can obtain a Bearer for https://api.x.ai/v1 without an API
37
+ # key. token_endpoint_auth_methods_supported includes 'none', so no
38
+ # client_secret is required -- only PKCE / device_code.
39
+ #
40
+ # Two grants are implemented:
41
+ # * RFC 8628 device_authorization -> default; headless / SSH friendly.
42
+ # * refresh_token -> silent renewal once enrolled.
43
+ #
44
+ # Access tokens are short-lived JWTs (ES256, `exp` claim). The
45
+ # refresh_token is long-lived -- persist it via pwn-vault under
46
+ # ai.grok.oauth.refresh_token and PWN::AI::Grok refreshes transparently
47
+ # on every run.
48
+ # ------------------------------------------------------------------
49
+ XAI_OAUTH_ISSUER = 'https://auth.x.ai'
50
+ XAI_OAUTH_DEVICE_URI = "#{XAI_OAUTH_ISSUER}/oauth2/device/code".freeze
51
+ XAI_OAUTH_TOKEN_URI = "#{XAI_OAUTH_ISSUER}/oauth2/token".freeze
52
+ # Public Grok-CLI client_id (same one hermes-agent uses).
53
+ XAI_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828'
54
+ XAI_OAUTH_SCOPE = 'openid profile email offline_access grok-cli:access api:access'
55
+
56
+ # Internal: decode a JWT payload (no sig verification) to read `exp`.
57
+ private_class_method def self.jwt_exp(opts = {})
58
+ seg = opts[:token].to_s.split('.')[1]
59
+ return nil unless seg
60
+
61
+ seg += '=' * ((4 - (seg.length % 4)) % 4)
62
+ JSON.parse(Base64.urlsafe_decode64(seg))['exp']
63
+ rescue StandardError
64
+ nil
65
+ end
66
+
67
+ # Internal: true when +opts[:token]+ is absent, not a JWT, or expires within
68
+ # +opts[:skew]+ seconds (default 120).
69
+ private_class_method def self.oauth_token_expiring?(opts = {})
70
+ token = opts[:token]
71
+ skew = opts[:skew] || 120
72
+ return true unless real_config_value?(value: token)
73
+
74
+ exp = jwt_exp(token: token)
75
+ return false if exp.nil? # opaque token -- trust it
76
+
77
+ Time.now.to_i >= (exp.to_i - skew)
78
+ end
79
+
16
80
  # Supported Method Parameters::
17
- # bearer = PWN::AI::Grok.obtain_oauth_bearer_token(
18
- # client_id: 'xAI OAuth Client ID',
19
- # client_secret: 'xAI OAuth Client Secret'
20
- # token_uri: 'optional - xAI OAuth token endpoint (defaults to https://auth.x.ai/oauth2/token)'
81
+ # access_token = PWN::AI::Grok.refresh_oauth_bearer_token(
82
+ # refresh_token: 'required - xAI OAuth refresh_token',
83
+ # client_id: 'optional - defaults to public Grok-CLI client',
84
+ # token_uri: 'optional - defaults to https://auth.x.ai/oauth2/token'
21
85
  # )
22
86
  #
23
- # Internal: only invoked when oauth config (client_id etc) is present and no valid bearer_token.
24
- # Constructs the authorize URL (https://auth.x.ai/oauth2/authorize) for the user to complete
25
- # consent in browser (standard for authorization_code flow). Prompts for the code returned
26
- # after redirect (OOB), then exchanges at token_uri for the bearer_token.
27
- # This fulfills calling the authorize endpoint (via URL) only when oauth configured.
28
- # Uses xAI's supported scopes like grok-cli:access. Stores result in the oauth hash for the session.
87
+ # Exchanges a refresh_token for a fresh access_token at auth.x.ai.
88
+ # On success, writes :bearer_token (and a rotated :refresh_token if
89
+ # returned) back into the passed opts/oauth Hash so the live PWN::Env
90
+ # stays warm for the rest of the process.
91
+ public_class_method def self.refresh_oauth_bearer_token(opts = {})
92
+ refresh_token = opts[:refresh_token]
93
+ raise 'refresh_token is required' unless real_config_value?(value: refresh_token)
94
+
95
+ client_id = real_config_value?(value: opts[:client_id]) ? opts[:client_id] : XAI_OAUTH_CLIENT_ID
96
+ token_uri = real_config_value?(value: opts[:token_uri]) ? opts[:token_uri] : XAI_OAUTH_TOKEN_URI
97
+
98
+ resp = RestClient.post(
99
+ token_uri,
100
+ {
101
+ grant_type: 'refresh_token',
102
+ refresh_token: refresh_token,
103
+ client_id: client_id
104
+ },
105
+ content_type: 'application/x-www-form-urlencoded',
106
+ accept: 'application/json'
107
+ )
108
+ data = JSON.parse(resp.body)
109
+ raise "xAI OAuth refresh error: #{data['error']} - #{data['error_description']}" if data['error']
110
+
111
+ opts[:bearer_token] = data['access_token']
112
+ opts[:refresh_token] = data['refresh_token'] if data['refresh_token']
113
+ opts[:expires_at] = Time.now.to_i + data['expires_in'].to_i if data['expires_in']
114
+ data['access_token']
115
+ rescue RestClient::ExceptionWithResponse => e
116
+ raise "xAI OAuth refresh failed (HTTP #{e.http_code}): #{e.response&.body}"
117
+ end
118
+
29
119
  # Supported Method Parameters::
30
120
  # bearer = PWN::AI::Grok.obtain_oauth_bearer_token(
31
- # client_id: 'xAI OAuth Client ID',
32
- # client_secret: 'xAI OAuth Client Secret',
33
- # token_uri: 'optional - xAI OAuth token endpoint (defaults to https://auth.x.ai/oauth2/token)'
121
+ # client_id: 'optional - xAI OAuth Client ID (defaults to public Grok-CLI client)',
122
+ # scope: 'optional - space-delimited scopes (defaults to XAI_OAUTH_SCOPE)',
123
+ # timeout: 'optional - seconds to wait for user consent (default 300)'
34
124
  # )
35
125
  #
36
- # Public so users can manually trigger enrollment if desired.
37
- # INTERNAL default path: only invoked from grok_rest_call when oauth client_id+secret present
38
- # and no bearer_token yet in the loaded PWN::Env (from pwn-vault encrypted ~/.pwn/pwn.yaml).
126
+ # Runs the RFC 8628 OAuth 2.0 Device Authorization Grant against
127
+ # auth.x.ai using xAI's public Grok-CLI client (no client_secret).
128
+ # This is the same identity path `hermes auth add xai-oauth` uses --
129
+ # a SuperGrok / X Premium+ subscription on the account is what grants
130
+ # api:access at consent time.
39
131
  #
40
- # This is a SINGULAR ENROLLMENT process (not per-call or per-session).
41
- # The resulting bearer_token (and optional refresh_token) is long-lived for xAI SuperGrok
42
- # subscriptions. Once you store it in your pwn-vault config, every future `pwn` / PWN::Env load
43
- # will have it; the guard will skip this flow entirely and use "Authorization: Bearer ..." directly.
44
- # (No re-prompting every time you run pwn or call PWN::AI::Grok.chat.)
132
+ # 1. POST /oauth2/device/code -> device_code, user_code, verification_uri
133
+ # 2. User opens verification_uri_complete in a browser and approves.
134
+ # 3. Poll POST /oauth2/token (grant_type=device_code) until access_token.
135
+ #
136
+ # On success the access_token + refresh_token are written back into the
137
+ # passed opts/oauth Hash (so PWN::Env[:ai][:grok][:oauth] is live-cached)
138
+ # and the operator is told exactly what to persist via pwn-vault.
45
139
  public_class_method def self.obtain_oauth_bearer_token(opts = {})
46
- client_id = opts[:client_id]
47
- client_secret = opts[:client_secret]
48
-
49
- scope = 'grok-cli:access'
50
- redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
51
- auth_uri = 'https://auth.x.ai/oauth2/authorize'
52
- token_uri = opts[:token_uri] || 'https://auth.x.ai/oauth2/token'
53
-
54
- # Build authorize URL -- this is the "call" to the authorize endpoint (user opens to consent)
55
- params = {
56
- client_id: client_id,
57
- response_type: 'code',
58
- scope: scope,
59
- redirect_uri: redirect_uri
60
- }
61
- authorize_url = "#{auth_uri}?#{URI.encode_www_form(params)}"
140
+ client_id = real_config_value?(value: opts[:client_id]) ? opts[:client_id] : XAI_OAUTH_CLIENT_ID
141
+ scope = real_config_value?(value: opts[:scope]) ? opts[:scope] : XAI_OAUTH_SCOPE
142
+ token_uri = real_config_value?(value: opts[:token_uri]) ? opts[:token_uri] : XAI_OAUTH_TOKEN_URI
143
+ timeout = (opts[:timeout] || 300).to_i
144
+
145
+ # -- Step 1: request device + user code -------------------------------
146
+ dev = JSON.parse(
147
+ RestClient.post(
148
+ XAI_OAUTH_DEVICE_URI,
149
+ { client_id: client_id, scope: scope },
150
+ content_type: 'application/x-www-form-urlencoded',
151
+ accept: 'application/json'
152
+ ).body
153
+ )
154
+ raise "xAI device_code error: #{dev['error']} - #{dev['error_description']}" if dev['error']
62
155
 
63
- puts "\n[*] OAuth ENROLLMENT for Grok (xAI SuperGrok subscription)."
64
- puts ' This is a ONE-TIME / SINGULAR enrollment process.'
65
- puts ' The bearer_token you receive is LONG-LIVED (store it once; no re-obtain every call or run).'
66
- puts ''
67
- puts ' Step 1: Open this URL in your browser and complete the authorization/consent for the grok-cli app:'
68
- puts " #{authorize_url}"
156
+ device_code = dev['device_code']
157
+ user_code = dev['user_code']
158
+ verify_uri = dev['verification_uri_complete'] || dev['verification_uri']
159
+ interval = (dev['interval'] || 5).to_i
160
+ expires_in = (dev['expires_in'] || timeout).to_i
161
+ deadline = Time.now.to_i + [expires_in, timeout].min
162
+
163
+ puts "\n[*] xAI Grok OAuth -- Device Authorization (RFC 8628, public client, no secret)"
164
+ puts ' A SuperGrok / X Premium+ subscription on the approving account is required.'
69
165
  puts ''
70
- puts ' Step 2: After consent you will see (or be redirected to) an authorization code. Copy it exactly.'
166
+ puts ' Step 1: In a browser (any device), open:'
167
+ puts " #{verify_uri}"
168
+ puts " Step 2: Confirm the code matches: #{user_code}"
169
+ puts ' Step 3: Approve access for "Grok CLI".'
71
170
  puts ''
171
+ puts " Waiting for approval (polling every #{interval}s, timeout #{deadline - Time.now.to_i}s)..."
172
+
173
+ # -- Step 2: poll the token endpoint ---------------------------------
174
+ data = nil
175
+ loop do
176
+ sleep interval
177
+ begin
178
+ tok = RestClient.post(
179
+ token_uri,
180
+ {
181
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
182
+ device_code: device_code,
183
+ client_id: client_id
184
+ },
185
+ content_type: 'application/x-www-form-urlencoded',
186
+ accept: 'application/json'
187
+ )
188
+ data = JSON.parse(tok.body)
189
+ rescue RestClient::ExceptionWithResponse => e
190
+ data = begin
191
+ JSON.parse(e.response.body)
192
+ rescue StandardError
193
+ { 'error' => "http_#{e.http_code}", 'error_description' => e.response&.body }
194
+ end
195
+ end
72
196
 
73
- code = PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Enter the authorization code from xAI OAuth')
74
-
75
- # Exchange code for bearer at token endpoint.
76
- # Use standard confidential client auth: Authorization: Basic base64(client_id:client_secret)
77
- # + client_id in body (secret NOT in body).
78
- basic = "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"
79
- payload = {
80
- grant_type: 'authorization_code',
81
- code: code,
82
- redirect_uri: redirect_uri,
83
- client_id: client_id
84
- }
85
-
86
- response = RestClient.post(
87
- token_uri,
88
- payload,
89
- {
90
- content_type: 'application/x-www-form-urlencoded',
91
- authorization: basic
92
- }
93
- )
197
+ case data['error']
198
+ when nil
199
+ break # success
200
+ when 'authorization_pending'
201
+ raise 'xAI OAuth device flow timed out waiting for user approval.' if Time.now.to_i >= deadline
202
+
203
+ next
204
+ when 'slow_down'
205
+ interval += 5
206
+ next
207
+ when 'access_denied'
208
+ raise 'xAI OAuth device flow: user denied the authorization request.'
209
+ when 'expired_token'
210
+ raise 'xAI OAuth device flow: device_code expired before approval; re-run enrollment.'
211
+ else
212
+ raise "xAI OAuth device flow error: #{data['error']} - #{data['error_description']}"
213
+ end
214
+ end
94
215
 
95
- data = JSON.parse(response.body)
216
+ access_token = data['access_token']
217
+ refresh_token = data['refresh_token']
218
+ raise 'xAI OAuth token endpoint returned no access_token.' unless access_token
96
219
 
97
- if data['error']
98
- desc = data['error_description'] || data['error']
99
- raise "xAI OAuth token endpoint error: #{data['error']} - #{desc}"
100
- end
220
+ opts[:bearer_token] = access_token
221
+ opts[:refresh_token] = refresh_token if refresh_token
222
+ opts[:expires_at] = Time.now.to_i + data['expires_in'].to_i if data['expires_in']
101
223
 
102
- access_token = data['access_token']
103
-
104
- if access_token
105
- opts[:bearer_token] = access_token
106
- opts[:refresh_token] = data['refresh_token'] if data['refresh_token']
107
- puts "\n[*] SUCCESS: Bearer token obtained via authorize + token exchange."
108
- puts ' (Cached in-memory for this Ruby process so subsequent Grok calls in the same run skip re-enrollment.)'
109
- puts ''
110
- puts ' TO MAKE THIS PERMANENT (strongly recommended -- one-time only):'
111
- puts ' 1. Copy the bearer_token below (and refresh_token if present).'
112
- puts ' 2. Run your pwn-vault tool (or equivalent) and store under the ai.grok.oauth section:'
113
- puts " ai.grok.oauth.bearer_token = #{access_token}"
114
- puts " ai.grok.oauth.refresh_token = #{data['refresh_token']}" if data['refresh_token']
115
- puts ' 3. (Optional) You may leave or remove client_id/client_secret after storing the bearer.'
116
- puts ' 4. Next time PWN::Env loads (pwn -Y, pwn REPL, scripts, etc.) the bearer will be present'
117
- puts ' from your encrypted ~/.pwn/pwn.yaml -- the guard will skip this entire flow.'
118
- puts ' No more browser prompts or code pasting on future uses.'
119
- puts ''
120
- puts ' The token is long-lived for your SuperGrok subscription (xAI manages expiry/refresh as needed).'
121
- return access_token
122
- end
224
+ puts "\n[*] SUCCESS: xAI Grok OAuth bearer obtained via device_code grant."
225
+ puts ' Cached in-memory for this pwn / pwn-ai process.'
226
+ puts ''
227
+ puts ' TO MAKE THIS PERMANENT (recommended -- one-time), store via pwn-vault:'
228
+ puts " ai.grok.oauth.refresh_token = #{refresh_token}" if refresh_token
229
+ puts " ai.grok.oauth.bearer_token = #{access_token}"
230
+ puts ' On future runs the refresh_token alone is enough -- PWN::AI::Grok will'
231
+ puts ' silently exchange it for a fresh access_token (no browser, no prompt).'
232
+ puts ''
123
233
 
124
- raise 'No access_token received from xAI OAuth token endpoint (unexpected response)'
234
+ access_token
235
+ rescue RestClient::ExceptionWithResponse => e
236
+ raise "Failed to obtain Grok OAuth bearer token (HTTP #{e.http_code}): #{e.response&.body}"
125
237
  rescue StandardError => e
126
238
  raise "Failed to obtain Grok OAuth bearer token: #{e.message}"
127
239
  end
@@ -137,28 +249,59 @@ module PWN
137
249
  # timeout: 'optional timeout in seconds (defaults to 900)',
138
250
  # spinner: 'optional - display spinner (defaults to false)'
139
251
  # )
140
-
141
252
  private_class_method def self.grok_rest_call(opts = {})
142
- engine = PWN::Env[:ai][:grok]
253
+ engine = PWN::Env.dig(:ai, :grok) if defined?(PWN::Env)
143
254
  raise 'ERROR: Grok Hash not found in PWN::Env. Run `pwn -Y default.yaml`, then `PWN::Env` for usage.' if engine.nil?
144
255
 
145
- oauth = engine[:oauth] ||= {}
146
- if oauth[:client_id] && !oauth[:client_id].to_s.empty? && !oauth[:client_id].to_s.match?(/optional/i) &&
147
- oauth[:client_secret] && !oauth[:client_secret].to_s.empty? && !oauth[:client_secret].to_s.match?(/optional/i) &&
148
- (oauth[:bearer_token].nil? || oauth[:bearer_token].to_s.empty? || oauth[:bearer_token].to_s.match?(/optional/i))
149
- # ONLY call authorize flow when BOTH oauth:client_id + client_secret configured (non-optional)
150
- # AND no valid bearer_token yet. This is the singular enrollment trigger.
151
- # (Bearer is long-lived; store via pwn-vault once so future PWN::Env loads skip this.)
152
- # Pass the live oauth hash so obtain can mutate it (for in-process cache; inner hash is mutable).
256
+ # ------------------------------------------------------------------
257
+ # Bearer resolution (all sourced from PWN::Config / pwn-vault via
258
+ # PWN::Env[:ai][:grok]). Priority order:
259
+ # 1. oauth[:bearer_token] -> if present AND not expiring (JWT exp).
260
+ # 2. oauth[:refresh_token] -> silent refresh_token grant at auth.x.ai
261
+ # (writes new bearer back into oauth hash).
262
+ # 3. oauth device flow -> RFC 8628 enrollment via public Grok-CLI
263
+ # client. Triggered when the oauth section
264
+ # is opted-in (client_id set OR :enroll
265
+ # truthy) OR when no engine[:key] exists.
266
+ # 4. engine[:key] -> classic xAI API key.
267
+ # 5. interactive prompt -> last resort.
268
+ # Placeholder strings ("optional - ..." / "required - ...") written by
269
+ # PWN::Config.default_env are treated as UNSET.
270
+ # ------------------------------------------------------------------
271
+ oauth = engine[:oauth].is_a?(Hash) ? engine[:oauth] : (engine[:oauth] ||= {})
272
+ token = nil
273
+
274
+ token = oauth[:bearer_token] if real_config_value?(value: oauth[:bearer_token]) && !oauth_token_expiring?(token: oauth[:bearer_token])
275
+
276
+ if token.nil? && real_config_value?(value: oauth[:refresh_token])
277
+ begin
278
+ token = refresh_oauth_bearer_token(oauth)
279
+ rescue StandardError => e
280
+ warn "[!] Grok OAuth refresh failed, falling back: #{e.message}"
281
+ end
282
+ end
283
+
284
+ oauth_opt_in = real_config_value?(value: oauth[:client_id]) ||
285
+ oauth[:enroll] == true ||
286
+ real_config_value?(value: oauth[:bearer_token]) ||
287
+ real_config_value?(value: oauth[:refresh_token])
288
+
289
+ if token.nil? && (oauth_opt_in || !real_config_value?(value: engine[:key]))
290
+ # Singular device-flow enrollment. Result is written back into the
291
+ # live oauth hash so subsequent grok_rest_call invocations inside the
292
+ # same pwn / pwn-ai process reuse it silently.
153
293
  token = obtain_oauth_bearer_token(oauth)
154
294
  end
155
295
 
156
- token ||= engine[:key]
157
- token ||= PWN::Plugins::AuthenticationHelper.mask_password(prompt: 'Grok API Key (oauth:client_id will auto-trigger authorize flow for bearer_token if configured)')
296
+ token = engine[:key] if token.nil? && real_config_value?(value: engine[:key])
297
+
298
+ token ||= PWN::Plugins::AuthenticationHelper.mask_password(
299
+ prompt: 'Grok API Key (or run PWN::AI::Grok.obtain_oauth_bearer_token for SuperGrok OAuth)'
300
+ )
158
301
 
159
- http_method = opts[:http_method].to_s.scrub.to_sym ||= :get
302
+ http_method = opts[:http_method].nil? ? :get : opts[:http_method].to_s.scrub.to_sym
160
303
 
161
- base_uri = engine[:base_uri] ||= 'https://api.x.ai/v1'
304
+ base_uri = real_config_value?(value: engine[:base_uri]) ? engine[:base_uri] : 'https://api.x.ai/v1'
162
305
  rest_call = opts[:rest_call].to_s.scrub
163
306
  params = opts[:params]
164
307
  headers = {
data/lib/pwn/ai.rb CHANGED
@@ -9,7 +9,6 @@ module PWN
9
9
  autoload :Anthropic, 'pwn/ai/anthropic'
10
10
  autoload :Gemini, 'pwn/ai/gemini'
11
11
  autoload :Grok, 'pwn/ai/grok'
12
- autoload :Introspection, 'pwn/ai/introspection'
13
12
  autoload :Ollama, 'pwn/ai/ollama'
14
13
  autoload :OpenAI, 'pwn/ai/open_ai'
15
14
 
data/lib/pwn/config.rb CHANGED
@@ -40,11 +40,16 @@ module PWN
40
40
  # OAuth support for xAI SuperGrok subscriptions (in addition to API key)
41
41
  # Populate via pwn-vault command (values stored encrypted in ~/.pwn/pwn.yaml)
42
42
  oauth: {
43
- client_id: 'optional - xAI SuperGrok OAuth Client ID (for subscriptions without API key)',
44
- client_secret: 'optional - xAI SuperGrok OAuth Client Secret',
45
- bearer_token: 'optional - xAI SuperGrok OAuth Access Token (preferred for SuperGrok subs; used as Bearer)',
46
- refresh_token: 'optional - xAI SuperGrok OAuth Refresh Token',
47
- token_uri: 'optional - OAuth token endpoint (defaults handled in PWN::AI::Grok if needed)'
43
+ # xAI Grok OAuth uses a PUBLIC client (Grok-CLI, same as hermes-agent) --
44
+ # NO client_secret. Run PWN::AI::Grok.obtain_oauth_bearer_token once
45
+ # (RFC 8628 device flow) then store refresh_token here; PWN refreshes
46
+ # the short-lived access_token automatically on every run.
47
+ refresh_token: 'optional - xAI SuperGrok OAuth Refresh Token (durable; enables silent re-auth)',
48
+ bearer_token: 'optional - xAI SuperGrok OAuth Access Token (short-lived JWT; auto-refreshed if refresh_token set)',
49
+ client_id: 'optional - override public Grok-CLI client_id (default: b1a00492-073a-47ea-816f-4c329264a828)',
50
+ scope: 'optional - override OAuth scope (default: openid profile email offline_access grok-cli:access api:access)',
51
+ token_uri: 'optional - override OAuth token endpoint (default: https://auth.x.ai/oauth2/token)',
52
+ enroll: 'optional - set true to force device-flow enrollment even when an API key is present'
48
53
  }
49
54
  },
50
55
  openai: {
@@ -69,6 +74,7 @@ module PWN
69
74
  model: 'optional - Anthropic model to use (e.g. claude-3-5-sonnet-20240620)',
70
75
  system_role_content: 'You are an ethically hacking Anthropic agent.',
71
76
  temp: 'optional - Anthropic temperature',
77
+ max_tokens: 'optional - Max output tokens per response (default 8192). Raise if tool calls truncate.',
72
78
  max_prompt_length: 200_000
73
79
  },
74
80
  gemini: {
@@ -82,7 +88,15 @@ module PWN
82
88
  agent: {
83
89
  native_tools: true,
84
90
  max_iters: 25,
91
+ # Swarm (agent_ask/agent_debate) sub-agent recursion cap
92
+ max_depth: 3,
93
+ # run PWN::AI::Agent::Learning.auto_reflect after every final answer
94
+ auto_reflect: true,
95
+ # also run PWN::AI::Agent::Extrospection.auto_extrospect from auto_reflect
96
+ auto_extrospect: false,
85
97
  toolsets: nil
98
+ # multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
99
+ # swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
86
100
  }
87
101
  },
88
102
  plugins: {
@@ -95,28 +109,6 @@ module PWN
95
109
  rpc_pass: 'bitcoin RPC Password'
96
110
  }
97
111
  },
98
- irc: {
99
- ui_nick: '_human_',
100
- shared_chan: '#pwn',
101
- ai_agent_nicks: {
102
- browser: {
103
- pwn_rb: '/opt/pwn/lib/pwn/plugins/transparent_browser.rb',
104
- system_role_content: 'You are a browser. You are a web browser that can be controlled by a human or AI agent'
105
- },
106
- nimjeh: {
107
- pwn_rb: '',
108
- system_role_content: 'You are a sarcastic hacker. You find software zero day vulnerabilities. This involves analyzing source code, race conditions, application binaries, and network protocols from an offensive security perspective.'
109
- },
110
- nmap: {
111
- pwn_rb: '/opt/pwn/lib/pwn/plugins/nmap_it.rb',
112
- system_role_content: 'You are a network scanner. You are a network scanner that can be controlled by a human or AI agent'
113
- },
114
- shodan: {
115
- pwn_rb: '/opt/pwn/lib/pwn/plugins/shodan.rb',
116
- system_role_content: 'You are a passive reconnaissance agent. You are a passive reconnaissance agent that can be controlled by a human or AI agent'
117
- }
118
- }
119
- },
120
112
  hunter: { api_key: 'hunter.how API Key' },
121
113
  jira_data_center: {
122
114
  base_uri: 'Jira Server Base API URI (e.g. https://jira.company.com/rest/api/latest)',
@@ -226,6 +218,7 @@ module PWN
226
218
  pass
227
219
  password
228
220
  psk
221
+ refresh_token
229
222
  secret_key
230
223
  token
231
224
  ]
@@ -294,20 +287,40 @@ module PWN
294
287
  iv: iv
295
288
  )
296
289
 
297
- valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :introspection }.map(&:downcase)
290
+ valid_ai_engines = PWN::AI.help.reject { |e| e.downcase == :agent }.map(&:downcase)
298
291
 
299
292
  engine = env[:ai][:active].to_s.downcase.to_sym
300
293
  raise "ERROR: Unsupported AI Engine: #{engine} in #{pwn_env_path}. Supported AI Engines:\n#{valid_ai_engines.inspect}" unless valid_ai_engines.include?(engine)
301
294
 
295
+ # Determine whether the active engine already has usable auth
296
+ # material so the pwn / pwn-ai REPL driver does not prompt for an
297
+ # API key when OAuth is configured via pwn-vault.
298
+ #
299
+ # A value is considered "real" when it is non-blank AND is not one
300
+ # of the placeholder strings ("optional - ..." / "required - ...")
301
+ # written by PWN::Config.default_env into a fresh ~/.pwn/pwn.yaml.
302
+ real_cfg = lambda do |v|
303
+ s = v.to_s.strip
304
+ !s.empty? && !s.match?(/\A(optional|required)\b/i)
305
+ end
306
+
302
307
  key = env[:ai][engine][:key]
303
- oauth_access = nil
308
+ key = nil unless real_cfg.call(key)
309
+
310
+ oauth_configured = false
304
311
  if engine == :grok
305
- oauth = env[:ai][engine][:oauth] ||= {}
306
- oauth_access = oauth[:bearer_token] if oauth[:bearer_token] && !oauth[:bearer_token].to_s.match?(/optional/i) && !oauth[:bearer_token].to_s.empty?
312
+ oauth = env[:ai][engine][:oauth]
313
+ oauth = env[:ai][engine][:oauth] = {} unless oauth.is_a?(Hash)
314
+ # OAuth is considered configured when either a bearer_token is
315
+ # stored (preferred, long-lived) OR client_id + client_secret are
316
+ # present (PWN::AI::Grok will run the singular enrollment flow).
317
+ oauth_configured = real_cfg.call(oauth[:bearer_token]) ||
318
+ (real_cfg.call(oauth[:client_id]) && real_cfg.call(oauth[:client_secret]))
307
319
  end
308
- if key.nil? && oauth_access.nil?
320
+
321
+ if key.nil? && !oauth_configured
309
322
  key = PWN::Plugins::AuthenticationHelper.mask_password(
310
- prompt: "#{engine} API Key (or configure oauth:bearer_token in pwn-vault for xAI SuperGrok subscriptions)"
323
+ prompt: "#{engine} API Key (or store ai.grok.oauth.refresh_token via pwn-vault -- run PWN::AI::Grok.obtain_oauth_bearer_token to enroll)"
311
324
  )
312
325
  env[:ai][engine][:key] = key
313
326
  end
@@ -372,6 +385,53 @@ module PWN
372
385
  File.join(File.dirname(pwn_env_path), 'skills')
373
386
  end
374
387
 
388
+ # Supported Method Parameters::
389
+ # refs = PWN::Config.parse_skill_references(content: '...')
390
+ #
391
+ # Extracts an Array of reference strings (URLs, CWE/CVE/ATT&CK ids, etc.)
392
+ # from a skill body. Supports two formats:
393
+ # 1) YAML front-matter block: ---\nreferences:\n - https://...\n---\n
394
+ # 2) Markdown section: ## References\n- https://...\n
395
+ public_class_method def self.parse_skill_references(opts = {})
396
+ content = opts[:content].to_s
397
+ refs = []
398
+
399
+ # YAML front-matter (--- ... ---) at top of file
400
+ if content.start_with?("---\n")
401
+ fm_end = content.index("\n---", 4)
402
+ if fm_end
403
+ begin
404
+ require 'yaml'
405
+ fm = YAML.safe_load(content[4..fm_end], permitted_classes: [], aliases: false) || {}
406
+ r = fm['references'] || fm[:references]
407
+ refs.concat(Array(r).map(&:to_s)) if r
408
+ rescue StandardError
409
+ # ignore malformed front-matter
410
+ end
411
+ end
412
+ end
413
+
414
+ # Markdown "## References" section (bullets or bare lines until next heading / EOF)
415
+ if content =~ /^\s*\#{1,3}\s*References\s*$/i
416
+ in_section = false
417
+ content.each_line do |line|
418
+ if line =~ /^\s*\#{1,3}\s*References\s*$/i
419
+ in_section = true
420
+ next
421
+ end
422
+ next unless in_section
423
+ break if line =~ /^\s*\#{1,3}\s+\S/ # next heading
424
+
425
+ l = line.strip.sub(/^[-*]\s*/, '')
426
+ refs << l unless l.empty?
427
+ end
428
+ end
429
+
430
+ refs.map(&:strip).reject(&:empty?).uniq
431
+ rescue StandardError
432
+ []
433
+ end
434
+
375
435
  # Supported Method Parameters::
376
436
  # skills = PWN::Config.load_skills(
377
437
  # pwn_skills_path: 'optional - Path to skills folder. Defaults to ~/.pwn/skills'
@@ -396,12 +456,12 @@ module PWN
396
456
  if ext == '.rb'
397
457
  begin
398
458
  require skill_file
399
- skills[basename] = { type: :ruby, path: skill_file, content: content, loaded: true }
459
+ skills[basename] = { type: :ruby, path: skill_file, content: content, loaded: true, references: parse_skill_references(content: content) }
400
460
  rescue StandardError => e
401
- skills[basename] = { type: :ruby, path: skill_file, content: content, loaded: false, error: e.message }
461
+ skills[basename] = { type: :ruby, path: skill_file, content: content, loaded: false, error: e.message, references: parse_skill_references(content: content) }
402
462
  end
403
463
  else
404
- skills[basename] = { type: :instruction, path: skill_file, content: content }
464
+ skills[basename] = { type: :instruction, path: skill_file, content: content, references: parse_skill_references(content: content) }
405
465
  end
406
466
  end
407
467