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
+237
View File
@@ -0,0 +1,237 @@
"""Extract endpoint and crackable hash artifacts from a capture context.
Extraction is independent of Django, pyshark, and I/O. Active endpoint probing
and hash cracking remain isolated in ``analyzer.enrich``.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Optional
from .context import CaptureContext
# --------------------------------------------------------------------------- #
# Artifact data models
# --------------------------------------------------------------------------- #
@dataclass
class UrlArtifact:
"""A domain, host, or URL observed in the capture."""
value: str
kind: str = "unknown" # "dns" | "sni" | "http_host" | "payload"
src_ip: Optional[str] = None
dst_ip: Optional[str] = None
timestamp: Optional[float] = None
packet_number: Optional[int] = None
# Passive observations are preferred over active probing results.
observed_ports: set[int] = field(default_factory=set)
observed_tls: set[str] = field(default_factory=set)
def to_dict(self) -> dict:
return {
"value": self.value, "kind": self.kind,
"src_ip": self.src_ip, "dst_ip": self.dst_ip,
"timestamp": self.timestamp, "packet_number": self.packet_number,
"observed_ports": sorted(self.observed_ports),
"observed_tls": sorted(self.observed_tls),
}
@dataclass
class HashArtifact:
"""A hash or crackable authentication artifact from the capture."""
value: str # Raw hash or complete hashcat line
hashcat_mode: Optional[int] = None
hash_format: Optional[str] = None # Human-readable format name
kind: str = "raw_hex" # "raw_hex" | "netntlmv2" | "krb5asrep" | "krb5tgs" | "wpa" | "digest"
account: Optional[str] = None
src_ip: Optional[str] = None
dst_ip: Optional[str] = None
timestamp: Optional[float] = None
packet_number: Optional[int] = None
def to_dict(self) -> dict:
return {
"value": self.value, "hashcat_mode": self.hashcat_mode,
"hash_format": self.hash_format, "kind": self.kind,
"account": self.account, "src_ip": self.src_ip, "dst_ip": self.dst_ip,
"timestamp": self.timestamp, "packet_number": self.packet_number,
}
# --------------------------------------------------------------------------- #
# Hashcat mode detection
# --------------------------------------------------------------------------- #
# Hex length to hashcat mode and format name.
_LEN_MODES = {
32: (0, "md5"),
40: (100, "sha1"),
56: (300, "sha224"),
64: (1400, "sha256"),
96: (6100, "sha384"),
128: (1700, "sha512"),
}
# Structured format name to hashcat mode.
_FORMAT_MODES = {
"netntlmv1": 5500,
"netntlmv2": 5600,
"krb5asrep": 18200,
"krb5tgs": 13100,
"wpa": 22000,
"eapol": 22000,
}
_NETNTLMV2_RE = re.compile(r"^[^:]+::[^:]*:[0-9a-fA-F]{16}:[0-9a-fA-F]{32}:[0-9a-fA-F]+$")
_NETNTLMV1_RE = re.compile(r"^[^:]+::[^:]*:[0-9a-fA-F]{48}:[0-9a-fA-F]{48}:[0-9a-fA-F]{16}$")
def identify_hash_mode(value: str, hint_format: Optional[str] = None):
"""Return a hashcat mode and format using structure before hex length."""
v = (value or "").strip()
if not v:
return None, None
if hint_format:
base = str(hint_format).split("(")[0].strip().lower()
if base in _FORMAT_MODES:
return _FORMAT_MODES[base], hint_format
low = v.lower()
if low.startswith("$krb5asrep$"):
return 18200, "krb5asrep"
if low.startswith("$krb5tgs$"):
return 13100, "krb5tgs"
if low.startswith("$wpapsk$") or low.startswith("wpa*") or low.startswith("$wpa$"):
return 22000, "wpa"
if _NETNTLMV2_RE.match(v):
return 5600, "NetNTLMv2"
if _NETNTLMV1_RE.match(v):
return 5500, "NetNTLMv1"
if all(c in "0123456789abcdefABCDEF" for c in v) and len(v) in _LEN_MODES:
return _LEN_MODES[len(v)]
return None, None
def _kind_from_mode(mode: Optional[int]) -> str:
return {
5500: "netntlmv1", 5600: "netntlmv2",
18200: "krb5asrep", 13100: "krb5tgs", 22000: "wpa",
}.get(mode, "raw_hex")
# --------------------------------------------------------------------------- #
# Endpoint extraction
# --------------------------------------------------------------------------- #
_URL_RE = re.compile(
r"\b(?:"
r"(?:https?://|www\.)(?:[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})" # URL or www host
r"|"
r"(?:[a-zA-Z0-9-]+\.){2,}[a-zA-Z]{2,6}" # Bare multi-label domain
r"|"
r"https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" # URL with IPv4 host
r")"
r"(?:/[a-zA-Z0-9._%/-]*)?\b"
)
def _normalize_host(value: Optional[str]) -> Optional[str]:
if not value:
return None
v = str(value).strip().lower()
v = v.rstrip(".")
return v or None
def extract_urls(ctx: CaptureContext) -> list[UrlArtifact]:
"""Collect endpoints and attach passively observed ports and TLS versions."""
by_value: dict[str, UrlArtifact] = {}
def add(value, kind, **kw) -> Optional[UrlArtifact]:
v = _normalize_host(value)
if not v:
return None
art = by_value.get(v)
if art is None:
art = UrlArtifact(value=v, kind=kind, **kw)
by_value[v] = art
return art
for q in ctx.dns_queries:
add(q.query_name, "dns", src_ip=q.src_ip, dst_ip=q.dst_ip,
timestamp=q.timestamp, packet_number=q.packet_number)
for t in ctx.tls_sessions:
if t.sni:
art = add(t.sni, "sni", src_ip=t.src_ip, dst_ip=t.dst_ip,
timestamp=t.timestamp, packet_number=t.packet_number)
if art:
if t.dst_port:
art.observed_ports.add(t.dst_port)
if t.version:
art.observed_tls.add(t.version)
for r in ctx.http_requests:
if r.host:
art = add(r.host, "http_host", src_ip=r.src_ip, dst_ip=r.dst_ip,
timestamp=r.timestamp, packet_number=r.packet_number)
if art and r.dst_port:
art.observed_ports.add(r.dst_port)
for p in ctx.text_payloads:
text = p.get("text") or ""
for m in _URL_RE.findall(text):
add(m, "payload", src_ip=p.get("src_ip"), dst_ip=p.get("dst_ip"),
timestamp=p.get("timestamp"), packet_number=p.get("packet_number"))
return list(by_value.values())
# --------------------------------------------------------------------------- #
# Hash extraction
# --------------------------------------------------------------------------- #
_HASH_HEX_RE = re.compile(r"\b[a-fA-F0-9]{32,64}\b")
def extract_hashes(ctx: CaptureContext) -> list[HashArtifact]:
"""Collect structured authentication material and raw hexadecimal hashes."""
out: list[HashArtifact] = []
seen: set[str] = set()
def add(value, *, mode, fmt, kind, **kw) -> None:
if not value or value in seen:
return
seen.add(value)
out.append(HashArtifact(value=value, hashcat_mode=mode, hash_format=fmt,
kind=kind, **kw))
# Structured material already reconstructed by the packet extractor.
for am in ctx.auth_material:
if getattr(am, "hashcat", None):
mode = am.hashcat_mode
fmt = am.hash_format
if mode is None:
mode, fmt = identify_hash_mode(am.hashcat, hint_format=fmt)
add(am.hashcat, mode=mode, fmt=fmt, kind=_kind_from_mode(mode),
account=am.account, src_ip=am.src_ip, dst_ip=am.dst_ip,
timestamp=am.timestamp, packet_number=am.packet_number)
# Raw 32–64 character hex values use a mode only when the length is known.
for p in ctx.text_payloads:
text = p.get("text") or ""
for m in _HASH_HEX_RE.findall(text):
mode, fmt = identify_hash_mode(m)
add(m.lower(), mode=mode, fmt=fmt, kind="raw_hex",
src_ip=p.get("src_ip"), dst_ip=p.get("dst_ip"),
timestamp=p.get("timestamp"), packet_number=p.get("packet_number"))
return out