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
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