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,400 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Terminal Executor
|
|
3
|
+
|
|
4
|
+
Async command execution with streaming output, timeouts, and safety controls.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
import signal
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import AsyncIterator, Callable, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionStatus(Enum):
|
|
24
|
+
"""Command execution status"""
|
|
25
|
+
PENDING = "pending"
|
|
26
|
+
RUNNING = "running"
|
|
27
|
+
SUCCESS = "success"
|
|
28
|
+
FAILED = "failed"
|
|
29
|
+
TIMEOUT = "timeout"
|
|
30
|
+
KILLED = "killed"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ExecutionConfig:
|
|
35
|
+
"""Configuration for command execution"""
|
|
36
|
+
timeout: float = 300.0 # 5 minutes default
|
|
37
|
+
working_dir: Optional[str] = None
|
|
38
|
+
environment: dict[str, str] = field(default_factory=dict)
|
|
39
|
+
shell: bool = False # Run in shell (less secure, but needed for pipes)
|
|
40
|
+
capture_output: bool = True
|
|
41
|
+
stream_output: bool = False
|
|
42
|
+
max_output_size: int = 10 * 1024 * 1024 # 10MB
|
|
43
|
+
|
|
44
|
+
# Safety settings
|
|
45
|
+
allow_sudo: bool = False
|
|
46
|
+
blocked_commands: list[str] = field(default_factory=lambda: [
|
|
47
|
+
"rm -rf /",
|
|
48
|
+
"mkfs",
|
|
49
|
+
"dd if=/dev/zero",
|
|
50
|
+
":(){:|:&};:", # Fork bomb
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
# Resource limits
|
|
54
|
+
max_memory_mb: int = 1024
|
|
55
|
+
max_cpu_time: int = 300
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class CommandResult:
|
|
60
|
+
"""Result of a command execution"""
|
|
61
|
+
command: str
|
|
62
|
+
status: ExecutionStatus
|
|
63
|
+
exit_code: Optional[int] = None
|
|
64
|
+
stdout: str = ""
|
|
65
|
+
stderr: str = ""
|
|
66
|
+
start_time: Optional[datetime] = None
|
|
67
|
+
end_time: Optional[datetime] = None
|
|
68
|
+
duration_seconds: float = 0.0
|
|
69
|
+
|
|
70
|
+
# Metadata
|
|
71
|
+
working_dir: str = ""
|
|
72
|
+
pid: Optional[int] = None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def success(self) -> bool:
|
|
76
|
+
return self.status == ExecutionStatus.SUCCESS and self.exit_code == 0
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def output(self) -> str:
|
|
80
|
+
"""Combined stdout and stderr"""
|
|
81
|
+
parts = []
|
|
82
|
+
if self.stdout:
|
|
83
|
+
parts.append(self.stdout)
|
|
84
|
+
if self.stderr:
|
|
85
|
+
parts.append(f"[STDERR]\n{self.stderr}")
|
|
86
|
+
return "\n".join(parts)
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"command": self.command,
|
|
91
|
+
"status": self.status.value,
|
|
92
|
+
"exit_code": self.exit_code,
|
|
93
|
+
"stdout": self.stdout[:10000] if self.stdout else "",
|
|
94
|
+
"stderr": self.stderr[:5000] if self.stderr else "",
|
|
95
|
+
"duration_seconds": self.duration_seconds,
|
|
96
|
+
"success": self.success,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TerminalExecutor:
|
|
101
|
+
"""
|
|
102
|
+
Async terminal command executor with safety controls.
|
|
103
|
+
|
|
104
|
+
Features:
|
|
105
|
+
- Async execution with streaming output
|
|
106
|
+
- Timeout handling
|
|
107
|
+
- Command sanitization
|
|
108
|
+
- Output size limits
|
|
109
|
+
- Resource control
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
executor = TerminalExecutor()
|
|
113
|
+
result = await executor.run("nmap -sV target.com")
|
|
114
|
+
print(result.stdout)
|
|
115
|
+
|
|
116
|
+
# Stream output
|
|
117
|
+
async for line in executor.stream("nikto -h target.com"):
|
|
118
|
+
print(line)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, config: Optional[ExecutionConfig] = None):
|
|
122
|
+
self.config = config or ExecutionConfig()
|
|
123
|
+
self._processes: dict[int, asyncio.subprocess.Process] = {}
|
|
124
|
+
self._history: list[CommandResult] = []
|
|
125
|
+
|
|
126
|
+
async def run(
|
|
127
|
+
self,
|
|
128
|
+
command: str,
|
|
129
|
+
timeout: Optional[float] = None,
|
|
130
|
+
working_dir: Optional[str] = None,
|
|
131
|
+
env: Optional[dict[str, str]] = None,
|
|
132
|
+
) -> CommandResult:
|
|
133
|
+
"""
|
|
134
|
+
Execute a command and return the result.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
command: Command to execute
|
|
138
|
+
timeout: Override default timeout
|
|
139
|
+
working_dir: Working directory
|
|
140
|
+
env: Additional environment variables
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
CommandResult with output and status
|
|
144
|
+
"""
|
|
145
|
+
# Validate command
|
|
146
|
+
validation_error = self._validate_command(command)
|
|
147
|
+
if validation_error:
|
|
148
|
+
return CommandResult(
|
|
149
|
+
command=command,
|
|
150
|
+
status=ExecutionStatus.FAILED,
|
|
151
|
+
stderr=validation_error,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
timeout = timeout or self.config.timeout
|
|
155
|
+
cwd = working_dir or self.config.working_dir or os.getcwd()
|
|
156
|
+
|
|
157
|
+
# Build environment
|
|
158
|
+
environment = os.environ.copy()
|
|
159
|
+
environment.update(self.config.environment)
|
|
160
|
+
if env:
|
|
161
|
+
environment.update(env)
|
|
162
|
+
|
|
163
|
+
result = CommandResult(
|
|
164
|
+
command=command,
|
|
165
|
+
status=ExecutionStatus.RUNNING,
|
|
166
|
+
start_time=datetime.utcnow(),
|
|
167
|
+
working_dir=cwd,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Create process
|
|
172
|
+
if self.config.shell:
|
|
173
|
+
process = await asyncio.create_subprocess_shell(
|
|
174
|
+
command,
|
|
175
|
+
stdout=asyncio.subprocess.PIPE if self.config.capture_output else None,
|
|
176
|
+
stderr=asyncio.subprocess.PIPE if self.config.capture_output else None,
|
|
177
|
+
cwd=cwd,
|
|
178
|
+
env=environment,
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
args = shlex.split(command)
|
|
182
|
+
process = await asyncio.create_subprocess_exec(
|
|
183
|
+
*args,
|
|
184
|
+
stdout=asyncio.subprocess.PIPE if self.config.capture_output else None,
|
|
185
|
+
stderr=asyncio.subprocess.PIPE if self.config.capture_output else None,
|
|
186
|
+
cwd=cwd,
|
|
187
|
+
env=environment,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
result.pid = process.pid
|
|
191
|
+
self._processes[process.pid] = process
|
|
192
|
+
|
|
193
|
+
logger.info(f"Executing: {command[:100]}... (PID: {process.pid})")
|
|
194
|
+
|
|
195
|
+
# Wait for completion with timeout
|
|
196
|
+
try:
|
|
197
|
+
stdout, stderr = await asyncio.wait_for(
|
|
198
|
+
process.communicate(),
|
|
199
|
+
timeout=timeout,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
result.exit_code = process.returncode
|
|
203
|
+
result.status = ExecutionStatus.SUCCESS if process.returncode == 0 else ExecutionStatus.FAILED
|
|
204
|
+
|
|
205
|
+
if stdout:
|
|
206
|
+
result.stdout = self._limit_output(stdout.decode("utf-8", errors="replace"))
|
|
207
|
+
if stderr:
|
|
208
|
+
result.stderr = self._limit_output(stderr.decode("utf-8", errors="replace"))
|
|
209
|
+
|
|
210
|
+
except asyncio.TimeoutError:
|
|
211
|
+
logger.warning(f"Command timed out after {timeout}s: {command[:50]}")
|
|
212
|
+
process.kill()
|
|
213
|
+
await process.wait()
|
|
214
|
+
result.status = ExecutionStatus.TIMEOUT
|
|
215
|
+
result.stderr = f"Command timed out after {timeout} seconds"
|
|
216
|
+
|
|
217
|
+
except FileNotFoundError as e:
|
|
218
|
+
result.status = ExecutionStatus.FAILED
|
|
219
|
+
result.stderr = f"Command not found: {e}"
|
|
220
|
+
except PermissionError as e:
|
|
221
|
+
result.status = ExecutionStatus.FAILED
|
|
222
|
+
result.stderr = f"Permission denied: {e}"
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Execution error: {e}")
|
|
225
|
+
result.status = ExecutionStatus.FAILED
|
|
226
|
+
result.stderr = str(e)
|
|
227
|
+
finally:
|
|
228
|
+
result.end_time = datetime.utcnow()
|
|
229
|
+
if result.start_time:
|
|
230
|
+
result.duration_seconds = (result.end_time - result.start_time).total_seconds()
|
|
231
|
+
|
|
232
|
+
# Cleanup
|
|
233
|
+
if result.pid and result.pid in self._processes:
|
|
234
|
+
del self._processes[result.pid]
|
|
235
|
+
|
|
236
|
+
self._history.append(result)
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
async def stream(
|
|
240
|
+
self,
|
|
241
|
+
command: str,
|
|
242
|
+
timeout: Optional[float] = None,
|
|
243
|
+
callback: Optional[Callable[[str], None]] = None,
|
|
244
|
+
) -> AsyncIterator[str]:
|
|
245
|
+
"""
|
|
246
|
+
Execute command and stream output line by line.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
command: Command to execute
|
|
250
|
+
timeout: Timeout in seconds
|
|
251
|
+
callback: Optional callback for each line
|
|
252
|
+
|
|
253
|
+
Yields:
|
|
254
|
+
Output lines as they're produced
|
|
255
|
+
"""
|
|
256
|
+
validation_error = self._validate_command(command)
|
|
257
|
+
if validation_error:
|
|
258
|
+
yield f"[ERROR] {validation_error}"
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
timeout = timeout or self.config.timeout
|
|
262
|
+
cwd = self.config.working_dir or os.getcwd()
|
|
263
|
+
|
|
264
|
+
environment = os.environ.copy()
|
|
265
|
+
environment.update(self.config.environment)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
if self.config.shell:
|
|
269
|
+
process = await asyncio.create_subprocess_shell(
|
|
270
|
+
command,
|
|
271
|
+
stdout=asyncio.subprocess.PIPE,
|
|
272
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
273
|
+
cwd=cwd,
|
|
274
|
+
env=environment,
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
args = shlex.split(command)
|
|
278
|
+
process = await asyncio.create_subprocess_exec(
|
|
279
|
+
*args,
|
|
280
|
+
stdout=asyncio.subprocess.PIPE,
|
|
281
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
282
|
+
cwd=cwd,
|
|
283
|
+
env=environment,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
self._processes[process.pid] = process
|
|
287
|
+
start_time = time.time()
|
|
288
|
+
output_size = 0
|
|
289
|
+
|
|
290
|
+
async for line in process.stdout:
|
|
291
|
+
# Check timeout
|
|
292
|
+
if time.time() - start_time > timeout:
|
|
293
|
+
process.kill()
|
|
294
|
+
yield "[TIMEOUT] Command execution timed out"
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# Check output size
|
|
298
|
+
output_size += len(line)
|
|
299
|
+
if output_size > self.config.max_output_size:
|
|
300
|
+
process.kill()
|
|
301
|
+
yield "[TRUNCATED] Output exceeded maximum size"
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
decoded = line.decode("utf-8", errors="replace").rstrip()
|
|
305
|
+
if callback:
|
|
306
|
+
callback(decoded)
|
|
307
|
+
yield decoded
|
|
308
|
+
|
|
309
|
+
await process.wait()
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
yield f"[ERROR] {str(e)}"
|
|
313
|
+
finally:
|
|
314
|
+
if process.pid in self._processes:
|
|
315
|
+
del self._processes[process.pid]
|
|
316
|
+
|
|
317
|
+
async def run_multiple(
|
|
318
|
+
self,
|
|
319
|
+
commands: list[str],
|
|
320
|
+
parallel: bool = False,
|
|
321
|
+
stop_on_error: bool = True,
|
|
322
|
+
) -> list[CommandResult]:
|
|
323
|
+
"""
|
|
324
|
+
Execute multiple commands.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
commands: List of commands
|
|
328
|
+
parallel: Run in parallel (True) or sequential (False)
|
|
329
|
+
stop_on_error: Stop on first error (sequential only)
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List of results
|
|
333
|
+
"""
|
|
334
|
+
if parallel:
|
|
335
|
+
tasks = [self.run(cmd) for cmd in commands]
|
|
336
|
+
return await asyncio.gather(*tasks)
|
|
337
|
+
else:
|
|
338
|
+
results = []
|
|
339
|
+
for cmd in commands:
|
|
340
|
+
result = await self.run(cmd)
|
|
341
|
+
results.append(result)
|
|
342
|
+
if stop_on_error and not result.success:
|
|
343
|
+
break
|
|
344
|
+
return results
|
|
345
|
+
|
|
346
|
+
async def kill(self, pid: int) -> bool:
|
|
347
|
+
"""Kill a running process by PID"""
|
|
348
|
+
if pid in self._processes:
|
|
349
|
+
try:
|
|
350
|
+
self._processes[pid].kill()
|
|
351
|
+
await self._processes[pid].wait()
|
|
352
|
+
del self._processes[pid]
|
|
353
|
+
return True
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.error(f"Failed to kill process {pid}: {e}")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
async def kill_all(self) -> int:
|
|
359
|
+
"""Kill all running processes"""
|
|
360
|
+
killed = 0
|
|
361
|
+
for pid in list(self._processes.keys()):
|
|
362
|
+
if await self.kill(pid):
|
|
363
|
+
killed += 1
|
|
364
|
+
return killed
|
|
365
|
+
|
|
366
|
+
def _validate_command(self, command: str) -> Optional[str]:
|
|
367
|
+
"""Validate command for safety"""
|
|
368
|
+
cmd_lower = command.lower()
|
|
369
|
+
|
|
370
|
+
# Check blocked commands
|
|
371
|
+
for blocked in self.config.blocked_commands:
|
|
372
|
+
if blocked.lower() in cmd_lower:
|
|
373
|
+
return f"Blocked command pattern: {blocked}"
|
|
374
|
+
|
|
375
|
+
# Check sudo
|
|
376
|
+
if not self.config.allow_sudo and cmd_lower.strip().startswith("sudo"):
|
|
377
|
+
return "sudo commands are not allowed"
|
|
378
|
+
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
def _limit_output(self, output: str) -> str:
|
|
382
|
+
"""Limit output size"""
|
|
383
|
+
if len(output) > self.config.max_output_size:
|
|
384
|
+
return output[:self.config.max_output_size] + "\n[OUTPUT TRUNCATED]"
|
|
385
|
+
return output
|
|
386
|
+
|
|
387
|
+
def get_history(self, limit: int = 100) -> list[CommandResult]:
|
|
388
|
+
"""Get command execution history"""
|
|
389
|
+
return self._history[-limit:]
|
|
390
|
+
|
|
391
|
+
def clear_history(self) -> None:
|
|
392
|
+
"""Clear execution history"""
|
|
393
|
+
self._history.clear()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Convenience function
|
|
397
|
+
async def run_command(command: str, timeout: float = 60.0) -> CommandResult:
|
|
398
|
+
"""Quick command execution"""
|
|
399
|
+
executor = TerminalExecutor()
|
|
400
|
+
return await executor.run(command, timeout=timeout)
|