aiptx 2.0.7__py3-none-any.whl

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 (187) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +46 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/exploit_agent.py +688 -0
  8. aipt_v2/agents/ptt.py +406 -0
  9. aipt_v2/agents/state.py +168 -0
  10. aipt_v2/app.py +957 -0
  11. aipt_v2/browser/__init__.py +31 -0
  12. aipt_v2/browser/automation.py +458 -0
  13. aipt_v2/browser/crawler.py +453 -0
  14. aipt_v2/cli.py +2933 -0
  15. aipt_v2/compliance/__init__.py +71 -0
  16. aipt_v2/compliance/compliance_report.py +449 -0
  17. aipt_v2/compliance/framework_mapper.py +424 -0
  18. aipt_v2/compliance/nist_mapping.py +345 -0
  19. aipt_v2/compliance/owasp_mapping.py +330 -0
  20. aipt_v2/compliance/pci_mapping.py +297 -0
  21. aipt_v2/config.py +341 -0
  22. aipt_v2/core/__init__.py +43 -0
  23. aipt_v2/core/agent.py +630 -0
  24. aipt_v2/core/llm.py +395 -0
  25. aipt_v2/core/memory.py +305 -0
  26. aipt_v2/core/ptt.py +329 -0
  27. aipt_v2/database/__init__.py +14 -0
  28. aipt_v2/database/models.py +232 -0
  29. aipt_v2/database/repository.py +384 -0
  30. aipt_v2/docker/__init__.py +23 -0
  31. aipt_v2/docker/builder.py +260 -0
  32. aipt_v2/docker/manager.py +222 -0
  33. aipt_v2/docker/sandbox.py +371 -0
  34. aipt_v2/evasion/__init__.py +58 -0
  35. aipt_v2/evasion/request_obfuscator.py +272 -0
  36. aipt_v2/evasion/tls_fingerprint.py +285 -0
  37. aipt_v2/evasion/ua_rotator.py +301 -0
  38. aipt_v2/evasion/waf_bypass.py +439 -0
  39. aipt_v2/execution/__init__.py +23 -0
  40. aipt_v2/execution/executor.py +302 -0
  41. aipt_v2/execution/parser.py +544 -0
  42. aipt_v2/execution/terminal.py +337 -0
  43. aipt_v2/health.py +437 -0
  44. aipt_v2/intelligence/__init__.py +194 -0
  45. aipt_v2/intelligence/adaptation.py +474 -0
  46. aipt_v2/intelligence/auth.py +520 -0
  47. aipt_v2/intelligence/chaining.py +775 -0
  48. aipt_v2/intelligence/correlation.py +536 -0
  49. aipt_v2/intelligence/cve_aipt.py +334 -0
  50. aipt_v2/intelligence/cve_info.py +1111 -0
  51. aipt_v2/intelligence/knowledge_graph.py +590 -0
  52. aipt_v2/intelligence/learning.py +626 -0
  53. aipt_v2/intelligence/llm_analyzer.py +502 -0
  54. aipt_v2/intelligence/llm_tool_selector.py +518 -0
  55. aipt_v2/intelligence/payload_generator.py +562 -0
  56. aipt_v2/intelligence/rag.py +239 -0
  57. aipt_v2/intelligence/scope.py +442 -0
  58. aipt_v2/intelligence/searchers/__init__.py +5 -0
  59. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  60. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  61. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  62. aipt_v2/intelligence/tools.json +443 -0
  63. aipt_v2/intelligence/triage.py +670 -0
  64. aipt_v2/interactive_shell.py +559 -0
  65. aipt_v2/interface/__init__.py +5 -0
  66. aipt_v2/interface/cli.py +230 -0
  67. aipt_v2/interface/main.py +501 -0
  68. aipt_v2/interface/tui.py +1276 -0
  69. aipt_v2/interface/utils.py +583 -0
  70. aipt_v2/llm/__init__.py +39 -0
  71. aipt_v2/llm/config.py +26 -0
  72. aipt_v2/llm/llm.py +514 -0
  73. aipt_v2/llm/memory.py +214 -0
  74. aipt_v2/llm/request_queue.py +89 -0
  75. aipt_v2/llm/utils.py +89 -0
  76. aipt_v2/local_tool_installer.py +1467 -0
  77. aipt_v2/models/__init__.py +15 -0
  78. aipt_v2/models/findings.py +295 -0
  79. aipt_v2/models/phase_result.py +224 -0
  80. aipt_v2/models/scan_config.py +207 -0
  81. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  82. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  83. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  84. aipt_v2/monitoring/prometheus.yml +60 -0
  85. aipt_v2/orchestration/__init__.py +52 -0
  86. aipt_v2/orchestration/pipeline.py +398 -0
  87. aipt_v2/orchestration/progress.py +300 -0
  88. aipt_v2/orchestration/scheduler.py +296 -0
  89. aipt_v2/orchestrator.py +2427 -0
  90. aipt_v2/payloads/__init__.py +27 -0
  91. aipt_v2/payloads/cmdi.py +150 -0
  92. aipt_v2/payloads/sqli.py +263 -0
  93. aipt_v2/payloads/ssrf.py +204 -0
  94. aipt_v2/payloads/templates.py +222 -0
  95. aipt_v2/payloads/traversal.py +166 -0
  96. aipt_v2/payloads/xss.py +204 -0
  97. aipt_v2/prompts/__init__.py +60 -0
  98. aipt_v2/proxy/__init__.py +29 -0
  99. aipt_v2/proxy/history.py +352 -0
  100. aipt_v2/proxy/interceptor.py +452 -0
  101. aipt_v2/recon/__init__.py +44 -0
  102. aipt_v2/recon/dns.py +241 -0
  103. aipt_v2/recon/osint.py +367 -0
  104. aipt_v2/recon/subdomain.py +372 -0
  105. aipt_v2/recon/tech_detect.py +311 -0
  106. aipt_v2/reports/__init__.py +17 -0
  107. aipt_v2/reports/generator.py +313 -0
  108. aipt_v2/reports/html_report.py +378 -0
  109. aipt_v2/runtime/__init__.py +53 -0
  110. aipt_v2/runtime/base.py +30 -0
  111. aipt_v2/runtime/docker.py +401 -0
  112. aipt_v2/runtime/local.py +346 -0
  113. aipt_v2/runtime/tool_server.py +205 -0
  114. aipt_v2/runtime/vps.py +830 -0
  115. aipt_v2/scanners/__init__.py +28 -0
  116. aipt_v2/scanners/base.py +273 -0
  117. aipt_v2/scanners/nikto.py +244 -0
  118. aipt_v2/scanners/nmap.py +402 -0
  119. aipt_v2/scanners/nuclei.py +273 -0
  120. aipt_v2/scanners/web.py +454 -0
  121. aipt_v2/scripts/security_audit.py +366 -0
  122. aipt_v2/setup_wizard.py +941 -0
  123. aipt_v2/skills/__init__.py +80 -0
  124. aipt_v2/skills/agents/__init__.py +14 -0
  125. aipt_v2/skills/agents/api_tester.py +706 -0
  126. aipt_v2/skills/agents/base.py +477 -0
  127. aipt_v2/skills/agents/code_review.py +459 -0
  128. aipt_v2/skills/agents/security_agent.py +336 -0
  129. aipt_v2/skills/agents/web_pentest.py +818 -0
  130. aipt_v2/skills/prompts/__init__.py +647 -0
  131. aipt_v2/system_detector.py +539 -0
  132. aipt_v2/telemetry/__init__.py +7 -0
  133. aipt_v2/telemetry/tracer.py +347 -0
  134. aipt_v2/terminal/__init__.py +28 -0
  135. aipt_v2/terminal/executor.py +400 -0
  136. aipt_v2/terminal/sandbox.py +350 -0
  137. aipt_v2/tools/__init__.py +44 -0
  138. aipt_v2/tools/active_directory/__init__.py +78 -0
  139. aipt_v2/tools/active_directory/ad_config.py +238 -0
  140. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  141. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  142. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  143. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  144. aipt_v2/tools/agents_graph/__init__.py +19 -0
  145. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  146. aipt_v2/tools/api_security/__init__.py +76 -0
  147. aipt_v2/tools/api_security/api_discovery.py +608 -0
  148. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  149. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  150. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  151. aipt_v2/tools/browser/__init__.py +5 -0
  152. aipt_v2/tools/browser/browser_actions.py +238 -0
  153. aipt_v2/tools/browser/browser_instance.py +535 -0
  154. aipt_v2/tools/browser/tab_manager.py +344 -0
  155. aipt_v2/tools/cloud/__init__.py +70 -0
  156. aipt_v2/tools/cloud/cloud_config.py +273 -0
  157. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  158. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  159. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  160. aipt_v2/tools/executor.py +307 -0
  161. aipt_v2/tools/parser.py +408 -0
  162. aipt_v2/tools/proxy/__init__.py +5 -0
  163. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  164. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  165. aipt_v2/tools/registry.py +196 -0
  166. aipt_v2/tools/scanners/__init__.py +343 -0
  167. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  168. aipt_v2/tools/scanners/burp_tool.py +631 -0
  169. aipt_v2/tools/scanners/config.py +156 -0
  170. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  171. aipt_v2/tools/scanners/zap_tool.py +612 -0
  172. aipt_v2/tools/terminal/__init__.py +5 -0
  173. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  174. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  175. aipt_v2/tools/terminal/terminal_session.py +449 -0
  176. aipt_v2/tools/tool_processing.py +108 -0
  177. aipt_v2/utils/__init__.py +17 -0
  178. aipt_v2/utils/logging.py +202 -0
  179. aipt_v2/utils/model_manager.py +187 -0
  180. aipt_v2/utils/searchers/__init__.py +269 -0
  181. aipt_v2/verify_install.py +793 -0
  182. aiptx-2.0.7.dist-info/METADATA +345 -0
  183. aiptx-2.0.7.dist-info/RECORD +187 -0
  184. aiptx-2.0.7.dist-info/WHEEL +5 -0
  185. aiptx-2.0.7.dist-info/entry_points.txt +7 -0
  186. aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
  187. aiptx-2.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,477 @@
