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
aipt_v2/core/llm.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT LLM Provider - Multi-provider LLM abstraction
|
|
3
|
+
|
|
4
|
+
Supports: OpenAI, Anthropic, Ollama (local)
|
|
5
|
+
Inspired by: Strix's multi-provider approach
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Optional, Generator, Any
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class LLMResponse:
|
|
17
|
+
"""Standardized LLM response"""
|
|
18
|
+
content: str
|
|
19
|
+
model: str
|
|
20
|
+
tokens_used: int
|
|
21
|
+
finish_reason: str
|
|
22
|
+
raw_response: Any = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LLMProvider(ABC):
|
|
26
|
+
"""Abstract base class for LLM providers"""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def invoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
30
|
+
"""Synchronous invocation"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def ainvoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
35
|
+
"""Async invocation"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def stream(self, messages: list[dict], **kwargs) -> Generator[str, None, None]:
|
|
40
|
+
"""Streaming invocation"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def count_tokens(self, text: str) -> int:
|
|
45
|
+
"""Count tokens in text"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def format_messages(self, system: str, user: str, history: list[dict] = None) -> list[dict]:
|
|
49
|
+
"""Format messages for the provider"""
|
|
50
|
+
messages = [{"role": "system", "content": system}]
|
|
51
|
+
if history:
|
|
52
|
+
messages.extend(history)
|
|
53
|
+
messages.append({"role": "user", "content": user})
|
|
54
|
+
return messages
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OpenAIProvider(LLMProvider):
|
|
58
|
+
"""OpenAI GPT provider (GPT-4o, GPT-4, etc.)"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
model: str = "gpt-4o",
|
|
63
|
+
api_key: Optional[str] = None,
|
|
64
|
+
temperature: float = 0.7,
|
|
65
|
+
max_tokens: int = 4096,
|
|
66
|
+
base_url: Optional[str] = None,
|
|
67
|
+
):
|
|
68
|
+
try:
|
|
69
|
+
from openai import OpenAI, AsyncOpenAI
|
|
70
|
+
self._openai_available = True
|
|
71
|
+
except ImportError:
|
|
72
|
+
self._openai_available = False
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
self.model = model
|
|
76
|
+
self.temperature = temperature
|
|
77
|
+
self.max_tokens = max_tokens
|
|
78
|
+
|
|
79
|
+
kwargs = {"api_key": api_key or os.getenv("OPENAI_API_KEY")}
|
|
80
|
+
if base_url:
|
|
81
|
+
kwargs["base_url"] = base_url
|
|
82
|
+
|
|
83
|
+
self.client = OpenAI(**kwargs)
|
|
84
|
+
self.async_client = AsyncOpenAI(**kwargs)
|
|
85
|
+
|
|
86
|
+
def invoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
87
|
+
"""Invoke OpenAI API"""
|
|
88
|
+
if not self._openai_available:
|
|
89
|
+
raise RuntimeError("OpenAI package not installed. Run: pip install openai")
|
|
90
|
+
|
|
91
|
+
response = self.client.chat.completions.create(
|
|
92
|
+
model=kwargs.get("model", self.model),
|
|
93
|
+
messages=messages,
|
|
94
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
95
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return LLMResponse(
|
|
99
|
+
content=response.choices[0].message.content or "",
|
|
100
|
+
model=self.model,
|
|
101
|
+
tokens_used=response.usage.total_tokens if response.usage else 0,
|
|
102
|
+
finish_reason=response.choices[0].finish_reason or "stop",
|
|
103
|
+
raw_response=response,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def ainvoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
107
|
+
"""Async invoke OpenAI API"""
|
|
108
|
+
if not self._openai_available:
|
|
109
|
+
raise RuntimeError("OpenAI package not installed. Run: pip install openai")
|
|
110
|
+
|
|
111
|
+
response = await self.async_client.chat.completions.create(
|
|
112
|
+
model=kwargs.get("model", self.model),
|
|
113
|
+
messages=messages,
|
|
114
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
115
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return LLMResponse(
|
|
119
|
+
content=response.choices[0].message.content or "",
|
|
120
|
+
model=self.model,
|
|
121
|
+
tokens_used=response.usage.total_tokens if response.usage else 0,
|
|
122
|
+
finish_reason=response.choices[0].finish_reason or "stop",
|
|
123
|
+
raw_response=response,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def stream(self, messages: list[dict], **kwargs) -> Generator[str, None, None]:
|
|
127
|
+
"""Stream OpenAI response"""
|
|
128
|
+
if not self._openai_available:
|
|
129
|
+
raise RuntimeError("OpenAI package not installed")
|
|
130
|
+
|
|
131
|
+
stream = self.client.chat.completions.create(
|
|
132
|
+
model=kwargs.get("model", self.model),
|
|
133
|
+
messages=messages,
|
|
134
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
135
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
136
|
+
stream=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
for chunk in stream:
|
|
140
|
+
if chunk.choices[0].delta.content:
|
|
141
|
+
yield chunk.choices[0].delta.content
|
|
142
|
+
|
|
143
|
+
def count_tokens(self, text: str) -> int:
|
|
144
|
+
"""Approximate token count (4 chars per token)"""
|
|
145
|
+
try:
|
|
146
|
+
import tiktoken
|
|
147
|
+
enc = tiktoken.encoding_for_model(self.model)
|
|
148
|
+
return len(enc.encode(text))
|
|
149
|
+
except ImportError:
|
|
150
|
+
return len(text) // 4
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class AnthropicProvider(LLMProvider):
|
|
154
|
+
"""Anthropic Claude provider (Claude 3.5, Claude 4, etc.)"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
model: str = "claude-sonnet-4-20250514",
|
|
159
|
+
api_key: Optional[str] = None,
|
|
160
|
+
temperature: float = 0.7,
|
|
161
|
+
max_tokens: int = 4096,
|
|
162
|
+
):
|
|
163
|
+
try:
|
|
164
|
+
from anthropic import Anthropic, AsyncAnthropic
|
|
165
|
+
self._anthropic_available = True
|
|
166
|
+
except ImportError:
|
|
167
|
+
self._anthropic_available = False
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self.model = model
|
|
171
|
+
self.temperature = temperature
|
|
172
|
+
self.max_tokens = max_tokens
|
|
173
|
+
|
|
174
|
+
api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
175
|
+
self.client = Anthropic(api_key=api_key)
|
|
176
|
+
self.async_client = AsyncAnthropic(api_key=api_key)
|
|
177
|
+
|
|
178
|
+
def _extract_messages(self, messages: list[dict]) -> tuple[str, list[dict]]:
|
|
179
|
+
"""Extract system message and chat messages"""
|
|
180
|
+
system = None
|
|
181
|
+
chat_messages = []
|
|
182
|
+
for msg in messages:
|
|
183
|
+
if msg["role"] == "system":
|
|
184
|
+
system = msg["content"]
|
|
185
|
+
else:
|
|
186
|
+
chat_messages.append(msg)
|
|
187
|
+
return system or "You are an expert penetration testing AI assistant.", chat_messages
|
|
188
|
+
|
|
189
|
+
def invoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
190
|
+
"""Invoke Anthropic API"""
|
|
191
|
+
if not self._anthropic_available:
|
|
192
|
+
raise RuntimeError("Anthropic package not installed. Run: pip install anthropic")
|
|
193
|
+
|
|
194
|
+
system, chat_messages = self._extract_messages(messages)
|
|
195
|
+
|
|
196
|
+
response = self.client.messages.create(
|
|
197
|
+
model=kwargs.get("model", self.model),
|
|
198
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
199
|
+
system=system,
|
|
200
|
+
messages=chat_messages,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
content = ""
|
|
204
|
+
for block in response.content:
|
|
205
|
+
if hasattr(block, "text"):
|
|
206
|
+
content += block.text
|
|
207
|
+
|
|
208
|
+
return LLMResponse(
|
|
209
|
+
content=content,
|
|
210
|
+
model=self.model,
|
|
211
|
+
tokens_used=response.usage.input_tokens + response.usage.output_tokens,
|
|
212
|
+
finish_reason=response.stop_reason or "end_turn",
|
|
213
|
+
raw_response=response,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
async def ainvoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
217
|
+
"""Async invoke Anthropic API"""
|
|
218
|
+
if not self._anthropic_available:
|
|
219
|
+
raise RuntimeError("Anthropic package not installed")
|
|
220
|
+
|
|
221
|
+
system, chat_messages = self._extract_messages(messages)
|
|
222
|
+
|
|
223
|
+
response = await self.async_client.messages.create(
|
|
224
|
+
model=kwargs.get("model", self.model),
|
|
225
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
226
|
+
system=system,
|
|
227
|
+
messages=chat_messages,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
content = ""
|
|
231
|
+
for block in response.content:
|
|
232
|
+
if hasattr(block, "text"):
|
|
233
|
+
content += block.text
|
|
234
|
+
|
|
235
|
+
return LLMResponse(
|
|
236
|
+
content=content,
|
|
237
|
+
model=self.model,
|
|
238
|
+
tokens_used=response.usage.input_tokens + response.usage.output_tokens,
|
|
239
|
+
finish_reason=response.stop_reason or "end_turn",
|
|
240
|
+
raw_response=response,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def stream(self, messages: list[dict], **kwargs) -> Generator[str, None, None]:
|
|
244
|
+
"""Stream Anthropic response"""
|
|
245
|
+
if not self._anthropic_available:
|
|
246
|
+
raise RuntimeError("Anthropic package not installed")
|
|
247
|
+
|
|
248
|
+
system, chat_messages = self._extract_messages(messages)
|
|
249
|
+
|
|
250
|
+
with self.client.messages.stream(
|
|
251
|
+
model=kwargs.get("model", self.model),
|
|
252
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
253
|
+
system=system,
|
|
254
|
+
messages=chat_messages,
|
|
255
|
+
) as stream:
|
|
256
|
+
for text in stream.text_stream:
|
|
257
|
+
yield text
|
|
258
|
+
|
|
259
|
+
def count_tokens(self, text: str) -> int:
|
|
260
|
+
"""Approximate token count"""
|
|
261
|
+
return len(text) // 4
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class OllamaProvider(LLMProvider):
|
|
265
|
+
"""Ollama local LLM provider (llama3, mistral, etc.)"""
|
|
266
|
+
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
model: str = "llama3:70b",
|
|
270
|
+
base_url: str = "http://localhost:11434",
|
|
271
|
+
temperature: float = 0.7,
|
|
272
|
+
):
|
|
273
|
+
self.model = model
|
|
274
|
+
self.base_url = base_url.rstrip("/")
|
|
275
|
+
self.temperature = temperature
|
|
276
|
+
|
|
277
|
+
def invoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
278
|
+
"""Invoke Ollama API"""
|
|
279
|
+
import httpx
|
|
280
|
+
|
|
281
|
+
with httpx.Client(timeout=300.0) as client:
|
|
282
|
+
response = client.post(
|
|
283
|
+
f"{self.base_url}/api/chat",
|
|
284
|
+
json={
|
|
285
|
+
"model": kwargs.get("model", self.model),
|
|
286
|
+
"messages": messages,
|
|
287
|
+
"stream": False,
|
|
288
|
+
"options": {
|
|
289
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
)
|
|
293
|
+
response.raise_for_status()
|
|
294
|
+
data = response.json()
|
|
295
|
+
|
|
296
|
+
return LLMResponse(
|
|
297
|
+
content=data.get("message", {}).get("content", ""),
|
|
298
|
+
model=self.model,
|
|
299
|
+
tokens_used=data.get("eval_count", 0) + data.get("prompt_eval_count", 0),
|
|
300
|
+
finish_reason="stop",
|
|
301
|
+
raw_response=data,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
async def ainvoke(self, messages: list[dict], **kwargs) -> LLMResponse:
|
|
305
|
+
"""Async invoke Ollama API"""
|
|
306
|
+
import httpx
|
|
307
|
+
|
|
308
|
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
309
|
+
response = await client.post(
|
|
310
|
+
f"{self.base_url}/api/chat",
|
|
311
|
+
json={
|
|
312
|
+
"model": kwargs.get("model", self.model),
|
|
313
|
+
"messages": messages,
|
|
314
|
+
"stream": False,
|
|
315
|
+
"options": {
|
|
316
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
)
|
|
320
|
+
response.raise_for_status()
|
|
321
|
+
data = response.json()
|
|
322
|
+
|
|
323
|
+
return LLMResponse(
|
|
324
|
+
content=data.get("message", {}).get("content", ""),
|
|
325
|
+
model=self.model,
|
|
326
|
+
tokens_used=data.get("eval_count", 0) + data.get("prompt_eval_count", 0),
|
|
327
|
+
finish_reason="stop",
|
|
328
|
+
raw_response=data,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def stream(self, messages: list[dict], **kwargs) -> Generator[str, None, None]:
|
|
332
|
+
"""Stream Ollama response"""
|
|
333
|
+
import httpx
|
|
334
|
+
import json
|
|
335
|
+
|
|
336
|
+
with httpx.stream(
|
|
337
|
+
"POST",
|
|
338
|
+
f"{self.base_url}/api/chat",
|
|
339
|
+
json={
|
|
340
|
+
"model": kwargs.get("model", self.model),
|
|
341
|
+
"messages": messages,
|
|
342
|
+
"stream": True,
|
|
343
|
+
"options": {
|
|
344
|
+
"temperature": kwargs.get("temperature", self.temperature),
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
timeout=300.0,
|
|
348
|
+
) as response:
|
|
349
|
+
for line in response.iter_lines():
|
|
350
|
+
if line:
|
|
351
|
+
data = json.loads(line)
|
|
352
|
+
if "message" in data and "content" in data["message"]:
|
|
353
|
+
yield data["message"]["content"]
|
|
354
|
+
|
|
355
|
+
def count_tokens(self, text: str) -> int:
|
|
356
|
+
"""Approximate token count"""
|
|
357
|
+
return len(text) // 4
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def get_llm(
|
|
361
|
+
provider: str = "openai",
|
|
362
|
+
model: Optional[str] = None,
|
|
363
|
+
**kwargs
|
|
364
|
+
) -> LLMProvider:
|
|
365
|
+
"""
|
|
366
|
+
Factory function to get LLM provider.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
provider: One of "openai", "anthropic", "ollama"
|
|
370
|
+
model: Model name (uses default if not specified)
|
|
371
|
+
**kwargs: Additional provider-specific arguments
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
LLMProvider instance
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
llm = get_llm("openai", model="gpt-4o")
|
|
378
|
+
llm = get_llm("anthropic", model="claude-sonnet-4-20250514")
|
|
379
|
+
llm = get_llm("ollama", model="llama3:70b")
|
|
380
|
+
"""
|
|
381
|
+
providers = {
|
|
382
|
+
"openai": OpenAIProvider,
|
|
383
|
+
"anthropic": AnthropicProvider,
|
|
384
|
+
"ollama": OllamaProvider,
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if provider not in providers:
|
|
388
|
+
raise ValueError(f"Unknown provider: {provider}. Choose from: {list(providers.keys())}")
|
|
389
|
+
|
|
390
|
+
provider_class = providers[provider]
|
|
391
|
+
|
|
392
|
+
if model:
|
|
393
|
+
kwargs["model"] = model
|
|
394
|
+
|
|
395
|
+
return provider_class(**kwargs)
|
aipt_v2/core/memory.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Memory Manager - Context compression and management
|
|
3
|
+
|
|
4
|
+
Prevents context overflow in long pentest sessions.
|
|
5
|
+
Inspired by: Strix's memory compression (100K limit, 15 recent, 90% compress)
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MemoryConfig:
|
|
17
|
+
"""Memory configuration"""
|
|
18
|
+
max_tokens: int = 32000 # Context window limit
|
|
19
|
+
compress_at: float = 0.8 # Compress when 80% full
|
|
20
|
+
recent_keep: int = 10 # Always keep last N messages
|
|
21
|
+
summary_max_tokens: int = 500 # Max tokens for summary
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemoryManager:
|
|
25
|
+
"""
|
|
26
|
+
Manages conversation context with automatic compression.
|
|
27
|
+
|
|
28
|
+
Key features:
|
|
29
|
+
- Tracks token usage
|
|
30
|
+
- Compresses old messages when approaching limit
|
|
31
|
+
- Preserves recent messages for context continuity
|
|
32
|
+
- Maintains compressed summary of older interactions
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
llm: Any = None,
|
|
38
|
+
config: Optional[MemoryConfig] = None,
|
|
39
|
+
):
|
|
40
|
+
self.llm = llm
|
|
41
|
+
self.config = config or MemoryConfig()
|
|
42
|
+
self.messages: list[dict] = []
|
|
43
|
+
self.compressed_summary: str = ""
|
|
44
|
+
self._total_tokens: int = 0
|
|
45
|
+
self._compression_count: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def total_tokens(self) -> int:
|
|
49
|
+
"""Current token count"""
|
|
50
|
+
return self._total_tokens
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def compression_count(self) -> int:
|
|
54
|
+
"""Number of compressions performed"""
|
|
55
|
+
return self._compression_count
|
|
56
|
+
|
|
57
|
+
def add_system(self, content: str) -> None:
|
|
58
|
+
"""Add system message (always kept at position 0)"""
|
|
59
|
+
# Remove existing system message if any
|
|
60
|
+
self.messages = [m for m in self.messages if m["role"] != "system"]
|
|
61
|
+
self.messages.insert(0, {"role": "system", "content": content})
|
|
62
|
+
self._recalculate_tokens()
|
|
63
|
+
|
|
64
|
+
def add_user(self, content: str) -> None:
|
|
65
|
+
"""Add user message"""
|
|
66
|
+
self._add_message("user", content)
|
|
67
|
+
|
|
68
|
+
def add_assistant(self, content: str) -> None:
|
|
69
|
+
"""Add assistant message"""
|
|
70
|
+
self._add_message("assistant", content)
|
|
71
|
+
|
|
72
|
+
def add_tool_result(self, tool_name: str, result: str, truncate: int = 5000) -> None:
|
|
73
|
+
"""Add tool execution result"""
|
|
74
|
+
if len(result) > truncate:
|
|
75
|
+
result = result[:truncate] + f"\n... [truncated at {truncate} chars]"
|
|
76
|
+
content = f"[Tool: {tool_name}]\n{result}"
|
|
77
|
+
self._add_message("user", content)
|
|
78
|
+
|
|
79
|
+
def add_finding(self, finding_type: str, description: str, severity: str = "info") -> None:
|
|
80
|
+
"""Add a security finding to memory"""
|
|
81
|
+
content = f"[Finding: {finding_type.upper()}] [{severity.upper()}] {description}"
|
|
82
|
+
self._add_message("user", content)
|
|
83
|
+
|
|
84
|
+
def _add_message(self, role: str, content: str) -> None:
|
|
85
|
+
"""Add message and check for compression"""
|
|
86
|
+
self.messages.append({
|
|
87
|
+
"role": role,
|
|
88
|
+
"content": content,
|
|
89
|
+
"timestamp": datetime.now().isoformat(),
|
|
90
|
+
})
|
|
91
|
+
self._total_tokens += self._count_tokens(content)
|
|
92
|
+
|
|
93
|
+
# Check if compression needed
|
|
94
|
+
if self._total_tokens > self.config.max_tokens * self.config.compress_at:
|
|
95
|
+
self._compress()
|
|
96
|
+
|
|
97
|
+
def _compress(self) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Compress older messages into a summary.
|
|
100
|
+
|
|
101
|
+
Strategy:
|
|
102
|
+
1. Keep system message (first)
|
|
103
|
+
2. Keep last N messages (recent_keep)
|
|
104
|
+
3. Summarize everything in between
|
|
105
|
+
"""
|
|
106
|
+
if len(self.messages) <= self.config.recent_keep + 1:
|
|
107
|
+
return # Not enough to compress
|
|
108
|
+
|
|
109
|
+
# Separate messages
|
|
110
|
+
system_msg = None
|
|
111
|
+
if self.messages and self.messages[0]["role"] == "system":
|
|
112
|
+
system_msg = self.messages[0]
|
|
113
|
+
other_messages = self.messages[1:]
|
|
114
|
+
else:
|
|
115
|
+
other_messages = self.messages
|
|
116
|
+
|
|
117
|
+
# Split into old (to compress) and recent (to keep)
|
|
118
|
+
split_point = len(other_messages) - self.config.recent_keep
|
|
119
|
+
old_messages = other_messages[:split_point]
|
|
120
|
+
recent_messages = other_messages[split_point:]
|
|
121
|
+
|
|
122
|
+
if not old_messages:
|
|
123
|
+
return # Nothing to compress
|
|
124
|
+
|
|
125
|
+
# Generate summary of old messages
|
|
126
|
+
summary = self._summarize(old_messages)
|
|
127
|
+
self._compression_count += 1
|
|
128
|
+
|
|
129
|
+
# Rebuild messages list
|
|
130
|
+
self.messages = []
|
|
131
|
+
if system_msg:
|
|
132
|
+
self.messages.append(system_msg)
|
|
133
|
+
|
|
134
|
+
# Add compressed summary as context
|
|
135
|
+
if summary or self.compressed_summary:
|
|
136
|
+
combined_summary = f"{self.compressed_summary}\n\n{summary}".strip()
|
|
137
|
+
self.compressed_summary = combined_summary[-4000:] # Keep last 4000 chars
|
|
138
|
+
self.messages.append({
|
|
139
|
+
"role": "user",
|
|
140
|
+
"content": f"[Previous Session Summary]\n{self.compressed_summary}"
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
# Add recent messages
|
|
144
|
+
self.messages.extend(recent_messages)
|
|
145
|
+
|
|
146
|
+
# Recalculate tokens
|
|
147
|
+
self._recalculate_tokens()
|
|
148
|
+
|
|
149
|
+
def _summarize(self, messages: list[dict]) -> str:
|
|
150
|
+
"""Generate concise summary of messages"""
|
|
151
|
+
if not messages:
|
|
152
|
+
return ""
|
|
153
|
+
|
|
154
|
+
# If no LLM available, use simple extraction
|
|
155
|
+
if not self.llm:
|
|
156
|
+
return self._simple_summarize(messages)
|
|
157
|
+
|
|
158
|
+
# Format messages for summarization
|
|
159
|
+
formatted = "\n".join([
|
|
160
|
+
f"{m['role'].upper()}: {m['content'][:500]}"
|
|
161
|
+
for m in messages
|
|
162
|
+
])
|
|
163
|
+
|
|
164
|
+
summary_prompt = [
|
|
165
|
+
{
|
|
166
|
+
"role": "system",
|
|
167
|
+
"content": "Summarize the following pentest session concisely. "
|
|
168
|
+
"Focus on: discovered hosts, open ports, vulnerabilities found, "
|
|
169
|
+
"credentials obtained, and actions taken. Be brief but complete."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"role": "user",
|
|
173
|
+
"content": f"Summarize this session:\n\n{formatted[:8000]}"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
response = self.llm.invoke(summary_prompt, max_tokens=self.config.summary_max_tokens)
|
|
179
|
+
return response.content
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return self._simple_summarize(messages) + f" [LLM summarization failed: {e}]"
|
|
182
|
+
|
|
183
|
+
def _simple_summarize(self, messages: list[dict]) -> str:
|
|
184
|
+
"""Simple extraction-based summarization without LLM"""
|
|
185
|
+
findings = []
|
|
186
|
+
actions = []
|
|
187
|
+
|
|
188
|
+
for msg in messages:
|
|
189
|
+
content = msg.get("content", "")
|
|
190
|
+
|
|
191
|
+
# Extract findings
|
|
192
|
+
if "[Finding:" in content or "vuln" in content.lower():
|
|
193
|
+
findings.append(content[:200])
|
|
194
|
+
|
|
195
|
+
# Extract tool executions
|
|
196
|
+
if "[Tool:" in content:
|
|
197
|
+
tool_line = content.split("\n")[0]
|
|
198
|
+
actions.append(tool_line)
|
|
199
|
+
|
|
200
|
+
summary_parts = []
|
|
201
|
+
if findings:
|
|
202
|
+
summary_parts.append(f"Findings ({len(findings)}): " + "; ".join(findings[:3]))
|
|
203
|
+
if actions:
|
|
204
|
+
summary_parts.append(f"Actions ({len(actions)}): " + "; ".join(actions[:5]))
|
|
205
|
+
|
|
206
|
+
return " | ".join(summary_parts) if summary_parts else f"[{len(messages)} messages compressed]"
|
|
207
|
+
|
|
208
|
+
def _count_tokens(self, text: str) -> int:
|
|
209
|
+
"""Count tokens in text"""
|
|
210
|
+
if self.llm and hasattr(self.llm, 'count_tokens'):
|
|
211
|
+
return self.llm.count_tokens(text)
|
|
212
|
+
return len(text) // 4 # Approximate
|
|
213
|
+
|
|
214
|
+
def _recalculate_tokens(self) -> None:
|
|
215
|
+
"""Recalculate total tokens"""
|
|
216
|
+
self._total_tokens = sum(
|
|
217
|
+
self._count_tokens(m.get("content", "")) for m in self.messages
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def get_messages(self, include_timestamps: bool = False) -> list[dict]:
|
|
221
|
+
"""Get current message list for LLM"""
|
|
222
|
+
if include_timestamps:
|
|
223
|
+
return self.messages.copy()
|
|
224
|
+
|
|
225
|
+
# Strip timestamps for LLM calls
|
|
226
|
+
return [
|
|
227
|
+
{"role": m["role"], "content": m["content"]}
|
|
228
|
+
for m in self.messages
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
def get_context_for_prompt(self) -> str:
|
|
232
|
+
"""Get formatted context string"""
|
|
233
|
+
parts = []
|
|
234
|
+
|
|
235
|
+
if self.compressed_summary:
|
|
236
|
+
parts.append(f"## Previous Findings\n{self.compressed_summary}")
|
|
237
|
+
|
|
238
|
+
# Add recent non-system messages
|
|
239
|
+
for msg in self.messages:
|
|
240
|
+
if msg["role"] == "system":
|
|
241
|
+
continue
|
|
242
|
+
role = "Assistant" if msg["role"] == "assistant" else "User/Tool"
|
|
243
|
+
content = msg["content"][:1000]
|
|
244
|
+
parts.append(f"**{role}**: {content}")
|
|
245
|
+
|
|
246
|
+
return "\n\n".join(parts)
|
|
247
|
+
|
|
248
|
+
def get_findings_summary(self) -> dict:
|
|
249
|
+
"""Extract summary of findings from memory"""
|
|
250
|
+
findings = {"hosts": [], "ports": [], "vulns": [], "creds": []}
|
|
251
|
+
|
|
252
|
+
for msg in self.messages:
|
|
253
|
+
content = msg.get("content", "").lower()
|
|
254
|
+
|
|
255
|
+
if "host" in content and "discovered" in content:
|
|
256
|
+
findings["hosts"].append(content[:100])
|
|
257
|
+
if "port" in content and "open" in content:
|
|
258
|
+
findings["ports"].append(content[:100])
|
|
259
|
+
if "vuln" in content or "cve-" in content:
|
|
260
|
+
findings["vulns"].append(content[:100])
|
|
261
|
+
if "credential" in content or "password" in content:
|
|
262
|
+
findings["creds"].append(content[:100])
|
|
263
|
+
|
|
264
|
+
return findings
|
|
265
|
+
|
|
266
|
+
def clear(self) -> None:
|
|
267
|
+
"""Clear all messages (keeps system message)"""
|
|
268
|
+
system_msg = None
|
|
269
|
+
if self.messages and self.messages[0]["role"] == "system":
|
|
270
|
+
system_msg = self.messages[0]
|
|
271
|
+
|
|
272
|
+
self.messages = []
|
|
273
|
+
if system_msg:
|
|
274
|
+
self.messages.append(system_msg)
|
|
275
|
+
|
|
276
|
+
self.compressed_summary = ""
|
|
277
|
+
self._recalculate_tokens()
|
|
278
|
+
|
|
279
|
+
def save_state(self) -> dict:
|
|
280
|
+
"""Export state for persistence"""
|
|
281
|
+
return {
|
|
282
|
+
"messages": self.messages,
|
|
283
|
+
"compressed_summary": self.compressed_summary,
|
|
284
|
+
"total_tokens": self._total_tokens,
|
|
285
|
+
"compression_count": self._compression_count,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def load_state(self, state: dict) -> None:
|
|
289
|
+
"""Import state from persistence"""
|
|
290
|
+
self.messages = state.get("messages", [])
|
|
291
|
+
self.compressed_summary = state.get("compressed_summary", "")
|
|
292
|
+
self._total_tokens = state.get("total_tokens", 0)
|
|
293
|
+
self._compression_count = state.get("compression_count", 0)
|
|
294
|
+
|
|
295
|
+
def to_json(self) -> str:
|
|
296
|
+
"""Export to JSON string"""
|
|
297
|
+
return json.dumps(self.save_state(), indent=2, default=str)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_json(cls, json_str: str, llm: Any = None) -> "MemoryManager":
|
|
301
|
+
"""Create from JSON string"""
|
|
302
|
+
state = json.loads(json_str)
|
|
303
|
+
manager = cls(llm=llm)
|
|
304
|
+
manager.load_state(state)
|
|
305
|
+
return manager
|