detecti-cli 2.0.0__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 (64) hide show
  1. detecti/__init__.py +0 -0
  2. detecti/cli.py +649 -0
  3. detecti/config.py +188 -0
  4. detecti/core/__init__.py +1 -0
  5. detecti/core/database/__init__.py +5 -0
  6. detecti/core/database/config_db.py +73 -0
  7. detecti/core/database/schema.py +136 -0
  8. detecti/core/database/storage.py +1388 -0
  9. detecti/core/engine.py +1032 -0
  10. detecti/core/models.py +278 -0
  11. detecti/data/config.sqlite +0 -0
  12. detecti/data/dbs/.gitkeep +2 -0
  13. detecti/data/dbs/example.com.sqlite +0 -0
  14. detecti/modules/__init__.py +29 -0
  15. detecti/modules/base.py +57 -0
  16. detecti/modules/censys.py +813 -0
  17. detecti/modules/crtsh.py +98 -0
  18. detecti/modules/exploitdb.py +138 -0
  19. detecti/modules/masscan.py +561 -0
  20. detecti/modules/nuclei.py +449 -0
  21. detecti/modules/nvd.py +300 -0
  22. detecti/modules/reverse_whois.py +225 -0
  23. detecti/modules/shodan.py +412 -0
  24. detecti/reporters/__init__.py +7 -0
  25. detecti/reporters/csv_reporter.py +74 -0
  26. detecti/reporters/html_reporter.py +356 -0
  27. detecti/reporters/json_reporter.py +26 -0
  28. detecti/reporters/markdown_reporter.py +203 -0
  29. detecti/utils/__init__.py +1 -0
  30. detecti/utils/http.py +294 -0
  31. detecti/utils/logger.py +378 -0
  32. detecti/utils/setup.py +453 -0
  33. detecti/web/__init__.py +6 -0
  34. detecti/web/api/__init__.py +1 -0
  35. detecti/web/api/auth.py +109 -0
  36. detecti/web/api/graph_builder.py +901 -0
  37. detecti/web/api/routes.py +1602 -0
  38. detecti/web/process_manager.py +283 -0
  39. detecti/web/server.py +183 -0
  40. detecti/web/static/android-chrome-192x192.png +0 -0
  41. detecti/web/static/android-chrome-512x512.png +0 -0
  42. detecti/web/static/apple-touch-icon.png +0 -0
  43. detecti/web/static/css/__init__.py +1 -0
  44. detecti/web/static/css/dashboard.css +3802 -0
  45. detecti/web/static/favicon-16x16.png +0 -0
  46. detecti/web/static/favicon-32x32.png +0 -0
  47. detecti/web/static/favicon.ico +0 -0
  48. detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
  49. detecti/web/static/img/detecti-ico.png +0 -0
  50. detecti/web/static/index.html +677 -0
  51. detecti/web/static/js/__init__.py +1 -0
  52. detecti/web/static/js/api.js +177 -0
  53. detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
  54. detecti/web/static/js/cytoscape-dagre.js +397 -0
  55. detecti/web/static/js/cytoscape.min.js +31 -0
  56. detecti/web/static/js/dagre.min.js +3809 -0
  57. detecti/web/static/js/graph.js +7439 -0
  58. detecti/web/static/js/lucide.min.js +12 -0
  59. detecti/web/static/login.html +290 -0
  60. detecti/web/static/site.webmanifest +1 -0
  61. detecti_cli-2.0.0.dist-info/METADATA +554 -0
  62. detecti_cli-2.0.0.dist-info/RECORD +64 -0
  63. detecti_cli-2.0.0.dist-info/WHEEL +4 -0
  64. detecti_cli-2.0.0.dist-info/entry_points.txt +3 -0