1
+ """
2
+ Base Security Agent - Foundation for all AI-powered security testing agents.
3
+
4
+ Architecture inspired by multi-agent security testing systems with:
5
+ - LiteLLM integration for multi-provider LLM support (Claude, GPT, DeepSeek, etc.)
6
+ - Tool registry with XML schema for structured tool calls
7
+ - Async execution for concurrent testing
8
+ - Structured vulnerability findings output
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import re
14
+ import time
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import Any, Callable, Dict, List, Optional, Type
19
+ from pathlib import Path
20
+
21
+ import structlog
22
+ from pydantic import BaseModel, Field
23
+
24
+ logger = structlog.get_logger()
25
+
26
+
27
+ class Severity(str, Enum):
28
+ """Vulnerability severity levels following CVSS-like classification."""
29
+ CRITICAL = "critical"
30
+ HIGH = "high"
31
+ MEDIUM = "medium"
32
+ LOW = "low"
33
+ INFO = "info"
34
+
35
+
36
+ class VulnCategory(str, Enum):
37
+ """OWASP Top 10 2021 aligned vulnerability categories."""
38
+ INJECTION = "A03:2021-Injection"
39
+ BROKEN_AUTH = "A07:2021-Auth-Failures"
40
+ SENSITIVE_DATA = "A02:2021-Crypto-Failures"
41
+ XXE = "A05:2021-Security-Misconfiguration"
42
+ BROKEN_ACCESS = "A01:2021-Broken-Access-Control"
43
+ SECURITY_MISCONFIG = "A05:2021-Security-Misconfiguration"
44
+ XSS = "A03:2021-Injection"
45
+ INSECURE_DESER = "A08:2021-Software-Data-Integrity"
46
+ VULN_COMPONENTS = "A06:2021-Vulnerable-Components"
47
+ INSUFFICIENT_LOGGING = "A09:2021-Security-Logging-Failures"
48
+ SSRF = "A10:2021-SSRF"
49
+
50
+
51
+ @dataclass
52
+ class Finding:
53
+ """Represents a security finding/vulnerability discovered by an agent."""
54
+ title: str
55
+ severity: Severity
56
+ category: VulnCategory
57
+ description: str
58
+ evidence: str
59
+ location: str # File path, URL, or endpoint
60
+ line_number: Optional[int] = None
61
+ remediation: str = ""
62
+ cwe_id: Optional[str] = None
63
+ cvss_score: Optional[float] = None
64
+ references: List[str] = field(default_factory=list)
65
+ raw_output: str = ""
66
+
67
+ def to_dict(self) -> Dict[str, Any]:
68
+ return {
69
+ "title": self.title,
70
+ "severity": self.severity.value,
71
+ "category": self.category.value,
72
+ "description": self.description,
73
+ "evidence": self.evidence,
74
+ "location": self.location,
75
+ "line_number": self.line_number,
76
+ "remediation": self.remediation,
77
+ "cwe_id": self.cwe_id,
78
+ "cvss_score": self.cvss_score,
79
+ "references": self.references,
80
+ }
81
+
82
+
83
+ @dataclass
84
+ class AgentResult:
85
+ """Result of an agent's security testing run."""
86
+ success: bool
87
+ findings: List[Finding] = field(default_factory=list)
88
+ errors: List[str] = field(default_factory=list)
89
+ execution_time: float = 0.0
90
+ total_steps: int = 0
91
+ tokens_used: int = 0
92
+ model_used: str = ""
93
+ raw_transcript: List[Dict[str, Any]] = field(default_factory=list)
94
+
95
+ @property
96
+ def critical_count(self) -> int:
97
+ return sum(1 for f in self.findings if f.severity == Severity.CRITICAL)
98
+
99
+ @property
100
+ def high_count(self) -> int:
101
+ return sum(1 for f in self.findings if f.severity == Severity.HIGH)
102
+
103
+ def to_dict(self) -> Dict[str, Any]:
104
+ return {
105
+ "success": self.success,
106
+ "findings": [f.to_dict() for f in self.findings],
107
+ "errors": self.errors,
108
+ "execution_time": self.execution_time,
109
+ "total_steps": self.total_steps,
110
+ "tokens_used": self.tokens_used,
111
+ "model_used": self.model_used,
112
+ "summary": {
113
+ "total": len(self.findings),
114
+ "critical": self.critical_count,
115
+ "high": self.high_count,
116
+ }
117
+ }
118
+
119
+
120
+ class AgentConfig(BaseModel):
121
+ """Configuration for AI security agents."""
122
+ # LLM Settings
123
+ model: str = Field(default="claude-sonnet-4-20250514", description="LLM model to use")
124
+ temperature: float = Field(default=0.7, ge=0, le=2)
125
+ max_tokens: int = Field(default=4096, ge=1)
126
+
127
+ # Agent Behavior
128
+ max_steps: int = Field(default=100, description="Maximum agent steps before stopping")
129
+ timeout: int = Field(default=600, description="Timeout in seconds")
130
+ aggressive_mode: bool = Field(default=False, description="Enable aggressive testing")
131
+
132
+ # Tool Settings
133
+ enable_terminal: bool = Field(default=True)
134
+ enable_browser: bool = Field(default=False)
135
+ enable_http_client: bool = Field(default=True)
136
+
137
+ # Output Settings
138
+ verbose: bool = Field(default=False)
139
+ save_transcript: bool = Field(default=True)
140
+
141
+
142
+ # Tool Registry for Agent Tools
143
+ _tool_registry: Dict[str, Dict[str, Any]] = {}
144
+
145
+
146
+ def register_tool(
147
+ name: str,
148
+ description: str,
149
+ parameters: Dict[str, Any],
150
+ category: str = "general"
151
+ ) -> Callable:
152
+ """Decorator to register a tool for use by agents.
153
+
154
+ Example:
155
+ @register_tool(
156
+ name="run_command",
157
+ description="Execute a shell command",
158
+ parameters={"command": {"type": "string", "required": True}}
159
+ )
160
+ async def run_command(command: str) -> str:
161
+ ...
162
+ """
163
+ def decorator(func: Callable) -> Callable:
164
+ _tool_registry[name] = {
165
+ "name": name,
166
+ "description": description,
167
+ "parameters": parameters,
168
+ "category": category,
169
+ "function": func,
170
+ }
171
+ return func
172
+ return decorator
173
+
174
+
175
+ def get_tool(name: str) -> Optional[Dict[str, Any]]:
176
+ """Get a registered tool by name."""
177
+ return _tool_registry.get(name)
178
+
179
+
180
+ def get_all_tools(category: Optional[str] = None) -> Dict[str, Dict[str, Any]]:
181
+ """Get all registered tools, optionally filtered by category."""
182
+ if category:
183
+ return {k: v for k, v in _tool_registry.items() if v["category"] == category}
184
+ return _tool_registry.copy()
185
+
186
+
187
+ class BaseSecurityAgent(ABC):
188
+ """
189
+ Base class for all AI-powered security testing agents.
190
+
191
+ Provides:
192
+ - LiteLLM integration for multi-provider LLM support
193
+ - Tool execution framework
194
+ - Message history management
195
+ - Structured finding extraction
196
+ - Async execution support
197
+
198
+ Subclasses must implement:
199
+ - get_system_prompt(): Return the agent's system prompt
200
+ - get_tools(): Return list of tools available to the agent
201
+ """
202
+
203
+ def __init__(self, config: Optional[AgentConfig] = None):
204
+ self.config = config or AgentConfig()
205
+ self.messages: List[Dict[str, str]] = []
206
+ self.findings: List[Finding] = []
207
+ self.step_count = 0
208
+ self.tokens_used = 0
209
+ self._start_time = 0.0
210
+ self._stop_requested = False
211
+
212
+ @abstractmethod
213
+ def get_system_prompt(self) -> str:
214
+ """Return the system prompt for this agent."""
215
+ pass
216
+
217
+ @abstractmethod
218
+ def get_tools(self) -> List[Dict[str, Any]]:
219
+ """Return the list of tools available to this agent."""
220
+ pass
221
+
222
+ def get_tool_definitions(self) -> List[Dict[str, Any]]:
223
+ """Convert tools to LLM-compatible format (OpenAI function calling schema)."""
224
+ tools = self.get_tools()
225
+ definitions = []
226
+
227
+ for tool in tools:
228
+ definitions.append({
229
+ "type": "function",
230
+ "function": {
231
+ "name": tool["name"],
232
+ "description": tool["description"],
233
+ "parameters": {
234
+ "type": "object",
235
+ "properties": tool.get("parameters", {}),
236
+ "required": tool.get("required", []),
237
+ }
238
+ }
239
+ })
240
+
241
+ return definitions
242
+
243
+ async def _call_llm(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
244
+ """Call the LLM via LiteLLM."""
245
+ import litellm
246
+
247
+ try:
248
+ response = await litellm.acompletion(
249
+ model=self.config.model,
250
+ messages=messages,
251
+ tools=self.get_tool_definitions() if self.get_tools() else None,
252
+ temperature=self.config.temperature,
253
+ max_tokens=self.config.max_tokens,
254
+ )
255
+
256
+ # Track token usage
257
+ if hasattr(response, 'usage') and response.usage:
258
+ self.tokens_used += response.usage.total_tokens
259
+
260
+ return response
261
+
262
+ except Exception as e:
263
+ logger.error("LLM call failed", error=str(e), model=self.config.model)
264
+ raise
265
+
266
+ async def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> str:
267
+ """Execute a tool and return the result."""
268
+ tool = get_tool(tool_name)
269
+
270
+ if not tool:
271
+ return f"Error: Unknown tool '{tool_name}'"
272
+
273
+ try:
274
+ func = tool["function"]
275
+ if asyncio.iscoroutinefunction(func):
276
+ result = await func(**arguments)
277
+ else:
278
+ result = func(**arguments)
279
+ return str(result)
280
+ except Exception as e:
281
+ logger.error("Tool execution failed", tool=tool_name, error=str(e))
282
+ return f"Error executing {tool_name}: {str(e)}"
283
+
284
+ def _extract_findings_from_response(self, content: str) -> List[Finding]:
285
+ """Extract structured findings from LLM response."""
286
+ findings = []
287
+
288
+ # Look for XML-style finding blocks
289
+ finding_pattern = r'<finding>(.*?)</finding>'
290
+ matches = re.findall(finding_pattern, content, re.DOTALL)
291
+
292
+ for match in matches:
293
+ try:
294
+ finding = self._parse_finding_xml(match)
295
+ if finding:
296
+ findings.append(finding)
297
+ except Exception as e:
298
+ logger.warning("Failed to parse finding", error=str(e))
299
+
300
+ # Also look for JSON-style findings
301
+ json_pattern = r'```json\s*(\{[^`]*"severity"[^`]*\})\s*```'
302
+ json_matches = re.findall(json_pattern, content, re.DOTALL)
303
+
304
+ for match in json_matches:
305
+ try:
306
+ data = json.loads(match)
307
+ if "title" in data and "severity" in data:
308
+ finding = Finding(
309
+ title=data.get("title", "Unknown"),
310
+ severity=Severity(data.get("severity", "info").lower()),
311
+ category=VulnCategory(data.get("category", VulnCategory.SECURITY_MISCONFIG.value)),
312
+ description=data.get("description", ""),
313
+ evidence=data.get("evidence", ""),
314
+ location=data.get("location", "Unknown"),
315
+ line_number=data.get("line_number"),
316
+ remediation=data.get("remediation", ""),
317
+ cwe_id=data.get("cwe_id"),
318
+ )
319
+ findings.append(finding)
320
+ except (json.JSONDecodeError, ValueError) as e:
321
+ logger.warning("Failed to parse JSON finding", error=str(e))
322
+
323
+ return findings
324
+
325
+ def _parse_finding_xml(self, xml_content: str) -> Optional[Finding]:
326
+ """Parse an XML-formatted finding."""
327
+ def extract_tag(tag: str, content: str) -> str:
328
+ pattern = f'<{tag}>(.*?)</{tag}>'
329
+ match = re.search(pattern, content, re.DOTALL)
330
+ return match.group(1).strip() if match else ""
331
+
332
+ title = extract_tag("title", xml_content)
333
+ if not title:
334
+ return None
335
+
336
+ severity_str = extract_tag("severity", xml_content).lower()
337
+ try:
338
+ severity = Severity(severity_str)
339
+ except ValueError:
340
+ severity = Severity.INFO
341
+
342
+ return Finding(
343
+ title=title,
344
+ severity=severity,
345
+ category=VulnCategory.SECURITY_MISCONFIG, # Default, agent should specify
346
+ description=extract_tag("description", xml_content),
347
+ evidence=extract_tag("evidence", xml_content),
348
+ location=extract_tag("location", xml_content) or "Unknown",
349
+ remediation=extract_tag("remediation", xml_content),
350
+ cwe_id=extract_tag("cwe", xml_content) or None,
351
+ )
352
+
353
+ def stop(self):
354
+ """Request the agent to stop execution."""
355
+ self._stop_requested = True
356
+
357
+ async def run(self, initial_message: Optional[str] = None) -> AgentResult:
358
+ """
359
+ Execute the agent's security testing workflow.
360
+
361
+ Args:
362
+ initial_message: Optional initial user message to start the conversation
363
+
364
+ Returns:
365
+ AgentResult with findings and execution metadata
366
+ """
367
+ self._start_time = time.time()
368
+ self._stop_requested = False
369
+ self.messages = []
370
+ self.findings = []
371
+ self.step_count = 0
372
+ self.tokens_used = 0
373
+ errors = []
374
+
375
+ # Initialize with system prompt
376
+ self.messages.append({
377
+ "role": "system",
378
+ "content": self.get_system_prompt()
379
+ })
380
+
381
+ # Add initial user message if provided
382
+ if initial_message:
383
+ self.messages.append({
384
+ "role": "user",
385
+ "content": initial_message
386
+ })
387
+
388
+ try:
389
+ while self.step_count < self.config.max_steps and not self._stop_requested:
390
+ # Check timeout
391
+ elapsed = time.time() - self._start_time
392
+ if elapsed > self.config.timeout:
393
+ logger.warning("Agent timeout reached", timeout=self.config.timeout)
394
+ errors.append(f"Timeout after {elapsed:.1f}s")
395
+ break
396
+
397
+ self.step_count += 1
398
+
399
+ # Call LLM
400
+ response = await self._call_llm(self.messages)
401
+ message = response.choices[0].message
402
+
403
+ # Extract text content
404
+ content = message.content or ""
405
+
406
+ # Check for findings in response
407
+ new_findings = self._extract_findings_from_response(content)
408
+ self.findings.extend(new_findings)
409
+
410
+ # Add assistant response to history
411
+ self.messages.append({
412
+ "role": "assistant",
413
+ "content": content,
414
+ })
415
+
416
+ # Check for tool calls
417
+ tool_calls = getattr(message, 'tool_calls', None)
418
+
419
+ if not tool_calls:
420
+ # No tool calls - check if agent is done
421
+ if self._is_completion_message(content):
422
+ logger.info("Agent completed", steps=self.step_count)
423
+ break
424
+ # Continue with next iteration if needed
425
+ continue
426
+
427
+ # Execute tool calls
428
+ for tool_call in tool_calls:
429
+ tool_name = tool_call.function.name
430
+ try:
431
+ arguments = json.loads(tool_call.function.arguments)
432
+ except json.JSONDecodeError:
433
+ arguments = {}
434
+
435
+ if self.config.verbose:
436
+ logger.info("Executing tool", tool=tool_name, args=arguments)
437
+
438
+ result = await self._execute_tool(tool_name, arguments)
439
+
440
+ # Add tool result to messages
441
+ self.messages.append({
442
+ "role": "tool",
443
+ "tool_call_id": tool_call.id,
444
+ "content": result,
445
+ })
446
+
447
+ except Exception as e:
448
+ logger.error("Agent execution failed", error=str(e))
449
+ errors.append(str(e))
450
+
451
+ execution_time = time.time() - self._start_time
452
+
453
+ return AgentResult(
454
+ success=len(errors) == 0,
455
+ findings=self.findings,
456
+ errors=errors,
457
+ execution_time=execution_time,
458
+ total_steps=self.step_count,
459
+ tokens_used=self.tokens_used,
460
+ model_used=self.config.model,
461
+ raw_transcript=self.messages if self.config.save_transcript else [],
462
+ )
463
+
464
+ def _is_completion_message(self, content: str) -> bool:
465
+ """Check if the message indicates the agent has completed its task."""
466
+ completion_indicators = [
467
+ "testing complete",
468
+ "analysis complete",
469
+ "scan complete",
470
+ "review complete",
471
+ "no more vulnerabilities",
472
+ "all tests completed",
473
+ "finished testing",
474
+ "security assessment complete",
475
+ ]
476
+ content_lower = content.lower()
477
+ return any(indicator in content_lower for indicator in completion_indicators)