aiptx 2.0.2__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.
Potentially problematic release.
This version of aiptx might be problematic. Click here for more details.
- 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 +24 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +960 -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 +321 -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 +288 -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 +85 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -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/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/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 +2284 -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 +44 -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/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/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 +201 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aiptx-2.0.2.dist-info/METADATA +324 -0
- aiptx-2.0.2.dist-info/RECORD +165 -0
- aiptx-2.0.2.dist-info/WHEEL +5 -0
- aiptx-2.0.2.dist-info/entry_points.txt +7 -0
- aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.2.dist-info/top_level.txt +1 -0
aipt_v2/scanners/nmap.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Nmap Scanner Integration
|
|
3
|
+
|
|
4
|
+
Network scanning and service detection using Nmap.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import xml.etree.ElementTree as ET
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .base import BaseScanner, ScanFinding, ScanResult, ScanSeverity
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class NmapConfig:
|
|
22
|
+
"""Nmap scanner configuration"""
|
|
23
|
+
# Scan types
|
|
24
|
+
syn_scan: bool = True # -sS (requires root)
|
|
25
|
+
version_scan: bool = True # -sV
|
|
26
|
+
os_detection: bool = False # -O (requires root)
|
|
27
|
+
script_scan: bool = True # -sC (default scripts)
|
|
28
|
+
aggressive: bool = False # -A
|
|
29
|
+
|
|
30
|
+
# Port selection
|
|
31
|
+
ports: str = "" # e.g., "1-1000" or "22,80,443"
|
|
32
|
+
top_ports: int = 0 # --top-ports N
|
|
33
|
+
all_ports: bool = False # -p-
|
|
34
|
+
|
|
35
|
+
# Timing
|
|
36
|
+
timing: int = 4 # -T0 to -T5
|
|
37
|
+
|
|
38
|
+
# Output
|
|
39
|
+
xml_output: bool = True
|
|
40
|
+
|
|
41
|
+
# Scripts
|
|
42
|
+
scripts: list[str] = field(default_factory=list) # Specific NSE scripts
|
|
43
|
+
script_args: dict[str, str] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
# Advanced
|
|
46
|
+
no_ping: bool = False # -Pn
|
|
47
|
+
udp_scan: bool = False # -sU
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class NmapHost:
|
|
52
|
+
"""Parsed Nmap host result"""
|
|
53
|
+
address: str
|
|
54
|
+
hostname: str = ""
|
|
55
|
+
state: str = "unknown"
|
|
56
|
+
os: str = ""
|
|
57
|
+
ports: list[dict] = field(default_factory=list)
|
|
58
|
+
scripts: list[dict] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NmapScanner(BaseScanner):
|
|
62
|
+
"""
|
|
63
|
+
Nmap network scanner integration.
|
|
64
|
+
|
|
65
|
+
Features:
|
|
66
|
+
- Port scanning
|
|
67
|
+
- Service version detection
|
|
68
|
+
- OS fingerprinting
|
|
69
|
+
- NSE script execution
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
scanner = NmapScanner(NmapConfig(
|
|
73
|
+
version_scan=True,
|
|
74
|
+
top_ports=100,
|
|
75
|
+
))
|
|
76
|
+
result = await scanner.scan("192.168.1.0/24")
|
|
77
|
+
|
|
78
|
+
for finding in result.findings:
|
|
79
|
+
print(f"{finding.host}:{finding.port} - {finding.title}")
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, config: Optional[NmapConfig] = None):
|
|
83
|
+
super().__init__()
|
|
84
|
+
self.config = config or NmapConfig()
|
|
85
|
+
self._hosts: list[NmapHost] = []
|
|
86
|
+
|
|
87
|
+
def is_available(self) -> bool:
|
|
88
|
+
"""Check if Nmap is installed"""
|
|
89
|
+
return self._check_tool("nmap")
|
|
90
|
+
|
|
91
|
+
async def scan(self, target: str, **kwargs) -> ScanResult:
|
|
92
|
+
"""
|
|
93
|
+
Run Nmap scan on target.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
target: IP, hostname, or CIDR range
|
|
97
|
+
**kwargs: Override config options
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ScanResult with findings
|
|
101
|
+
"""
|
|
102
|
+
result = ScanResult(scanner="nmap", target=target)
|
|
103
|
+
result.start_time = datetime.utcnow()
|
|
104
|
+
result.status = "running"
|
|
105
|
+
|
|
106
|
+
if not self.is_available():
|
|
107
|
+
result.status = "failed"
|
|
108
|
+
result.errors.append("Nmap is not installed")
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
# Build command
|
|
112
|
+
command = self._build_command(target, **kwargs)
|
|
113
|
+
logger.info(f"Running Nmap: {' '.join(command)}")
|
|
114
|
+
|
|
115
|
+
# Execute
|
|
116
|
+
exit_code, stdout, stderr = await self._run_command(
|
|
117
|
+
command,
|
|
118
|
+
timeout=kwargs.get("timeout", 900.0), # 15 min default
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result.end_time = datetime.utcnow()
|
|
122
|
+
result.duration_seconds = (result.end_time - result.start_time).total_seconds()
|
|
123
|
+
result.raw_output = stdout
|
|
124
|
+
|
|
125
|
+
if exit_code != 0:
|
|
126
|
+
result.status = "failed"
|
|
127
|
+
result.errors.append(stderr)
|
|
128
|
+
else:
|
|
129
|
+
result.status = "completed"
|
|
130
|
+
|
|
131
|
+
# Parse output
|
|
132
|
+
if self.config.xml_output:
|
|
133
|
+
result.findings = self.parse_output(stdout)
|
|
134
|
+
else:
|
|
135
|
+
result.findings = self._parse_text_output(stdout, target)
|
|
136
|
+
|
|
137
|
+
logger.info(
|
|
138
|
+
f"Nmap scan complete: {len(result.findings)} findings in {result.duration_seconds:.1f}s"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def parse_output(self, output: str) -> list[ScanFinding]:
|
|
144
|
+
"""Parse Nmap XML output"""
|
|
145
|
+
findings = []
|
|
146
|
+
self._hosts = []
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
# Find XML content
|
|
150
|
+
xml_start = output.find("<?xml")
|
|
151
|
+
if xml_start == -1:
|
|
152
|
+
return self._parse_text_output(output, "")
|
|
153
|
+
|
|
154
|
+
xml_content = output[xml_start:]
|
|
155
|
+
root = ET.fromstring(xml_content)
|
|
156
|
+
|
|
157
|
+
for host_elem in root.findall(".//host"):
|
|
158
|
+
host = self._parse_host(host_elem)
|
|
159
|
+
self._hosts.append(host)
|
|
160
|
+
|
|
161
|
+
# Create findings for open ports
|
|
162
|
+
for port_info in host.ports:
|
|
163
|
+
if port_info["state"] == "open":
|
|
164
|
+
finding = ScanFinding(
|
|
165
|
+
title=f"Open Port: {port_info['port']}/{port_info['protocol']}",
|
|
166
|
+
severity=ScanSeverity.INFO,
|
|
167
|
+
description=f"Service: {port_info['service']} {port_info['version']}".strip(),
|
|
168
|
+
host=host.address,
|
|
169
|
+
port=int(port_info["port"]),
|
|
170
|
+
scanner="nmap",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Add product/version info
|
|
174
|
+
if port_info.get("product"):
|
|
175
|
+
finding.evidence = f"Product: {port_info['product']}"
|
|
176
|
+
if port_info.get("version"):
|
|
177
|
+
finding.evidence += f" Version: {port_info['version']}"
|
|
178
|
+
|
|
179
|
+
findings.append(finding)
|
|
180
|
+
|
|
181
|
+
# Create findings from script results
|
|
182
|
+
for script in host.scripts:
|
|
183
|
+
severity = self._script_severity(script)
|
|
184
|
+
finding = ScanFinding(
|
|
185
|
+
title=f"NSE Script: {script['id']}",
|
|
186
|
+
severity=severity,
|
|
187
|
+
description=script.get("output", "")[:500],
|
|
188
|
+
host=host.address,
|
|
189
|
+
scanner="nmap",
|
|
190
|
+
template=script["id"],
|
|
191
|
+
)
|
|
192
|
+
findings.append(finding)
|
|
193
|
+
|
|
194
|
+
except ET.ParseError as e:
|
|
195
|
+
logger.error(f"XML parse error: {e}")
|
|
196
|
+
return self._parse_text_output(output, "")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Nmap output parse error: {e}")
|
|
199
|
+
|
|
200
|
+
return findings
|
|
201
|
+
|
|
202
|
+
def _parse_host(self, host_elem: ET.Element) -> NmapHost:
|
|
203
|
+
"""Parse host element from Nmap XML"""
|
|
204
|
+
host = NmapHost(address="")
|
|
205
|
+
|
|
206
|
+
# Address
|
|
207
|
+
addr_elem = host_elem.find("address")
|
|
208
|
+
if addr_elem is not None:
|
|
209
|
+
host.address = addr_elem.get("addr", "")
|
|
210
|
+
|
|
211
|
+
# Hostname
|
|
212
|
+
hostname_elem = host_elem.find(".//hostname")
|
|
213
|
+
if hostname_elem is not None:
|
|
214
|
+
host.hostname = hostname_elem.get("name", "")
|
|
215
|
+
|
|
216
|
+
# Status
|
|
217
|
+
status_elem = host_elem.find("status")
|
|
218
|
+
if status_elem is not None:
|
|
219
|
+
host.state = status_elem.get("state", "unknown")
|
|
220
|
+
|
|
221
|
+
# OS detection
|
|
222
|
+
os_elem = host_elem.find(".//osmatch")
|
|
223
|
+
if os_elem is not None:
|
|
224
|
+
host.os = os_elem.get("name", "")
|
|
225
|
+
|
|
226
|
+
# Ports
|
|
227
|
+
for port_elem in host_elem.findall(".//port"):
|
|
228
|
+
port_info = {
|
|
229
|
+
"port": port_elem.get("portid", ""),
|
|
230
|
+
"protocol": port_elem.get("protocol", "tcp"),
|
|
231
|
+
"state": "",
|
|
232
|
+
"service": "",
|
|
233
|
+
"product": "",
|
|
234
|
+
"version": "",
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
state_elem = port_elem.find("state")
|
|
238
|
+
if state_elem is not None:
|
|
239
|
+
port_info["state"] = state_elem.get("state", "")
|
|
240
|
+
|
|
241
|
+
service_elem = port_elem.find("service")
|
|
242
|
+
if service_elem is not None:
|
|
243
|
+
port_info["service"] = service_elem.get("name", "")
|
|
244
|
+
port_info["product"] = service_elem.get("product", "")
|
|
245
|
+
port_info["version"] = service_elem.get("version", "")
|
|
246
|
+
|
|
247
|
+
host.ports.append(port_info)
|
|
248
|
+
|
|
249
|
+
# Port-level scripts
|
|
250
|
+
for script_elem in port_elem.findall("script"):
|
|
251
|
+
host.scripts.append({
|
|
252
|
+
"id": script_elem.get("id", ""),
|
|
253
|
+
"output": script_elem.get("output", ""),
|
|
254
|
+
"port": port_info["port"],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
# Host-level scripts
|
|
258
|
+
for script_elem in host_elem.findall(".//hostscript/script"):
|
|
259
|
+
host.scripts.append({
|
|
260
|
+
"id": script_elem.get("id", ""),
|
|
261
|
+
"output": script_elem.get("output", ""),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return host
|
|
265
|
+
|
|
266
|
+
def _parse_text_output(self, output: str, target: str) -> list[ScanFinding]:
|
|
267
|
+
"""Fallback text output parsing"""
|
|
268
|
+
findings = []
|
|
269
|
+
|
|
270
|
+
# Simple regex for open ports
|
|
271
|
+
port_pattern = r"(\d+)/(tcp|udp)\s+open\s+(\S+)(?:\s+(.+))?"
|
|
272
|
+
|
|
273
|
+
for match in re.finditer(port_pattern, output):
|
|
274
|
+
port, protocol, service, version = match.groups()
|
|
275
|
+
finding = ScanFinding(
|
|
276
|
+
title=f"Open Port: {port}/{protocol}",
|
|
277
|
+
severity=ScanSeverity.INFO,
|
|
278
|
+
description=f"Service: {service}" + (f" - {version}" if version else ""),
|
|
279
|
+
host=target,
|
|
280
|
+
port=int(port),
|
|
281
|
+
scanner="nmap",
|
|
282
|
+
)
|
|
283
|
+
findings.append(finding)
|
|
284
|
+
|
|
285
|
+
return findings
|
|
286
|
+
|
|
287
|
+
def _script_severity(self, script: dict) -> ScanSeverity:
|
|
288
|
+
"""Determine severity from script results"""
|
|
289
|
+
script_id = script.get("id", "").lower()
|
|
290
|
+
output = script.get("output", "").lower()
|
|
291
|
+
|
|
292
|
+
# High severity indicators
|
|
293
|
+
if any(kw in script_id for kw in ["vuln", "exploit", "backdoor"]):
|
|
294
|
+
return ScanSeverity.HIGH
|
|
295
|
+
|
|
296
|
+
# Medium severity
|
|
297
|
+
if any(kw in script_id for kw in ["default", "brute", "enum"]):
|
|
298
|
+
return ScanSeverity.MEDIUM
|
|
299
|
+
|
|
300
|
+
# Check output for vulnerability indicators
|
|
301
|
+
if "vulnerable" in output or "exploitable" in output:
|
|
302
|
+
return ScanSeverity.HIGH
|
|
303
|
+
|
|
304
|
+
return ScanSeverity.LOW
|
|
305
|
+
|
|
306
|
+
def _build_command(self, target: str, **kwargs) -> list[str]:
|
|
307
|
+
"""Build Nmap command"""
|
|
308
|
+
command = ["nmap"]
|
|
309
|
+
|
|
310
|
+
# Scan types
|
|
311
|
+
if self.config.syn_scan:
|
|
312
|
+
command.append("-sS")
|
|
313
|
+
else:
|
|
314
|
+
command.append("-sT") # TCP connect scan
|
|
315
|
+
|
|
316
|
+
if self.config.version_scan:
|
|
317
|
+
command.append("-sV")
|
|
318
|
+
|
|
319
|
+
if self.config.os_detection:
|
|
320
|
+
command.append("-O")
|
|
321
|
+
|
|
322
|
+
if self.config.script_scan:
|
|
323
|
+
command.append("-sC")
|
|
324
|
+
|
|
325
|
+
if self.config.aggressive:
|
|
326
|
+
command.append("-A")
|
|
327
|
+
|
|
328
|
+
if self.config.udp_scan:
|
|
329
|
+
command.append("-sU")
|
|
330
|
+
|
|
331
|
+
# Port selection
|
|
332
|
+
if self.config.all_ports:
|
|
333
|
+
command.append("-p-")
|
|
334
|
+
elif self.config.ports:
|
|
335
|
+
command.extend(["-p", self.config.ports])
|
|
336
|
+
elif self.config.top_ports:
|
|
337
|
+
command.extend(["--top-ports", str(self.config.top_ports)])
|
|
338
|
+
|
|
339
|
+
# Timing
|
|
340
|
+
command.append(f"-T{self.config.timing}")
|
|
341
|
+
|
|
342
|
+
# Scripts
|
|
343
|
+
if self.config.scripts:
|
|
344
|
+
command.extend(["--script", ",".join(self.config.scripts)])
|
|
345
|
+
|
|
346
|
+
if self.config.script_args:
|
|
347
|
+
args = ",".join(f"{k}={v}" for k, v in self.config.script_args.items())
|
|
348
|
+
command.extend(["--script-args", args])
|
|
349
|
+
|
|
350
|
+
# Options
|
|
351
|
+
if self.config.no_ping:
|
|
352
|
+
command.append("-Pn")
|
|
353
|
+
|
|
354
|
+
# Output format
|
|
355
|
+
if self.config.xml_output:
|
|
356
|
+
command.extend(["-oX", "-"]) # XML to stdout
|
|
357
|
+
|
|
358
|
+
# Target
|
|
359
|
+
command.append(target)
|
|
360
|
+
|
|
361
|
+
return command
|
|
362
|
+
|
|
363
|
+
def get_hosts(self) -> list[NmapHost]:
|
|
364
|
+
"""Get parsed hosts from last scan"""
|
|
365
|
+
return self._hosts
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# Convenience functions
|
|
369
|
+
async def quick_port_scan(target: str, ports: str = "1-1000") -> ScanResult:
|
|
370
|
+
"""Quick port scan"""
|
|
371
|
+
config = NmapConfig(
|
|
372
|
+
ports=ports,
|
|
373
|
+
version_scan=False,
|
|
374
|
+
script_scan=False,
|
|
375
|
+
timing=4,
|
|
376
|
+
)
|
|
377
|
+
scanner = NmapScanner(config)
|
|
378
|
+
return await scanner.scan(target)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def service_scan(target: str) -> ScanResult:
|
|
382
|
+
"""Service version detection scan"""
|
|
383
|
+
config = NmapConfig(
|
|
384
|
+
top_ports=1000,
|
|
385
|
+
version_scan=True,
|
|
386
|
+
script_scan=True,
|
|
387
|
+
timing=3,
|
|
388
|
+
)
|
|
389
|
+
scanner = NmapScanner(config)
|
|
390
|
+
return await scanner.scan(target)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
async def vuln_scan(target: str) -> ScanResult:
|
|
394
|
+
"""Vulnerability scan with NSE scripts"""
|
|
395
|
+
config = NmapConfig(
|
|
396
|
+
top_ports=1000,
|
|
397
|
+
version_scan=True,
|
|
398
|
+
scripts=["vuln", "exploit"],
|
|
399
|
+
timing=3,
|
|
400
|
+
)
|
|
401
|
+
scanner = NmapScanner(config)
|
|
402
|
+
return await scanner.scan(target, timeout=1800.0)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Nuclei Scanner Integration
|
|
3
|
+
|
|
4
|
+
Template-based vulnerability scanning using Nuclei.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import AsyncIterator, Optional
|
|
13
|
+
|
|
14
|
+
from .base import BaseScanner, ScanFinding, ScanResult, ScanSeverity
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class NucleiConfig:
|
|
21
|
+
"""Nuclei scanner configuration"""
|
|
22
|
+
# Template selection
|
|
23
|
+
templates: list[str] = field(default_factory=list) # Specific templates
|
|
24
|
+
tags: list[str] = field(default_factory=list) # Filter by tags
|
|
25
|
+
severity: list[str] = field(default_factory=lambda: ["critical", "high", "medium"])
|
|
26
|
+
exclude_tags: list[str] = field(default_factory=lambda: ["dos", "fuzz"])
|
|
27
|
+
|
|
28
|
+
# Scanning options
|
|
29
|
+
rate_limit: int = 150 # Requests per second
|
|
30
|
+
bulk_size: int = 25
|
|
31
|
+
concurrency: int = 25
|
|
32
|
+
timeout: int = 10 # Per-request timeout
|
|
33
|
+
|
|
34
|
+
# Output
|
|
35
|
+
json_output: bool = True
|
|
36
|
+
silent: bool = True
|
|
37
|
+
no_color: bool = True
|
|
38
|
+
|
|
39
|
+
# Advanced
|
|
40
|
+
new_templates: bool = False # Only new templates
|
|
41
|
+
automatic_scan: bool = False # Auto-detect tech stack
|
|
42
|
+
headless: bool = False # Browser-based templates
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NucleiScanner(BaseScanner):
|
|
46
|
+
"""
|
|
47
|
+
Nuclei vulnerability scanner integration.
|
|
48
|
+
|
|
49
|
+
Nuclei is a fast, template-based scanner that checks for:
|
|
50
|
+
- CVEs
|
|
51
|
+
- Misconfigurations
|
|
52
|
+
- Exposed panels
|
|
53
|
+
- Default credentials
|
|
54
|
+
- Known vulnerabilities
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
scanner = NucleiScanner(NucleiConfig(
|
|
58
|
+
severity=["critical", "high"],
|
|
59
|
+
tags=["cve", "exposure"],
|
|
60
|
+
))
|
|
61
|
+
result = await scanner.scan("https://target.com")
|
|
62
|
+
|
|
63
|
+
for finding in result.get_critical_and_high():
|
|
64
|
+
print(f"{finding.severity}: {finding.title}")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, config: Optional[NucleiConfig] = None):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self.config = config or NucleiConfig()
|
|
70
|
+
|
|
71
|
+
def is_available(self) -> bool:
|
|
72
|
+
"""Check if Nuclei is installed"""
|
|
73
|
+
return self._check_tool("nuclei")
|
|
74
|
+
|
|
75
|
+
async def scan(self, target: str, **kwargs) -> ScanResult:
|
|
76
|
+
"""
|
|
77
|
+
Run Nuclei scan on target.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
target: URL or host to scan
|
|
81
|
+
**kwargs: Override config options
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
ScanResult with findings
|
|
85
|
+
"""
|
|
86
|
+
result = ScanResult(scanner="nuclei", target=target)
|
|
87
|
+
result.start_time = datetime.utcnow()
|
|
88
|
+
result.status = "running"
|
|
89
|
+
|
|
90
|
+
if not self.is_available():
|
|
91
|
+
result.status = "failed"
|
|
92
|
+
result.errors.append("Nuclei is not installed")
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
# Build command
|
|
96
|
+
command = self._build_command(target, **kwargs)
|
|
97
|
+
logger.info(f"Running Nuclei: {' '.join(command)}")
|
|
98
|
+
|
|
99
|
+
# Execute
|
|
100
|
+
exit_code, stdout, stderr = await self._run_command(
|
|
101
|
+
command,
|
|
102
|
+
timeout=kwargs.get("timeout", 600.0),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
result.end_time = datetime.utcnow()
|
|
106
|
+
result.duration_seconds = (result.end_time - result.start_time).total_seconds()
|
|
107
|
+
result.raw_output = stdout
|
|
108
|
+
|
|
109
|
+
if exit_code != 0 and "no results found" not in stderr.lower():
|
|
110
|
+
result.status = "failed"
|
|
111
|
+
result.errors.append(stderr)
|
|
112
|
+
else:
|
|
113
|
+
result.status = "completed"
|
|
114
|
+
|
|
115
|
+
# Parse output
|
|
116
|
+
result.findings = self.parse_output(stdout)
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
f"Nuclei scan complete: {len(result.findings)} findings in {result.duration_seconds:.1f}s"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
async def stream_scan(self, target: str, **kwargs) -> AsyncIterator[str]:
|
|
125
|
+
"""Stream Nuclei output as it runs"""
|
|
126
|
+
if not self.is_available():
|
|
127
|
+
yield "[ERROR] Nuclei is not installed"
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Build command with streaming-friendly options
|
|
131
|
+
config = NucleiConfig(**{**self.config.__dict__, **kwargs})
|
|
132
|
+
config.silent = False
|
|
133
|
+
|
|
134
|
+
command = self._build_command(target, config=config)
|
|
135
|
+
|
|
136
|
+
async for line in self._stream_command(command, timeout=kwargs.get("timeout", 600.0)):
|
|
137
|
+
yield line
|
|
138
|
+
|
|
139
|
+
def parse_output(self, output: str) -> list[ScanFinding]:
|
|
140
|
+
"""Parse Nuclei JSON output"""
|
|
141
|
+
findings = []
|
|
142
|
+
|
|
143
|
+
for line in output.strip().split("\n"):
|
|
144
|
+
if not line.strip():
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
data = json.loads(line)
|
|
149
|
+
|
|
150
|
+
severity_map = {
|
|
151
|
+
"info": ScanSeverity.INFO,
|
|
152
|
+
"low": ScanSeverity.LOW,
|
|
153
|
+
"medium": ScanSeverity.MEDIUM,
|
|
154
|
+
"high": ScanSeverity.HIGH,
|
|
155
|
+
"critical": ScanSeverity.CRITICAL,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
info = data.get("info", {})
|
|
159
|
+
severity_str = info.get("severity", "info").lower()
|
|
160
|
+
|
|
161
|
+
finding = ScanFinding(
|
|
162
|
+
title=info.get("name", "Unknown"),
|
|
163
|
+
severity=severity_map.get(severity_str, ScanSeverity.INFO),
|
|
164
|
+
description=info.get("description", ""),
|
|
165
|
+
url=data.get("matched-at", data.get("host", "")),
|
|
166
|
+
host=data.get("host", ""),
|
|
167
|
+
template=data.get("template-id", ""),
|
|
168
|
+
tags=info.get("tags", []),
|
|
169
|
+
scanner="nuclei",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Extract CVE/CWE if present
|
|
173
|
+
classification = info.get("classification", {})
|
|
174
|
+
if classification.get("cve-id"):
|
|
175
|
+
cves = classification["cve-id"]
|
|
176
|
+
finding.cve = cves[0] if isinstance(cves, list) else cves
|
|
177
|
+
if classification.get("cwe-id"):
|
|
178
|
+
cwes = classification["cwe-id"]
|
|
179
|
+
finding.cwe = cwes[0] if isinstance(cwes, list) else cwes
|
|
180
|
+
if classification.get("cvss-score"):
|
|
181
|
+
finding.cvss = float(classification["cvss-score"])
|
|
182
|
+
|
|
183
|
+
# Extract evidence
|
|
184
|
+
if data.get("extracted-results"):
|
|
185
|
+
finding.evidence = "\n".join(data["extracted-results"])
|
|
186
|
+
elif data.get("matcher-name"):
|
|
187
|
+
finding.evidence = f"Matched: {data['matcher-name']}"
|
|
188
|
+
|
|
189
|
+
# Request/response if available
|
|
190
|
+
if data.get("request"):
|
|
191
|
+
finding.request = data["request"][:2000]
|
|
192
|
+
if data.get("response"):
|
|
193
|
+
finding.response = data["response"][:2000]
|
|
194
|
+
|
|
195
|
+
findings.append(finding)
|
|
196
|
+
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
# Non-JSON output line, skip
|
|
199
|
+
continue
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.debug(f"Error parsing Nuclei output line: {e}")
|
|
202
|
+
|
|
203
|
+
return findings
|
|
204
|
+
|
|
205
|
+
def _build_command(self, target: str, config: Optional[NucleiConfig] = None, **kwargs) -> list[str]:
|
|
206
|
+
"""Build Nuclei command"""
|
|
207
|
+
cfg = config or self.config
|
|
208
|
+
|
|
209
|
+
command = ["nuclei", "-u", target]
|
|
210
|
+
|
|
211
|
+
# Template selection
|
|
212
|
+
if cfg.templates:
|
|
213
|
+
for template in cfg.templates:
|
|
214
|
+
command.extend(["-t", template])
|
|
215
|
+
|
|
216
|
+
if cfg.tags:
|
|
217
|
+
command.extend(["-tags", ",".join(cfg.tags)])
|
|
218
|
+
|
|
219
|
+
if cfg.severity:
|
|
220
|
+
command.extend(["-severity", ",".join(cfg.severity)])
|
|
221
|
+
|
|
222
|
+
if cfg.exclude_tags:
|
|
223
|
+
command.extend(["-exclude-tags", ",".join(cfg.exclude_tags)])
|
|
224
|
+
|
|
225
|
+
# Rate limiting
|
|
226
|
+
command.extend(["-rate-limit", str(cfg.rate_limit)])
|
|
227
|
+
command.extend(["-bulk-size", str(cfg.bulk_size)])
|
|
228
|
+
command.extend(["-concurrency", str(cfg.concurrency)])
|
|
229
|
+
command.extend(["-timeout", str(cfg.timeout)])
|
|
230
|
+
|
|
231
|
+
# Output format
|
|
232
|
+
if cfg.json_output:
|
|
233
|
+
command.append("-json")
|
|
234
|
+
|
|
235
|
+
if cfg.silent:
|
|
236
|
+
command.append("-silent")
|
|
237
|
+
|
|
238
|
+
if cfg.no_color:
|
|
239
|
+
command.append("-no-color")
|
|
240
|
+
|
|
241
|
+
# Advanced options
|
|
242
|
+
if cfg.new_templates:
|
|
243
|
+
command.append("-new-templates")
|
|
244
|
+
|
|
245
|
+
if cfg.automatic_scan:
|
|
246
|
+
command.append("-automatic-scan")
|
|
247
|
+
|
|
248
|
+
if cfg.headless:
|
|
249
|
+
command.append("-headless")
|
|
250
|
+
|
|
251
|
+
return command
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# Convenience functions
|
|
255
|
+
async def quick_nuclei_scan(target: str, severity: list[str] = None) -> ScanResult:
|
|
256
|
+
"""Quick Nuclei scan with defaults"""
|
|
257
|
+
config = NucleiConfig(
|
|
258
|
+
severity=severity or ["critical", "high"],
|
|
259
|
+
rate_limit=100,
|
|
260
|
+
)
|
|
261
|
+
scanner = NucleiScanner(config)
|
|
262
|
+
return await scanner.scan(target)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def full_nuclei_scan(target: str) -> ScanResult:
|
|
266
|
+
"""Comprehensive Nuclei scan"""
|
|
267
|
+
config = NucleiConfig(
|
|
268
|
+
severity=["info", "low", "medium", "high", "critical"],
|
|
269
|
+
automatic_scan=True,
|
|
270
|
+
rate_limit=50, # Slower but more thorough
|
|
271
|
+
)
|
|
272
|
+
scanner = NucleiScanner(config)
|
|
273
|
+
return await scanner.scan(target, timeout=1800.0) # 30 minute timeout
|