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.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +2933 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +341 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interactive_shell.py +559 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/local_tool_installer.py +1467 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2427 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +53 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/runtime/vps.py +830 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- 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)
|