detecti/core/models.py ADDED
@@ -0,0 +1,278 @@
1
+ """Unified Pydantic Models for DetecTI Findings and Intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from enum import Enum
8
+ from typing import Any, Dict, List, Optional
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class FindingType(str, Enum):
13
+ """Enumeration of all supported finding types."""
14
+
15
+ HOST_INFO = "HOST_INFO"
16
+ SUBDOMAIN = "SUBDOMAIN"
17
+ ASSOCIATED_DOMAIN = "ASSOCIATED_DOMAIN"
18
+ OPEN_PORT = "OPEN_PORT"
19
+ VULNERABILITY = "VULNERABILITY"
20
+ EXPLOIT = "EXPLOIT"
21
+
22
+
23
+ class SeverityLevel(str, Enum):
24
+ """CVSS / Risk severity levels."""
25
+
26
+ CRITICAL = "CRITICAL"
27
+ HIGH = "HIGH"
28
+ MEDIUM = "MEDIUM"
29
+ LOW = "LOW"
30
+ NONE = "NONE"
31
+ UNKNOWN = "UNKNOWN"
32
+
33
+
34
+ class ExploitData(BaseModel):
35
+ """Exploit and Proof of Concept metadata."""
36
+
37
+ title: str = Field(..., description="Title or description of the exploit/PoC")
38
+ source: str = Field(..., description="Source of the exploit (e.g., ExploitDB, GitHub, PacketStorm)")
39
+ url: str = Field(..., description="Direct link or reference URL to the exploit")
40
+ verified: bool = Field(default=False, description="Whether the exploit has been verified")
41
+ author: Optional[str] = Field(default=None, description="Author or repository owner")
42
+ date: Optional[str] = Field(default=None, description="Date published or released")
43
+ exploit_type: Optional[str] = Field(default=None, description="Exploit classification (remote, local, web, dos)")
44
+
45
+
46
+ class CISAKEVData(BaseModel):
47
+ """Details from CISA Known Exploited Vulnerabilities catalog."""
48
+
49
+ in_cisa_kev: bool = Field(default=True, description="Whether the CVE is present in CISA KEV")
50
+ vendor_project: Optional[str] = Field(default=None, description="Vendor or project name")
51
+ product: Optional[str] = Field(default=None, description="Vulnerable product name")
52
+ vulnerability_name: Optional[str] = Field(default=None, description="Name of the vulnerability in KEV")
53
+ date_added: Optional[str] = Field(default=None, description="Date added to CISA KEV catalog")
54
+ due_date: Optional[str] = Field(default=None, description="Remediation due date for federal agencies")
55
+ required_action: Optional[str] = Field(default=None, description="Action required by CISA")
56
+ known_ransomware_campaign_use: Optional[str] = Field(
57
+ default=None, description="Known use in ransomware campaigns (Known/Unknown)"
58
+ )
59
+ notes: Optional[str] = Field(default=None, description="Additional CISA notes")
60
+
61
+
62
+ class EPSSData(BaseModel):
63
+ """EPSS (Exploit Prediction Scoring System) metrics."""
64
+
65
+ epss_score: float = Field(..., description="EPSS score between 0.0 and 1.0 (probability of exploitation in next 30 days)")
66
+ epss_percentile: float = Field(..., description="EPSS percentile relative to all other scored CVEs (0.0 to 1.0)")
67
+ date: Optional[str] = Field(default=None, description="EPSS scoring date")
68
+
69
+
70
+ class VulnerabilityData(BaseModel):
71
+ """Vulnerability (CVE) enriched data with CVSS, EPSS, CISA KEV, and Exploits."""
72
+
73
+ cve_id: str = Field(..., description="CVE identifier, e.g., CVE-2021-44228")
74
+ cvss_score: Optional[float] = Field(default=None, description="CVSS Base Score (0.0 to 10.0)")
75
+ cvss_version: Optional[str] = Field(default=None, description="CVSS version (e.g., '3.1', '3.0', '2.0')")
76
+ cvss_severity: SeverityLevel = Field(default=SeverityLevel.UNKNOWN, description="CVSS qualitative severity rating")
77
+ description: Optional[str] = Field(default=None, description="Summary or description of the vulnerability")
78
+ cwe_id: Optional[str] = Field(default=None, description="CWE identifier, e.g., CWE-502")
79
+ cwe_name: Optional[str] = Field(default=None, description="CWE weakness name, e.g., Deserialization of Untrusted Data")
80
+ epss: Optional[EPSSData] = Field(default=None, description="EPSS prediction score")
81
+ cisa_kev: Optional[CISAKEVData] = Field(default=None, description="CISA KEV catalog metadata")
82
+ references: List[str] = Field(default_factory=list, description="List of reference URLs (from NVD or vendor)")
83
+ exploits: List[ExploitData] = Field(default_factory=list, description="Public exploits and GitHub PoCs")
84
+
85
+ @property
86
+ def epss_score(self) -> Optional[float]:
87
+ return self.epss.epss_score if self.epss else None
88
+
89
+ @property
90
+ def in_cisa_kev(self) -> bool:
91
+ return bool(self.cisa_kev and self.cisa_kev.in_cisa_kev)
92
+
93
+
94
+ class PortData(BaseModel):
95
+ """Detailed open port / service information."""
96
+
97
+ port: int = Field(..., description="Port number")
98
+ transport: str = Field(default="tcp", description="Transport protocol (tcp/udp)")
99
+ service: Optional[str] = Field(default=None, description="Service name (http, ssh, etc.)")
100
+ product: Optional[str] = Field(default=None, description="Software/Product name")
101
+ version: Optional[str] = Field(default=None, description="Software version")
102
+ banner: Optional[str] = Field(default=None, description="Raw service banner")
103
+ url: Optional[str] = Field(default=None, description="Web URL if HTTP/HTTPS service")
104
+ ssl: bool = Field(default=False, description="Whether SSL/TLS is enabled")
105
+ sources: List[str] = Field(default_factory=list, description="Discovery sources for this port (e.g., Shodan, Censys)")
106
+
107
+ @property
108
+ def source(self) -> str:
109
+ """Formatted string of unique sources that identified this port."""
110
+ if not self.sources:
111
+ return "Unknown"
112
+ return ", ".join(dict.fromkeys(self.sources))
113
+
114
+
115
+ class HostInfoData(BaseModel):
116
+ """Detailed host metadata from Shodan, Censys, or DNS."""
117
+
118
+ ip: str = Field(..., description="IP address")
119
+ hostnames: List[str] = Field(default_factory=list, description="Hostnames resolved")
120
+ domains: List[str] = Field(default_factory=list, description="Domain names associated")
121
+ org: Optional[str] = Field(default=None, description="Organization name")
122
+ isp: Optional[str] = Field(default=None, description="ISP name")
123
+ asn: Optional[str] = Field(default=None, description="Autonomous System Number")
124
+ os: Optional[str] = Field(default=None, description="Operating System detected")
125
+ country_name: Optional[str] = Field(default=None, description="Country name")
126
+ country_code: Optional[str] = Field(default=None, description="Country 2-letter code")
127
+ city: Optional[str] = Field(default=None, description="City name")
128
+ region_code: Optional[str] = Field(default=None, description="State/Region code")
129
+ postal_code: Optional[str] = Field(default=None, description="Postal / ZIP code")
130
+ latitude: Optional[float] = Field(default=None, description="Latitude coordinate")
131
+ longitude: Optional[float] = Field(default=None, description="Longitude coordinate")
132
+ ports: List[int] = Field(default_factory=list, description="List of open port numbers")
133
+ vulns: List[str] = Field(default_factory=list, description="List of CVE IDs identified")
134
+
135
+
136
+ class HostResult(BaseModel):
137
+ """Complete host dossier encapsulating infrastructure, ports, and correlated vulnerabilities."""
138
+
139
+ ip: str = Field(..., description="Host IP address")
140
+ hostnames: List[str] = Field(default_factory=list, description="Hostnames pointing to this IP")
141
+ domains: List[str] = Field(default_factory=list, description="Domains associated with this host")
142
+ org: Optional[str] = Field(default=None, description="Organization name")
143
+ isp: Optional[str] = Field(default=None, description="ISP name")
144
+ asn: Optional[str] = Field(default=None, description="Autonomous System Number")
145
+ os: Optional[str] = Field(default=None, description="Operating System")
146
+ country_name: Optional[str] = Field(default=None, description="Country name")
147
+ country_code: Optional[str] = Field(default=None, description="Country code")
148
+ city: Optional[str] = Field(default=None, description="City name")
149
+ region_code: Optional[str] = Field(default=None, description="State/Region code")
150
+ postal_code: Optional[str] = Field(default=None, description="Postal / ZIP code")
151
+ latitude: Optional[float] = Field(default=None, description="Latitude coordinate")
152
+ longitude: Optional[float] = Field(default=None, description="Longitude coordinate")
153
+ ports: List[PortData] = Field(default_factory=list, description="Open ports and running services")
154
+ vulnerabilities: List[VulnerabilityData] = Field(
155
+ default_factory=list,
156
+ description="Enriched vulnerabilities specifically affecting this host",
157
+ )
158
+ sources: List[str] = Field(default_factory=list, description="Discovery sources that reported this host")
159
+ metadata: Dict[str, Any] = Field(default_factory=dict)
160
+
161
+ @property
162
+ def source(self) -> str:
163
+ """Formatted string of unique sources that identified this host."""
164
+ if not self.sources:
165
+ return "Unknown"
166
+ return ", ".join(dict.fromkeys(self.sources))
167
+
168
+
169
+ class Finding(BaseModel):
170
+ """Standardized finding item produced by any ThreatTrack module."""
171
+
172
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique finding ID")
173
+ type: FindingType = Field(..., description="Category of finding")
174
+ target: str = Field(..., description="Input target that triggered the finding")
175
+ value: str = Field(..., description="Primary finding value (domain, IP:port, CVE, etc.)")
176
+ source: str = Field(..., description="Module/Data source that produced the finding")
177
+ host_ip: Optional[str] = Field(default=None, description="Host IP this finding is associated with, if any")
178
+ host_info: Optional[HostInfoData] = Field(default=None, description="Host metadata if applicable")
179
+ port_info: Optional[PortData] = Field(default=None, description="Port metadata if applicable")
180
+ vulnerability: Optional[VulnerabilityData] = Field(default=None, description="Vulnerability metadata if applicable")
181
+ exploit: Optional[ExploitData] = Field(default=None, description="Exploit metadata if applicable")
182
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional module-specific context")
183
+ timestamp: datetime = Field(
184
+ default_factory=lambda: datetime.now(timezone.utc),
185
+ description="Timestamp when the finding was recorded",
186
+ )
187
+
188
+
189
+ class ScanSummary(BaseModel):
190
+ """Statistical summary of scan results."""
191
+
192
+ total_findings: int = Field(default=0)
193
+ total_hosts_count: int = Field(default=0)
194
+ subdomains_count: int = Field(default=0)
195
+ associated_domains_count: int = Field(default=0)
196
+ open_ports_count: int = Field(default=0)
197
+ vulnerabilities_count: int = Field(default=0)
198
+ exploits_count: int = Field(default=0)
199
+ cisa_kev_count: int = Field(default=0)
200
+ critical_vulns_count: int = Field(default=0)
201
+ high_vulns_count: int = Field(default=0)
202
+ medium_vulns_count: int = Field(default=0)
203
+ low_vulns_count: int = Field(default=0)
204
+
205
+
206
+ class ScanResult(BaseModel):
207
+ """Complete results of a ThreatTrack execution."""
208
+
209
+ scan_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Scan identifier")
210
+ target: str = Field(..., description="Original target queried")
211
+ target_type: str = Field(default="unknown", description="Classified target type (ip, cidr, domain, query, file, cve)")
212
+ started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
213
+ completed_at: Optional[datetime] = Field(default=None)
214
+ elapsed_seconds: float = Field(default=0.0)
215
+ modules_run: List[str] = Field(default_factory=list)
216
+ hosts: List[HostResult] = Field(default_factory=list, description="Detailed per-host dossiers")
217
+ findings: List[Finding] = Field(default_factory=list, description="All flattened findings")
218
+ warnings: List[str] = Field(default_factory=list, description="API notices and runtime warning logs")
219
+ summary: ScanSummary = Field(default_factory=ScanSummary)
220
+
221
+ def calculate_summary(self) -> ScanSummary:
222
+ """Compute the statistical summary across hosts and findings."""
223
+ summary = ScanSummary(
224
+ total_findings=len(self.findings),
225
+ total_hosts_count=len(self.hosts),
226
+ )
227
+ seen_vulns = set()
228
+
229
+ # Count from hosts if present
230
+ for host in self.hosts:
231
+ summary.open_ports_count += len(host.ports)
232
+ for v in host.vulnerabilities:
233
+ if v.cve_id not in seen_vulns:
234
+ seen_vulns.add(v.cve_id)
235
+ summary.vulnerabilities_count += 1
236
+ if v.in_cisa_kev:
237
+ summary.cisa_kev_count += 1
238
+ sev = v.cvss_severity
239
+ if sev == SeverityLevel.CRITICAL:
240
+ summary.critical_vulns_count += 1
241
+ elif sev == SeverityLevel.HIGH:
242
+ summary.high_vulns_count += 1
243
+ elif sev == SeverityLevel.MEDIUM:
244
+ summary.medium_vulns_count += 1
245
+ elif sev == SeverityLevel.LOW:
246
+ summary.low_vulns_count += 1
247
+ summary.exploits_count += len(v.exploits)
248
+
249
+ # Count domain findings
250
+ for finding in self.findings:
251
+ if finding.type == FindingType.SUBDOMAIN:
252
+ summary.subdomains_count += 1
253
+ elif finding.type == FindingType.ASSOCIATED_DOMAIN:
254
+ summary.associated_domains_count += 1
255
+ elif finding.type == FindingType.OPEN_PORT and not self.hosts:
256
+ summary.open_ports_count += 1
257
+ elif finding.type == FindingType.EXPLOIT and not self.hosts:
258
+ summary.exploits_count += 1
259
+ elif finding.type == FindingType.VULNERABILITY and finding.vulnerability and not self.hosts:
260
+ cve = finding.vulnerability.cve_id
261
+ if cve not in seen_vulns:
262
+ seen_vulns.add(cve)
263
+ summary.vulnerabilities_count += 1
264
+ if finding.vulnerability.in_cisa_kev:
265
+ summary.cisa_kev_count += 1
266
+ sev = finding.vulnerability.cvss_severity
267
+ if sev == SeverityLevel.CRITICAL:
268
+ summary.critical_vulns_count += 1
269
+ elif sev == SeverityLevel.HIGH:
270
+ summary.high_vulns_count += 1
271
+ elif sev == SeverityLevel.MEDIUM:
272
+ summary.medium_vulns_count += 1
273
+ elif sev == SeverityLevel.LOW:
274
+ summary.low_vulns_count += 1
275
+ summary.exploits_count += len(finding.vulnerability.exploits)
276
+
277
+ self.summary = summary
278
+ return summary
Binary file
@@ -0,0 +1,2 @@
1
+ # This directory stores SQLite databases for DetecTI-CLI EASM scan results
2
+ # Each target gets its own database file for web dashboard visualization
Binary file
@@ -0,0 +1,29 @@
1
+ """DetecTI-CLI intelligence and data collection modules package."""
2
+
3
+ from detecti.modules.base import BaseModule
4
+ from detecti.modules.censys import (
5
+ CensysAPIError,
6
+ CensysAuthError,
7
+ CensysModule,
8
+ CensysPlatformClient,
9
+ CensysRateLimitError,
10
+ )
11
+ from detecti.modules.crtsh import CrtshModule
12
+ from detecti.modules.exploitdb import ExploitDBModule
13
+ from detecti.modules.nvd import NVDModule
14
+ from detecti.modules.reverse_whois import ReverseWhoisModule
15
+ from detecti.modules.shodan import ShodanModule
16
+
17
+ __all__ = [
18
+ "BaseModule",
19
+ "ShodanModule",
20
+ "CensysModule",
21
+ "CensysPlatformClient",
22
+ "CensysAPIError",
23
+ "CensysAuthError",
24
+ "CensysRateLimitError",
25
+ "CrtshModule",
26
+ "ReverseWhoisModule",
27
+ "NVDModule",
28
+ "ExploitDBModule",
29
+ ]
@@ -0,0 +1,57 @@
1
+ """Base module interface definition for DetecTI data collectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict, List, Optional
7
+ from detecti.core.models import Finding
8
+ from detecti.utils.http import AsyncHTTPClient, http_client
9
+
10
+
11
+ class BaseModule(ABC):
12
+ """Abstract Base Class for all DetecTI collection and enrichment modules."""
13
+
14
+ name: str = "base_module"
15
+ description: str = "Base collector module"
16
+ category: str = "general" # recon, osint, vuln, exploit
17
+
18
+ def __init__(
19
+ self,
20
+ client: Optional[AsyncHTTPClient] = None,
21
+ progress_callback: Optional[Any] = None,
22
+ ):
23
+ self.http_client = client or http_client
24
+ self.progress_callback = progress_callback
25
+
26
+ def notify(self, message: str) -> None:
27
+ """Send notification via progress callback if registered."""
28
+ if self.progress_callback:
29
+ self.progress_callback(self.name, message)
30
+
31
+ @abstractmethod
32
+ async def run(
33
+ self,
34
+ target: str,
35
+ context: Optional[Dict[str, Any]] = None,
36
+ ) -> List[Finding]:
37
+ """Execute the module logic against the target and return list of findings.
38
+
39
+ Args:
40
+ target: The input value (IP, domain, CIDR, query, CVE, or organization)
41
+ context: Shared scan context or metadata passed between modules
42
+
43
+ Returns:
44
+ List of standardized Finding objects
45
+ """
46
+ pass
47
+
48
+ def is_configured(self) -> bool:
49
+ """Check if the module has all required configurations (e.g. API keys)."""
50
+ return True
51
+
52
+ async def health_check(self) -> bool:
53
+ """Verify that the external data source is reachable and operational."""
54
+ return True
55
+
56
+ def __repr__(self) -> str:
57
+ return f"<{self.__class__.__name__} name={self.name} category={self.category}>"