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.

Files changed (165) 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 +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. aiptx-2.0.2.dist-info/top_level.txt +1 -0
@@ -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