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.
- detecti/__init__.py +0 -0
- detecti/cli.py +649 -0
- detecti/config.py +188 -0
- detecti/core/__init__.py +1 -0
- detecti/core/database/__init__.py +5 -0
- detecti/core/database/config_db.py +73 -0
- detecti/core/database/schema.py +136 -0
- detecti/core/database/storage.py +1388 -0
- detecti/core/engine.py +1032 -0
- detecti/core/models.py +278 -0
- detecti/data/config.sqlite +0 -0
- detecti/data/dbs/.gitkeep +2 -0
- detecti/data/dbs/example.com.sqlite +0 -0
- detecti/modules/__init__.py +29 -0
- detecti/modules/base.py +57 -0
- detecti/modules/censys.py +813 -0
- detecti/modules/crtsh.py +98 -0
- detecti/modules/exploitdb.py +138 -0
- detecti/modules/masscan.py +561 -0
- detecti/modules/nuclei.py +449 -0
- detecti/modules/nvd.py +300 -0
- detecti/modules/reverse_whois.py +225 -0
- detecti/modules/shodan.py +412 -0
- detecti/reporters/__init__.py +7 -0
- detecti/reporters/csv_reporter.py +74 -0
- detecti/reporters/html_reporter.py +356 -0
- detecti/reporters/json_reporter.py +26 -0
- detecti/reporters/markdown_reporter.py +203 -0
- detecti/utils/__init__.py +1 -0
- detecti/utils/http.py +294 -0
- detecti/utils/logger.py +378 -0
- detecti/utils/setup.py +453 -0
- detecti/web/__init__.py +6 -0
- detecti/web/api/__init__.py +1 -0
- detecti/web/api/auth.py +109 -0
- detecti/web/api/graph_builder.py +901 -0
- detecti/web/api/routes.py +1602 -0
- detecti/web/process_manager.py +283 -0
- detecti/web/server.py +183 -0
- detecti/web/static/android-chrome-192x192.png +0 -0
- detecti/web/static/android-chrome-512x512.png +0 -0
- detecti/web/static/apple-touch-icon.png +0 -0
- detecti/web/static/css/__init__.py +1 -0
- detecti/web/static/css/dashboard.css +3802 -0
- detecti/web/static/favicon-16x16.png +0 -0
- detecti/web/static/favicon-32x32.png +0 -0
- detecti/web/static/favicon.ico +0 -0
- detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
- detecti/web/static/img/detecti-ico.png +0 -0
- detecti/web/static/index.html +677 -0
- detecti/web/static/js/__init__.py +1 -0
- detecti/web/static/js/api.js +177 -0
- detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
- detecti/web/static/js/cytoscape-dagre.js +397 -0
- detecti/web/static/js/cytoscape.min.js +31 -0
- detecti/web/static/js/dagre.min.js +3809 -0
- detecti/web/static/js/graph.js +7439 -0
- detecti/web/static/js/lucide.min.js +12 -0
- detecti/web/static/login.html +290 -0
- detecti/web/static/site.webmanifest +1 -0
- detecti_cli-2.0.0.dist-info/METADATA +554 -0
- detecti_cli-2.0.0.dist-info/RECORD +64 -0
- detecti_cli-2.0.0.dist-info/WHEEL +4 -0
- detecti_cli-2.0.0.dist-info/entry_points.txt +3 -0
detecti/modules/crtsh.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Certificate Transparency (crt.sh) Subdomain Enumeration Module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any, Dict, List, Optional, Set
|
|
8
|
+
from detecti.config import settings
|
|
9
|
+
from detecti.core.models import Finding, FindingType
|
|
10
|
+
from detecti.modules.base import BaseModule
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("detecti.crtsh")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CrtshModule(BaseModule):
|
|
16
|
+
"""Enumerates subdomains using public Certificate Transparency logs via crt.sh."""
|
|
17
|
+
|
|
18
|
+
name: str = "crtsh"
|
|
19
|
+
description: str = "Certificate Transparency logs subdomain discovery"
|
|
20
|
+
category: str = "recon"
|
|
21
|
+
|
|
22
|
+
# Regex to validate valid domain/subdomain formats
|
|
23
|
+
DOMAIN_REGEX = re.compile(
|
|
24
|
+
r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def run(
|
|
28
|
+
self,
|
|
29
|
+
target: str,
|
|
30
|
+
context: Optional[Dict[str, Any]] = None,
|
|
31
|
+
) -> List[Finding]:
|
|
32
|
+
"""Query crt.sh for domain certificates and extract unique subdomains."""
|
|
33
|
+
# Clean target and extract registered root domain
|
|
34
|
+
domain = target.strip().lower()
|
|
35
|
+
if domain.startswith("http://") or domain.startswith("https://"):
|
|
36
|
+
domain = domain.split("//")[1].split("/")[0]
|
|
37
|
+
if ":" in domain:
|
|
38
|
+
domain = domain.split(":")[0]
|
|
39
|
+
if "/" in domain:
|
|
40
|
+
domain = domain.split("/")[0]
|
|
41
|
+
|
|
42
|
+
import tldextract
|
|
43
|
+
ext = tldextract.extract(domain)
|
|
44
|
+
search_domain = ext.registered_domain if (ext.domain and ext.suffix) else domain
|
|
45
|
+
|
|
46
|
+
# crt.sh query format
|
|
47
|
+
url = f"{settings.crtsh_api_url}/"
|
|
48
|
+
params = {"q": f"%.{search_domain}", "output": "json"}
|
|
49
|
+
|
|
50
|
+
findings: List[Finding] = []
|
|
51
|
+
discovered_subdomains: Set[str] = set()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
data = await self.http_client.get_json(url=url, params=params, timeout=20.0)
|
|
55
|
+
if not data or not isinstance(data, list):
|
|
56
|
+
logger.debug(f"No certificate data returned from crt.sh for {domain}")
|
|
57
|
+
return findings
|
|
58
|
+
|
|
59
|
+
for entry in data:
|
|
60
|
+
name_value = entry.get("name_value", "")
|
|
61
|
+
if not name_value:
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# An entry may contain multiple names separated by newlines
|
|
65
|
+
names = name_value.split("\n")
|
|
66
|
+
for name in names:
|
|
67
|
+
clean_name = name.strip().lower()
|
|
68
|
+
# Strip leading wildcard *.
|
|
69
|
+
if clean_name.startswith("*."):
|
|
70
|
+
clean_name = clean_name[2:]
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
clean_name
|
|
74
|
+
and clean_name.endswith(domain)
|
|
75
|
+
and self.DOMAIN_REGEX.match(clean_name)
|
|
76
|
+
):
|
|
77
|
+
discovered_subdomains.add(clean_name)
|
|
78
|
+
|
|
79
|
+
for sub in sorted(discovered_subdomains):
|
|
80
|
+
findings.append(
|
|
81
|
+
Finding(
|
|
82
|
+
type=FindingType.SUBDOMAIN,
|
|
83
|
+
target=domain,
|
|
84
|
+
value=sub,
|
|
85
|
+
source="crt.sh",
|
|
86
|
+
metadata={
|
|
87
|
+
"parent_domain": domain,
|
|
88
|
+
"is_apex": sub == domain,
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
logger.info(f"crt.sh discovered {len(findings)} unique subdomains for {domain}")
|
|
94
|
+
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
logger.warning(f"Error querying crt.sh for {domain}: {exc}")
|
|
97
|
+
|
|
98
|
+
return findings
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""ExploitDB & GitHub Proof of Concept (PoC) Intelligence Module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List, Optional, Set
|
|
8
|
+
import cve_searchsploit as cs
|
|
9
|
+
from detecti.config import settings
|
|
10
|
+
from detecti.core.models import ExploitData, Finding, FindingType
|
|
11
|
+
from detecti.modules.base import BaseModule
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("detecti.exploitdb")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ExploitDBModule(BaseModule):
|
|
17
|
+
"""Searches for public exploits and GitHub Proof-of-Concepts (PoCs) for CVEs."""
|
|
18
|
+
|
|
19
|
+
name: str = "exploitdb"
|
|
20
|
+
description: str = "ExploitDB and GitHub PoC exploit intelligence collector"
|
|
21
|
+
category: str = "exploit"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def update_database() -> None:
|
|
25
|
+
"""Update local ExploitDB searchsploit mapping database."""
|
|
26
|
+
logger.info("Updating local ExploitDB/SearchSploit database...")
|
|
27
|
+
cs.update_db()
|
|
28
|
+
|
|
29
|
+
async def run(
|
|
30
|
+
self,
|
|
31
|
+
target: str,
|
|
32
|
+
context: Optional[Dict[str, Any]] = None,
|
|
33
|
+
) -> List[Finding]:
|
|
34
|
+
"""Collect exploits and PoCs for targeted CVEs."""
|
|
35
|
+
target = target.strip()
|
|
36
|
+
cves_to_search: Set[str] = set()
|
|
37
|
+
|
|
38
|
+
if target.upper().startswith("CVE-"):
|
|
39
|
+
cves_to_search.add(target.upper())
|
|
40
|
+
|
|
41
|
+
if context and "cves" in context:
|
|
42
|
+
for c in context["cves"]:
|
|
43
|
+
if isinstance(c, str) and c.upper().startswith("CVE-"):
|
|
44
|
+
cves_to_search.add(c.upper())
|
|
45
|
+
|
|
46
|
+
if not cves_to_search:
|
|
47
|
+
logger.debug("No CVEs provided to ExploitDB module.")
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
findings: List[Finding] = []
|
|
51
|
+
for cve_id in sorted(cves_to_search):
|
|
52
|
+
exploits = await self.get_exploits_for_cve(cve_id)
|
|
53
|
+
for exp in exploits:
|
|
54
|
+
findings.append(
|
|
55
|
+
Finding(
|
|
56
|
+
type=FindingType.EXPLOIT,
|
|
57
|
+
target=target,
|
|
58
|
+
value=f"{cve_id} -> {exp.title}",
|
|
59
|
+
source=exp.source,
|
|
60
|
+
exploit=exp,
|
|
61
|
+
metadata={"cve_id": cve_id, "url": exp.url},
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return findings
|
|
66
|
+
|
|
67
|
+
async def get_exploits_for_cve(self, cve_id: str) -> List[ExploitData]:
|
|
68
|
+
"""Search ExploitDB (via cve_searchsploit) and GitHub PoC API concurrently."""
|
|
69
|
+
results: List[ExploitData] = []
|
|
70
|
+
|
|
71
|
+
# 1. Search ExploitDB
|
|
72
|
+
xdb_task = asyncio.to_thread(self._search_exploitdb, cve_id)
|
|
73
|
+
# 2. Search GitHub PoCs
|
|
74
|
+
git_task = self._search_github_pocs(cve_id)
|
|
75
|
+
|
|
76
|
+
xdb_results, git_results = await asyncio.gather(xdb_task, git_task, return_exceptions=True)
|
|
77
|
+
|
|
78
|
+
if isinstance(xdb_results, list):
|
|
79
|
+
results.extend(xdb_results)
|
|
80
|
+
elif isinstance(xdb_results, Exception):
|
|
81
|
+
logger.debug(f"Error querying ExploitDB for {cve_id}: {xdb_results}")
|
|
82
|
+
|
|
83
|
+
if isinstance(git_results, list):
|
|
84
|
+
results.extend(git_results)
|
|
85
|
+
elif isinstance(git_results, Exception):
|
|
86
|
+
logger.debug(f"Error querying GitHub PoCs for {cve_id}: {git_results}")
|
|
87
|
+
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def _search_exploitdb(self, cve_id: str) -> List[ExploitData]:
|
|
91
|
+
"""Search local cve_searchsploit index for ExploitDB entries."""
|
|
92
|
+
exploits: List[ExploitData] = []
|
|
93
|
+
try:
|
|
94
|
+
edb_ids = cs.edbid_from_cve(cve_id)
|
|
95
|
+
if edb_ids:
|
|
96
|
+
for eid in edb_ids:
|
|
97
|
+
url = f"{settings.exploit_db_base_url}/{eid}"
|
|
98
|
+
exploits.append(
|
|
99
|
+
ExploitData(
|
|
100
|
+
title=f"ExploitDB EDB-ID {eid} for {cve_id}",
|
|
101
|
+
source="ExploitDB",
|
|
102
|
+
url=url,
|
|
103
|
+
verified=True,
|
|
104
|
+
exploit_type="Exploit",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logger.debug(f"cve_searchsploit lookup failed for {cve_id}: {exc}")
|
|
109
|
+
return exploits
|
|
110
|
+
|
|
111
|
+
async def _search_github_pocs(self, cve_id: str) -> List[ExploitData]:
|
|
112
|
+
"""Search PoC-in-GitHub community aggregator for public PoC repositories."""
|
|
113
|
+
exploits: List[ExploitData] = []
|
|
114
|
+
url = f"{settings.github_poc_api_url}/"
|
|
115
|
+
params = {"cve_id": cve_id}
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
data = await self.http_client.get_json(url=url, params=params, timeout=12.0)
|
|
119
|
+
if data and isinstance(data, dict) and "pocs" in data and data["pocs"]:
|
|
120
|
+
for item in data["pocs"]:
|
|
121
|
+
repo_name = item.get("name") or item.get("html_url", "").split("/")[-1]
|
|
122
|
+
html_url = item.get("html_url")
|
|
123
|
+
owner = item.get("owner", {}).get("login") if isinstance(item.get("owner"), dict) else None
|
|
124
|
+
if html_url:
|
|
125
|
+
exploits.append(
|
|
126
|
+
ExploitData(
|
|
127
|
+
title=f"GitHub PoC ({repo_name})",
|
|
128
|
+
source="GitHub",
|
|
129
|
+
url=html_url,
|
|
130
|
+
verified=False,
|
|
131
|
+
author=owner,
|
|
132
|
+
exploit_type="Proof of Concept",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
logger.debug(f"GitHub PoC lookup failed for {cve_id}: {exc}")
|
|
137
|
+
|
|
138
|
+
return exploits
|