build 4.6 Demo

This commit is contained in:
DaStra3003
2026-08-19 08:04:59 +02:00
commit ff8f1ca989
123 changed files with 17696 additions and 0 deletions
@@ -0,0 +1,11 @@
"""Optional, lazily loaded hash and endpoint enrichment."""
from .hash_crack import HashCrack, crack_hash
from .url_probe import UrlProbe, probe_url
from .engine import EnrichmentEngine
__all__ = [
"HashCrack", "crack_hash",
"UrlProbe", "probe_url",
"EnrichmentEngine",
]
+39
View File
@@ -0,0 +1,39 @@
"""Coordinate optional enrichment and progress callbacks."""
from __future__ import annotations
from typing import Callable, Optional, Sequence
from ..artifacts import HashArtifact, UrlArtifact
from .hash_crack import HashCrack, crack_hash
from .url_probe import UrlProbe, probe_url
# progress(done, total, item) -> None
ProgressCb = Optional[Callable[[int, int, object], None]]
class EnrichmentEngine:
"""Run hash and endpoint enrichment for artifact lists."""
def crack_hashes(self, artifacts: Sequence[HashArtifact], *,
progress: ProgressCb = None, **crack_kwargs) -> list[HashCrack]:
results: list[HashCrack] = []
total = len(artifacts)
for i, art in enumerate(artifacts, 1):
res = crack_hash(art, **crack_kwargs)
results.append(res)
if progress:
progress(i, total, res)
return results
def probe_urls(self, artifacts: Sequence[UrlArtifact], *,
progress: ProgressCb = None, active: bool = False,
**probe_kwargs) -> list[UrlProbe]:
results: list[UrlProbe] = []
total = len(artifacts)
for i, art in enumerate(artifacts, 1):
res = probe_url(art, active=active, **probe_kwargs)
results.append(res)
if progress:
progress(i, total, res)
return results
@@ -0,0 +1,144 @@
"""Run bounded offline hashcat attacks against supported capture artifacts."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
from dataclasses import dataclass, field
from typing import Callable, Optional, Sequence, Union
from ..artifacts import HashArtifact, identify_hash_mode
@dataclass
class HashCrack:
value: str
hashcat_mode: Optional[int] = None
hash_format: Optional[str] = None
cracked: bool = False
plaintext: Optional[str] = None
duration_seconds: Optional[float] = None
# pending | cracked | not_cracked | timeout | error | unsupported
state: str = "pending"
error: Optional[str] = None
def to_dict(self) -> dict:
return {
"value": self.value, "hashcat_mode": self.hashcat_mode,
"hash_format": self.hash_format, "cracked": self.cracked,
"plaintext": self.plaintext, "duration_seconds": self.duration_seconds,
"state": self.state, "error": self.error,
}
# Keep the bundled demo list first; authorized deployments may add a system list.
DEFAULT_WORDLISTS = (
str(Path(__file__).resolve().parents[3] / "demo_files" / "demo_wordlist.txt"),
"/usr/share/wordlists/rockyou.txt",
)
def build_crack_command(hashcat_path: str, mode: int, hash_file: str,
wordlists: Sequence[str], potfile: str) -> list[str]:
"""Build the hashcat dictionary-attack command."""
cmd = [hashcat_path, "-m", str(mode), "-a", "0", "-D", "1,2",
hash_file, *wordlists, "--potfile-path", potfile, "--force"]
return cmd
def build_show_command(hashcat_path: str, mode: int, hash_file: str,
potfile: str) -> list[str]:
"""Build the hashcat command used to read cracked results."""
return [hashcat_path, "-m", str(mode), "--show", "--potfile-path", potfile, hash_file]
def parse_show_output(stdout: str) -> Optional[str]:
"""Extract plaintext from ``hashcat --show`` output."""
if not stdout:
return None
for line in stdout.strip().splitlines():
if ":" in line:
return line.rsplit(":", 1)[1]
return None
def crack_hash(
artifact: Union[HashArtifact, str],
*,
wordlists: Sequence[str] = DEFAULT_WORDLISTS,
hashcat_path: str = "hashcat",
timeout: int = 300,
potfile: Optional[str] = None,
runner: Callable[..., subprocess.CompletedProcess] = subprocess.run,
which: Callable[[str], Optional[str]] = shutil.which,
) -> HashCrack:
"""Crack one artifact and convert runtime failures into a result state."""
if isinstance(artifact, HashArtifact):
value = artifact.value
mode = artifact.hashcat_mode
fmt = artifact.hash_format
else:
value = str(artifact)
mode = None
fmt = None
if mode is None:
mode, fmt = identify_hash_mode(value, hint_format=fmt)
result = HashCrack(value=value, hashcat_mode=mode, hash_format=fmt)
if mode is None:
result.state = "unsupported"
result.error = "No supported hashcat mode was detected"
return result
if which(hashcat_path) is None:
result.state = "error"
result.error = "hashcat is not installed or not available on PATH"
return result
available = [w for w in wordlists if os.path.exists(w)]
if not available:
result.state = "error"
result.error = "No wordlist is available"
return result
tmpdir = tempfile.mkdtemp(prefix="hashcrack_")
hash_file = os.path.join(tmpdir, "hash.txt")
pot = potfile or os.path.join(tmpdir, "hashcat.potfile")
try:
with open(hash_file, "w", encoding="utf-8") as fh:
fh.write(value.strip() + "\n")
start = time.time()
try:
runner(build_crack_command(hashcat_path, mode, hash_file, available, pot),
capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired:
result.state = "timeout"
result.duration_seconds = round(time.time() - start, 2)
result.error = f"hashcat timed out after {timeout}s"
return result
show = runner(build_show_command(hashcat_path, mode, hash_file, pot),
capture_output=True, text=True, timeout=timeout)
result.duration_seconds = round(time.time() - start, 2)
plaintext = parse_show_output(getattr(show, "stdout", "") or "")
if plaintext is not None:
result.cracked = True
result.plaintext = plaintext
result.state = "cracked"
else:
result.state = "not_cracked"
return result
except Exception as exc: # Enrichment failures must not stop capture analysis.
result.state = "error"
result.error = str(exc)[:300]
return result
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
+143
View File
@@ -0,0 +1,143 @@
"""Optionally enrich observed endpoints with DNS, port, and TLS checks.
Passive capture evidence remains authoritative. Active checks are opt-in and
limited to a small port set because operators must have authorization to scan.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, Optional, Sequence, Union
from urllib.parse import urlparse
from ..artifacts import UrlArtifact
DEFAULT_PORTS = (22, 80, 443, 8000, 8080)
@dataclass
class UrlProbe:
url: str
host: str
resolved_ips: list[str] = field(default_factory=list)
open_ports: list[int] = field(default_factory=list)
tls_versions: list[str] = field(default_factory=list)
cipher_suites: list[str] = field(default_factory=list)
certificate: Optional[str] = None
active: bool = False
error: Optional[str] = None
def to_dict(self) -> dict:
return {
"url": self.url, "host": self.host,
"resolved_ips": self.resolved_ips, "open_ports": self.open_ports,
"tls_versions": self.tls_versions, "cipher_suites": self.cipher_suites,
"certificate": self.certificate, "active": self.active, "error": self.error,
}
def host_of(url: str) -> str:
"""Reduce a URL or domain value to its hostname."""
s = str(url).strip()
if "://" not in s:
s = "//" + s
netloc = urlparse(s).netloc or ""
host = netloc.split("@")[-1].split(":")[0]
return host or str(url).strip()
def probe_url(
artifact: Union[UrlArtifact, str],
*,
active: bool = False,
ports: Sequence[int] = DEFAULT_PORTS,
dns_resolver: Optional[Callable[[str], list]] = None,
port_scanner: Optional[Callable[[str, Sequence[int]], list]] = None,
tls_scanner: Optional[Callable[[str], dict]] = None,
) -> UrlProbe:
"""Return passive observations and optionally perform authorized active checks."""
if isinstance(artifact, UrlArtifact):
url = artifact.value
probe = UrlProbe(url=url, host=host_of(url))
probe.open_ports = sorted(artifact.observed_ports)
probe.tls_versions = sorted(artifact.observed_tls)
else:
url = str(artifact)
probe = UrlProbe(url=url, host=host_of(url))
if not active:
return probe
probe.active = True
host = probe.host
try:
resolver = dns_resolver or _default_dns_resolver
probe.resolved_ips = resolver(host) or []
except Exception as exc:
probe.error = f"DNS: {exc}"
try:
scanner = port_scanner or _default_port_scanner
found = scanner(host, ports) or []
probe.open_ports = sorted(set(probe.open_ports) | set(found))
except Exception as exc:
probe.error = (probe.error or "") + f" Ports: {exc}"
try:
tls = tls_scanner or _default_tls_scanner
info = tls(host) or {}
if info.get("tls_version"):
probe.tls_versions = sorted(set(probe.tls_versions) | {info["tls_version"]})
if info.get("cipher"):
probe.cipher_suites = sorted(set(probe.cipher_suites) | {info["cipher"]})
probe.certificate = info.get("certificate")
except Exception as exc:
probe.error = (probe.error or "") + f" TLS: {exc}"
return probe
# --------------------------------------------------------------------------- #
# Lazy defaults are imported only when active checks are requested.
# --------------------------------------------------------------------------- #
def _default_dns_resolver(host: str) -> list[str]:
import dns.resolver # lazy
answers = dns.resolver.resolve(host, "A")
return [str(r) for r in answers]
def _default_port_scanner(host: str, ports: Sequence[int]) -> list[int]:
import nmap # lazy
nm = nmap.PortScanner()
port_arg = ",".join(str(p) for p in ports)
# TCP connect scanning works without raw-socket privileges; -Pn skips ICMP discovery.
nm.scan(hosts=host, arguments=f"-sT -Pn -p {port_arg} --max-retries 1 --host-timeout 20s")
open_ports: set[int] = set()
# nmap indexes results by resolved address, so iterate over every returned host.
for h in nm.all_hosts():
try:
for port in nm[h].all_tcp():
if nm[h]["tcp"][port]["state"] == "open":
open_ports.add(int(port))
except Exception:
continue
return sorted(open_ports)
def _default_tls_scanner(host: str) -> dict:
import socket
import ssl
ctx = ssl.create_default_context()
try:
with socket.create_connection((host, 443), timeout=5) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as ssock:
cert = ssock.getpeercert()
cipher = ssock.cipher()
return {
"tls_version": ssock.version(),
"cipher": cipher[0] if cipher else None,
"certificate": "Present" if cert else "Not Present",
}
except Exception:
return {}