Update app.py
Browse files
app.py
CHANGED
|
@@ -20,6 +20,19 @@ DEFAULT_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
|
| 20 |
DEFAULT_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
| 21 |
DEFAULT_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# =====================================================================
|
| 24 |
# 2. STRICT VALIDATION & INPUT SANITIZATION LAYER
|
| 25 |
# =====================================================================
|
|
@@ -55,6 +68,7 @@ def validate_http_url(url: str) -> str:
|
|
| 55 |
return url
|
| 56 |
|
| 57 |
|
|
|
|
| 58 |
class SubdomainEnumInput(BaseModel):
|
| 59 |
domain: str = Field(..., description="Target domain to inspect (e.g., example.com)")
|
| 60 |
|
|
@@ -104,10 +118,146 @@ class PacketCaptureInput(BaseModel):
|
|
| 104 |
return v
|
| 105 |
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# =====================================================================
|
| 108 |
-
# 3. ASYNCHRONOUS
|
| 109 |
# =====================================================================
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
async def run_subdomain_enumerator(domain: str) -> Dict[str, Any]:
|
| 112 |
"""Query certificate transparency logs (crt.sh) for domain asset mapping."""
|
| 113 |
try:
|
|
@@ -140,11 +290,7 @@ async def run_subdomain_enumerator(domain: str) -> Dict[str, Any]:
|
|
| 140 |
"subdomains": sorted(list(subdomains))[:100]
|
| 141 |
}
|
| 142 |
except Exception as exc:
|
| 143 |
-
return {
|
| 144 |
-
"status": "error",
|
| 145 |
-
"message": f"Subdomain enumeration failed: {str(exc)}",
|
| 146 |
-
"subdomains": []
|
| 147 |
-
}
|
| 148 |
|
| 149 |
|
| 150 |
async def run_nmap_port_scanner(target: str) -> Dict[str, Any]:
|
|
@@ -169,15 +315,9 @@ async def run_nmap_port_scanner(target: str) -> Dict[str, Any]:
|
|
| 169 |
"stderr": stderr.decode("utf-8", errors="replace")
|
| 170 |
}
|
| 171 |
except asyncio.TimeoutError:
|
| 172 |
-
return {
|
| 173 |
-
"status": "error",
|
| 174 |
-
"message": "Nmap scan execution timed out (45s limit)."
|
| 175 |
-
}
|
| 176 |
except Exception as exc:
|
| 177 |
-
return {
|
| 178 |
-
"status": "error",
|
| 179 |
-
"message": f"Nmap execution error: {str(exc)}"
|
| 180 |
-
}
|
| 181 |
|
| 182 |
|
| 183 |
async def run_web_scraper_and_analyzer(url: str) -> Dict[str, Any]:
|
|
@@ -186,9 +326,7 @@ async def run_web_scraper_and_analyzer(url: str) -> Dict[str, Any]:
|
|
| 186 |
validated = WebScraperInput(url=url)
|
| 187 |
target_url = validated.url
|
| 188 |
|
| 189 |
-
headers = {
|
| 190 |
-
"User-Agent": "Diagnostic-Audit-Agent/1.0 (Compliance; Non-Intrusive)"
|
| 191 |
-
}
|
| 192 |
|
| 193 |
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
|
| 194 |
resp = await client.get(target_url, headers=headers)
|
|
@@ -223,10 +361,7 @@ async def run_web_scraper_and_analyzer(url: str) -> Dict[str, Any]:
|
|
| 223 |
"external_scripts": scripts
|
| 224 |
}
|
| 225 |
except Exception as exc:
|
| 226 |
-
return {
|
| 227 |
-
"status": "error",
|
| 228 |
-
"message": f"Web analyzer error: {str(exc)}"
|
| 229 |
-
}
|
| 230 |
|
| 231 |
|
| 232 |
async def run_packet_capture_analyzer(pcap_path: str, display_filter: str = "") -> Dict[str, Any]:
|
|
@@ -237,10 +372,7 @@ async def run_packet_capture_analyzer(pcap_path: str, display_filter: str = "")
|
|
| 237 |
filt = validated.display_filter
|
| 238 |
|
| 239 |
if not os.path.exists(target_path):
|
| 240 |
-
return {
|
| 241 |
-
"status": "error",
|
| 242 |
-
"message": f"PCAP file '{target_path}' not found in runtime directory."
|
| 243 |
-
}
|
| 244 |
|
| 245 |
cmd = ["tshark", "-r", target_path, "-c", "50"]
|
| 246 |
if filt:
|
|
@@ -260,10 +392,7 @@ async def run_packet_capture_analyzer(pcap_path: str, display_filter: str = "")
|
|
| 260 |
"stderr": stderr.decode("utf-8", errors="replace")
|
| 261 |
}
|
| 262 |
except Exception as exc:
|
| 263 |
-
return {
|
| 264 |
-
"status": "error",
|
| 265 |
-
"message": f"Tshark analysis error: {str(exc)}"
|
| 266 |
-
}
|
| 267 |
|
| 268 |
|
| 269 |
# =====================================================================
|
|
@@ -271,6 +400,98 @@ async def run_packet_capture_analyzer(pcap_path: str, display_filter: str = "")
|
|
| 271 |
# =====================================================================
|
| 272 |
|
| 273 |
DIAGNOSTIC_TOOLS = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
{
|
| 275 |
"type": "function",
|
| 276 |
"function": {
|
|
@@ -346,6 +567,10 @@ DIAGNOSTIC_TOOLS = [
|
|
| 346 |
]
|
| 347 |
|
| 348 |
TOOL_HANDLERS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
"subdomain_enumerator": run_subdomain_enumerator,
|
| 350 |
"nmap_port_scanner": run_nmap_port_scanner,
|
| 351 |
"web_scraper_and_analyzer": run_web_scraper_and_analyzer,
|
|
@@ -357,18 +582,8 @@ TOOL_HANDLERS = {
|
|
| 357 |
# 5. RECURSIVE AGENTIC REACTION LOOP
|
| 358 |
# =====================================================================
|
| 359 |
|
| 360 |
-
SYSTEM_PROMPT = """You are an automated System Administration and Infrastructure Diagnostics Agent.
|
| 361 |
-
You assist system administrators and network engineers in auditing network assets, inspecting security configurations, and mapping infrastructure surfaces.
|
| 362 |
-
|
| 363 |
-
Operational Guidelines:
|
| 364 |
-
1. Systematically investigate target infrastructure using the provided diagnostic tools.
|
| 365 |
-
2. If tool outputs indicate further investigation is necessary, call subsequent tools autonomously.
|
| 366 |
-
3. Present final summaries structured with: Target Profile, Technical Diagnostics, Configuration Observations, and Remediation/Hardening Recommendations.
|
| 367 |
-
4. Maintain a neutral, engineering-focused reporting format."""
|
| 368 |
-
|
| 369 |
-
|
| 370 |
async def execute_tool(tool_name: str, arguments_json: str) -> str:
|
| 371 |
-
"""Safely parse arguments and route to designated
|
| 372 |
if tool_name not in TOOL_HANDLERS:
|
| 373 |
return json.dumps({"error": f"Tool '{tool_name}' not recognized."})
|
| 374 |
|
|
@@ -391,10 +606,11 @@ async def run_agent_loop(
|
|
| 391 |
api_key: str,
|
| 392 |
base_url: str,
|
| 393 |
model_name: str,
|
| 394 |
-
|
|
|
|
| 395 |
):
|
| 396 |
"""
|
| 397 |
-
Executes the asynchronous multi-turn tool calling loop
|
| 398 |
Streams execution progress, reasoning traces, and the final synthesis.
|
| 399 |
"""
|
| 400 |
if not api_key:
|
|
@@ -403,7 +619,7 @@ async def run_agent_loop(
|
|
| 403 |
|
| 404 |
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
| 405 |
|
| 406 |
-
messages = [{"role": "system", "content":
|
| 407 |
for msg in chat_history:
|
| 408 |
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 409 |
messages.append({"role": "user", "content": user_message})
|
|
@@ -511,7 +727,7 @@ def create_ui():
|
|
| 511 |
"""
|
| 512 |
)
|
| 513 |
|
| 514 |
-
with gr.Accordion("⚙️ Endpoint &
|
| 515 |
with gr.Row():
|
| 516 |
api_key_input = gr.Textbox(
|
| 517 |
label="API Key",
|
|
@@ -527,10 +743,14 @@ def create_ui():
|
|
| 527 |
label="Model Identifier",
|
| 528 |
value=DEFAULT_MODEL
|
| 529 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
with gr.Row():
|
| 532 |
with gr.Column(scale=5):
|
| 533 |
-
# Cleaned keyword arguments for universal Gradio 4/5/6 compatibility
|
| 534 |
chatbot = gr.Chatbot(
|
| 535 |
label="Diagnostic Workflow Session",
|
| 536 |
height=520
|
|
@@ -538,7 +758,7 @@ def create_ui():
|
|
| 538 |
with gr.Row():
|
| 539 |
user_input = gr.Textbox(
|
| 540 |
label="Audit Command / Target Specification",
|
| 541 |
-
placeholder="E.g.,
|
| 542 |
lines=2,
|
| 543 |
scale=4
|
| 544 |
)
|
|
@@ -552,7 +772,7 @@ def create_ui():
|
|
| 552 |
elem_id="terminal-output"
|
| 553 |
)
|
| 554 |
|
| 555 |
-
async def on_submit(user_msg, chat_hist, key, url, model):
|
| 556 |
if not user_msg.strip():
|
| 557 |
yield chat_hist, "*No input provided.*"
|
| 558 |
return
|
|
@@ -563,14 +783,14 @@ def create_ui():
|
|
| 563 |
chat_history=chat_hist,
|
| 564 |
api_key=key,
|
| 565 |
base_url=url,
|
| 566 |
-
model_name=model
|
|
|
|
| 567 |
):
|
| 568 |
yield updated_chat, updated_logs
|
| 569 |
|
| 570 |
-
# Submit handlers (Button click + Enter key press)
|
| 571 |
submit_btn.click(
|
| 572 |
fn=on_submit,
|
| 573 |
-
inputs=[user_input, chatbot, api_key_input, base_url_input, model_name_input],
|
| 574 |
outputs=[chatbot, terminal_logs]
|
| 575 |
).then(
|
| 576 |
fn=lambda: "",
|
|
@@ -580,7 +800,7 @@ def create_ui():
|
|
| 580 |
|
| 581 |
user_input.submit(
|
| 582 |
fn=on_submit,
|
| 583 |
-
inputs=[user_input, chatbot, api_key_input, base_url_input, model_name_input],
|
| 584 |
outputs=[chatbot, terminal_logs]
|
| 585 |
).then(
|
| 586 |
fn=lambda: "",
|
|
|
|
| 20 |
DEFAULT_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
| 21 |
DEFAULT_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat")
|
| 22 |
|
| 23 |
+
DEFAULT_SYSTEM_PROMPT = os.environ.get(
|
| 24 |
+
"SYSTEM_PROMPT",
|
| 25 |
+
"""You are an automated System Administration and Infrastructure Diagnostics Agent.
|
| 26 |
+
You assist system administrators and network engineers in auditing network assets, inspecting configurations, executing terminal diagnostics, and mapping infrastructure surfaces.
|
| 27 |
+
|
| 28 |
+
Operational Guidelines:
|
| 29 |
+
1. Systematically investigate target infrastructure using the provided diagnostic and terminal tools.
|
| 30 |
+
2. When necessary, use the bash executor, curl requester, or Python interpreter to inspect live responses, process outputs, or run diagnostic commands.
|
| 31 |
+
3. If tool outputs indicate further investigation is necessary, call subsequent tools autonomously.
|
| 32 |
+
4. Present final summaries structured with: Target Profile, Technical Diagnostics, Configuration Observations, and Hardening Recommendations.
|
| 33 |
+
5. Maintain a neutral, engineering-focused reporting format."""
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
# =====================================================================
|
| 37 |
# 2. STRICT VALIDATION & INPUT SANITIZATION LAYER
|
| 38 |
# =====================================================================
|
|
|
|
| 68 |
return url
|
| 69 |
|
| 70 |
|
| 71 |
+
# Pydantic Schemas for Tools
|
| 72 |
class SubdomainEnumInput(BaseModel):
|
| 73 |
domain: str = Field(..., description="Target domain to inspect (e.g., example.com)")
|
| 74 |
|
|
|
|
| 118 |
return v
|
| 119 |
|
| 120 |
|
| 121 |
+
class BashCommandInput(BaseModel):
|
| 122 |
+
command: str = Field(..., description="Shell command to execute in the persistent container environment")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class PythonCodeInput(BaseModel):
|
| 126 |
+
code: str = Field(..., description="Python 3 script code to execute in the container")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
class CurlRequestInput(BaseModel):
|
| 130 |
+
url: str = Field(..., description="Target URL to query with curl")
|
| 131 |
+
method: Optional[str] = Field(default="GET", description="HTTP Method (GET, POST, HEAD, OPTIONS)")
|
| 132 |
+
headers: Optional[List[str]] = Field(default=[], description="List of headers in 'Header: Value' format")
|
| 133 |
+
data: Optional[str] = Field(default="", description="Request body payload")
|
| 134 |
+
include_headers: Optional[bool] = Field(default=True, description="Whether to include response headers (-i)")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
class DnsLookupInput(BaseModel):
|
| 138 |
+
domain: str = Field(..., description="Domain name or host to query")
|
| 139 |
+
record_type: Optional[str] = Field(default="ANY", description="DNS record type (A, AAAA, MX, NS, TXT, CNAME, ANY)")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
# =====================================================================
|
| 143 |
+
# 3. ASYNCHRONOUS TOOL IMPLEMENTATIONS
|
| 144 |
# =====================================================================
|
| 145 |
|
| 146 |
+
async def run_bash_executor(command: str) -> Dict[str, Any]:
|
| 147 |
+
"""Executes arbitrary shell commands in the persistent container workspace."""
|
| 148 |
+
try:
|
| 149 |
+
validated = BashCommandInput(command=command)
|
| 150 |
+
process = await asyncio.create_subprocess_shell(
|
| 151 |
+
validated.command,
|
| 152 |
+
stdout=asyncio.subprocess.PIPE,
|
| 153 |
+
stderr=asyncio.subprocess.PIPE,
|
| 154 |
+
cwd="/app"
|
| 155 |
+
)
|
| 156 |
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60.0)
|
| 157 |
+
|
| 158 |
+
return {
|
| 159 |
+
"status": "success" if process.returncode == 0 else "error",
|
| 160 |
+
"exit_code": process.returncode,
|
| 161 |
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
| 162 |
+
"stderr": stderr.decode("utf-8", errors="replace")
|
| 163 |
+
}
|
| 164 |
+
except asyncio.TimeoutError:
|
| 165 |
+
return {"status": "error", "message": "Command execution timed out (60s limit)."}
|
| 166 |
+
except Exception as exc:
|
| 167 |
+
return {"status": "error", "message": f"Execution error: {str(exc)}"}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
async def run_python_interpreter(code: str) -> Dict[str, Any]:
|
| 171 |
+
"""Executes Python code scripts asynchronously in the runtime container."""
|
| 172 |
+
try:
|
| 173 |
+
validated = PythonCodeInput(code=code)
|
| 174 |
+
process = await asyncio.create_subprocess_exec(
|
| 175 |
+
"python3", "-c", validated.code,
|
| 176 |
+
stdout=asyncio.subprocess.PIPE,
|
| 177 |
+
stderr=asyncio.subprocess.PIPE,
|
| 178 |
+
cwd="/app"
|
| 179 |
+
)
|
| 180 |
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30.0)
|
| 181 |
+
|
| 182 |
+
return {
|
| 183 |
+
"status": "success" if process.returncode == 0 else "error",
|
| 184 |
+
"exit_code": process.returncode,
|
| 185 |
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
| 186 |
+
"stderr": stderr.decode("utf-8", errors="replace")
|
| 187 |
+
}
|
| 188 |
+
except asyncio.TimeoutError:
|
| 189 |
+
return {"status": "error", "message": "Python script execution timed out (30s limit)."}
|
| 190 |
+
except Exception as exc:
|
| 191 |
+
return {"status": "error", "message": f"Python execution error: {str(exc)}"}
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
async def run_curl_requester(
|
| 195 |
+
url: str,
|
| 196 |
+
method: str = "GET",
|
| 197 |
+
headers: Optional[List[str]] = None,
|
| 198 |
+
data: Optional[str] = "",
|
| 199 |
+
include_headers: bool = True
|
| 200 |
+
) -> Dict[str, Any]:
|
| 201 |
+
"""Performs HTTP/HTTPS network operations using system curl."""
|
| 202 |
+
try:
|
| 203 |
+
validated = CurlRequestInput(
|
| 204 |
+
url=url,
|
| 205 |
+
method=method,
|
| 206 |
+
headers=headers or [],
|
| 207 |
+
data=data or "",
|
| 208 |
+
include_headers=include_headers
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
cmd = ["curl", "-s", "-S", "-X", validated.method.upper()]
|
| 212 |
+
if validated.include_headers:
|
| 213 |
+
cmd.append("-i")
|
| 214 |
+
if validated.headers:
|
| 215 |
+
for h in validated.headers:
|
| 216 |
+
cmd.extend(["-H", h])
|
| 217 |
+
if validated.data:
|
| 218 |
+
cmd.extend(["--data", validated.data])
|
| 219 |
+
cmd.append(validated.url)
|
| 220 |
+
|
| 221 |
+
process = await asyncio.create_subprocess_exec(
|
| 222 |
+
*cmd,
|
| 223 |
+
stdout=asyncio.subprocess.PIPE,
|
| 224 |
+
stderr=asyncio.subprocess.PIPE
|
| 225 |
+
)
|
| 226 |
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30.0)
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
"status": "success" if process.returncode == 0 else "error",
|
| 230 |
+
"exit_code": process.returncode,
|
| 231 |
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
| 232 |
+
"stderr": stderr.decode("utf-8", errors="replace")
|
| 233 |
+
}
|
| 234 |
+
except Exception as exc:
|
| 235 |
+
return {"status": "error", "message": f"Curl execution error: {str(exc)}"}
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
async def run_dns_lookup(domain: str, record_type: str = "ANY") -> Dict[str, Any]:
|
| 239 |
+
"""Queries DNS records using dig."""
|
| 240 |
+
try:
|
| 241 |
+
validated = DnsLookupInput(domain=domain, record_type=record_type)
|
| 242 |
+
cmd = ["dig", "+noall", "+answer", "+comments", validated.domain, validated.record_type.upper()]
|
| 243 |
+
|
| 244 |
+
process = await asyncio.create_subprocess_exec(
|
| 245 |
+
*cmd,
|
| 246 |
+
stdout=asyncio.subprocess.PIPE,
|
| 247 |
+
stderr=asyncio.subprocess.PIPE
|
| 248 |
+
)
|
| 249 |
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=15.0)
|
| 250 |
+
|
| 251 |
+
return {
|
| 252 |
+
"status": "success" if process.returncode == 0 else "error",
|
| 253 |
+
"exit_code": process.returncode,
|
| 254 |
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
| 255 |
+
"stderr": stderr.decode("utf-8", errors="replace")
|
| 256 |
+
}
|
| 257 |
+
except Exception as exc:
|
| 258 |
+
return {"status": "error", "message": f"DNS query error: {str(exc)}"}
|
| 259 |
+
|
| 260 |
+
|
| 261 |
async def run_subdomain_enumerator(domain: str) -> Dict[str, Any]:
|
| 262 |
"""Query certificate transparency logs (crt.sh) for domain asset mapping."""
|
| 263 |
try:
|
|
|
|
| 290 |
"subdomains": sorted(list(subdomains))[:100]
|
| 291 |
}
|
| 292 |
except Exception as exc:
|
| 293 |
+
return {"status": "error", "message": f"Subdomain enumeration failed: {str(exc)}", "subdomains": []}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
|
| 296 |
async def run_nmap_port_scanner(target: str) -> Dict[str, Any]:
|
|
|
|
| 315 |
"stderr": stderr.decode("utf-8", errors="replace")
|
| 316 |
}
|
| 317 |
except asyncio.TimeoutError:
|
| 318 |
+
return {"status": "error", "message": "Nmap scan execution timed out (45s limit)."}
|
|
|
|
|
|
|
|
|
|
| 319 |
except Exception as exc:
|
| 320 |
+
return {"status": "error", "message": f"Nmap execution error: {str(exc)}"}
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
|
| 323 |
async def run_web_scraper_and_analyzer(url: str) -> Dict[str, Any]:
|
|
|
|
| 326 |
validated = WebScraperInput(url=url)
|
| 327 |
target_url = validated.url
|
| 328 |
|
| 329 |
+
headers = {"User-Agent": "Diagnostic-Audit-Agent/1.0 (Compliance; Non-Intrusive)"}
|
|
|
|
|
|
|
| 330 |
|
| 331 |
async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client:
|
| 332 |
resp = await client.get(target_url, headers=headers)
|
|
|
|
| 361 |
"external_scripts": scripts
|
| 362 |
}
|
| 363 |
except Exception as exc:
|
| 364 |
+
return {"status": "error", "message": f"Web analyzer error: {str(exc)}"}
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
|
| 367 |
async def run_packet_capture_analyzer(pcap_path: str, display_filter: str = "") -> Dict[str, Any]:
|
|
|
|
| 372 |
filt = validated.display_filter
|
| 373 |
|
| 374 |
if not os.path.exists(target_path):
|
| 375 |
+
return {"status": "error", "message": f"PCAP file '{target_path}' not found in runtime directory."}
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
cmd = ["tshark", "-r", target_path, "-c", "50"]
|
| 378 |
if filt:
|
|
|
|
| 392 |
"stderr": stderr.decode("utf-8", errors="replace")
|
| 393 |
}
|
| 394 |
except Exception as exc:
|
| 395 |
+
return {"status": "error", "message": f"Tshark analysis error: {str(exc)}"}
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
|
| 398 |
# =====================================================================
|
|
|
|
| 400 |
# =====================================================================
|
| 401 |
|
| 402 |
DIAGNOSTIC_TOOLS = [
|
| 403 |
+
{
|
| 404 |
+
"type": "function",
|
| 405 |
+
"function": {
|
| 406 |
+
"name": "bash_executor",
|
| 407 |
+
"description": "Executes shell commands directly inside the persistent Linux container workspace (/app).",
|
| 408 |
+
"parameters": {
|
| 409 |
+
"type": "object",
|
| 410 |
+
"properties": {
|
| 411 |
+
"command": {
|
| 412 |
+
"type": "string",
|
| 413 |
+
"description": "The bash shell command to run."
|
| 414 |
+
}
|
| 415 |
+
},
|
| 416 |
+
"required": ["command"]
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"type": "function",
|
| 422 |
+
"function": {
|
| 423 |
+
"name": "python_interpreter",
|
| 424 |
+
"description": "Runs Python 3 code in the container runtime environment.",
|
| 425 |
+
"parameters": {
|
| 426 |
+
"type": "object",
|
| 427 |
+
"properties": {
|
| 428 |
+
"code": {
|
| 429 |
+
"type": "string",
|
| 430 |
+
"description": "Python code snippet to execute."
|
| 431 |
+
}
|
| 432 |
+
},
|
| 433 |
+
"required": ["code"]
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
},
|
| 437 |
+
{
|
| 438 |
+
"type": "function",
|
| 439 |
+
"function": {
|
| 440 |
+
"name": "curl_requester",
|
| 441 |
+
"description": "Issues HTTP/HTTPS network requests using curl with custom methods, headers, and payloads.",
|
| 442 |
+
"parameters": {
|
| 443 |
+
"type": "object",
|
| 444 |
+
"properties": {
|
| 445 |
+
"url": {
|
| 446 |
+
"type": "string",
|
| 447 |
+
"description": "Target URL."
|
| 448 |
+
},
|
| 449 |
+
"method": {
|
| 450 |
+
"type": "string",
|
| 451 |
+
"description": "HTTP Method (GET, POST, HEAD, OPTIONS).",
|
| 452 |
+
"default": "GET"
|
| 453 |
+
},
|
| 454 |
+
"headers": {
|
| 455 |
+
"type": "array",
|
| 456 |
+
"items": {"type": "string"},
|
| 457 |
+
"description": "List of headers in 'Header: Value' format."
|
| 458 |
+
},
|
| 459 |
+
"data": {
|
| 460 |
+
"type": "string",
|
| 461 |
+
"description": "Request body data."
|
| 462 |
+
},
|
| 463 |
+
"include_headers": {
|
| 464 |
+
"type": "boolean",
|
| 465 |
+
"description": "Include response headers in output.",
|
| 466 |
+
"default": True
|
| 467 |
+
}
|
| 468 |
+
},
|
| 469 |
+
"required": ["url"]
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
},
|
| 473 |
+
{
|
| 474 |
+
"type": "function",
|
| 475 |
+
"function": {
|
| 476 |
+
"name": "dns_lookup",
|
| 477 |
+
"description": "Queries DNS records using dig for domain discovery and verification.",
|
| 478 |
+
"parameters": {
|
| 479 |
+
"type": "object",
|
| 480 |
+
"properties": {
|
| 481 |
+
"domain": {
|
| 482 |
+
"type": "string",
|
| 483 |
+
"description": "Domain or host to look up."
|
| 484 |
+
},
|
| 485 |
+
"record_type": {
|
| 486 |
+
"type": "string",
|
| 487 |
+
"description": "Record type: A, AAAA, MX, NS, TXT, CNAME, ANY.",
|
| 488 |
+
"default": "ANY"
|
| 489 |
+
}
|
| 490 |
+
},
|
| 491 |
+
"required": ["domain"]
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
},
|
| 495 |
{
|
| 496 |
"type": "function",
|
| 497 |
"function": {
|
|
|
|
| 567 |
]
|
| 568 |
|
| 569 |
TOOL_HANDLERS = {
|
| 570 |
+
"bash_executor": run_bash_executor,
|
| 571 |
+
"python_interpreter": run_python_interpreter,
|
| 572 |
+
"curl_requester": run_curl_requester,
|
| 573 |
+
"dns_lookup": run_dns_lookup,
|
| 574 |
"subdomain_enumerator": run_subdomain_enumerator,
|
| 575 |
"nmap_port_scanner": run_nmap_port_scanner,
|
| 576 |
"web_scraper_and_analyzer": run_web_scraper_and_analyzer,
|
|
|
|
| 582 |
# 5. RECURSIVE AGENTIC REACTION LOOP
|
| 583 |
# =====================================================================
|
| 584 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
async def execute_tool(tool_name: str, arguments_json: str) -> str:
|
| 586 |
+
"""Safely parse arguments and route to designated tool wrapper."""
|
| 587 |
if tool_name not in TOOL_HANDLERS:
|
| 588 |
return json.dumps({"error": f"Tool '{tool_name}' not recognized."})
|
| 589 |
|
|
|
|
| 606 |
api_key: str,
|
| 607 |
base_url: str,
|
| 608 |
model_name: str,
|
| 609 |
+
system_prompt: str,
|
| 610 |
+
max_steps: int = 6
|
| 611 |
):
|
| 612 |
"""
|
| 613 |
+
Executes the asynchronous multi-turn tool calling loop.
|
| 614 |
Streams execution progress, reasoning traces, and the final synthesis.
|
| 615 |
"""
|
| 616 |
if not api_key:
|
|
|
|
| 619 |
|
| 620 |
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
| 621 |
|
| 622 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 623 |
for msg in chat_history:
|
| 624 |
messages.append({"role": msg["role"], "content": msg["content"]})
|
| 625 |
messages.append({"role": "user", "content": user_message})
|
|
|
|
| 727 |
"""
|
| 728 |
)
|
| 729 |
|
| 730 |
+
with gr.Accordion("⚙️ Endpoint & System Settings", open=False):
|
| 731 |
with gr.Row():
|
| 732 |
api_key_input = gr.Textbox(
|
| 733 |
label="API Key",
|
|
|
|
| 743 |
label="Model Identifier",
|
| 744 |
value=DEFAULT_MODEL
|
| 745 |
)
|
| 746 |
+
system_prompt_input = gr.Textbox(
|
| 747 |
+
label="System Prompt (Loaded from SYSTEM_PROMPT env by default)",
|
| 748 |
+
value=DEFAULT_SYSTEM_PROMPT,
|
| 749 |
+
lines=4
|
| 750 |
+
)
|
| 751 |
|
| 752 |
with gr.Row():
|
| 753 |
with gr.Column(scale=5):
|
|
|
|
| 754 |
chatbot = gr.Chatbot(
|
| 755 |
label="Diagnostic Workflow Session",
|
| 756 |
height=520
|
|
|
|
| 758 |
with gr.Row():
|
| 759 |
user_input = gr.Textbox(
|
| 760 |
label="Audit Command / Target Specification",
|
| 761 |
+
placeholder="E.g., Query DNS records, check HTTP response headers, or run a diagnostic script for scanme.nmap.org",
|
| 762 |
lines=2,
|
| 763 |
scale=4
|
| 764 |
)
|
|
|
|
| 772 |
elem_id="terminal-output"
|
| 773 |
)
|
| 774 |
|
| 775 |
+
async def on_submit(user_msg, chat_hist, key, url, model, sys_prompt):
|
| 776 |
if not user_msg.strip():
|
| 777 |
yield chat_hist, "*No input provided.*"
|
| 778 |
return
|
|
|
|
| 783 |
chat_history=chat_hist,
|
| 784 |
api_key=key,
|
| 785 |
base_url=url,
|
| 786 |
+
model_name=model,
|
| 787 |
+
system_prompt=sys_prompt
|
| 788 |
):
|
| 789 |
yield updated_chat, updated_logs
|
| 790 |
|
|
|
|
| 791 |
submit_btn.click(
|
| 792 |
fn=on_submit,
|
| 793 |
+
inputs=[user_input, chatbot, api_key_input, base_url_input, model_name_input, system_prompt_input],
|
| 794 |
outputs=[chatbot, terminal_logs]
|
| 795 |
).then(
|
| 796 |
fn=lambda: "",
|
|
|
|
| 800 |
|
| 801 |
user_input.submit(
|
| 802 |
fn=on_submit,
|
| 803 |
+
inputs=[user_input, chatbot, api_key_input, base_url_input, model_name_input, system_prompt_input],
|
| 804 |
outputs=[chatbot, terminal_logs]
|
| 805 |
).then(
|
| 806 |
fn=lambda: "",
|