Files
talos/backend/app/profiles.py
T
ilia 12a6e2b6bc
CI / skip-ci-check (push) Successful in 8s
CI / secret-scan (push) Successful in 7s
CI / python-ci (push) Failing after 19s
Initial commit: Talos edge-facing scan orchestrator.
FastAPI + Kali tools UI for authorized scans of levkin.ca / LAN targets.
Gitignore local data/.env; add pytest + gitleaks CI.
2026-07-12 11:46:54 -04:00

242 lines
7.4 KiB
Python

from dataclasses import dataclass
from typing import Callable
import shlex
import re
from app.models import ScanOptions
ProfileName = str
@dataclass(frozen=True)
class PhaseDef:
name: str
tool: str
command_template: str
requires: tuple[str, ...] = ()
success_exit_codes: tuple[int, ...] = (0,)
timeout_seconds: int | None = None
retries: int = 0
retry_delay_seconds: float = 0.0
llm_analyze: bool = True
def should_run(self, options: ScanOptions) -> bool:
if "include_web" in self.requires and not options.include_web:
return False
if "include_brute" in self.requires and not options.include_brute:
return False
if "include_ssl" in self.requires and not options.include_ssl:
return False
return True
def _target_url(target: str) -> str:
if target.startswith("http://") or target.startswith("https://"):
return target
return f"http://{target}"
def _https_target_url(target: str) -> str:
if target.startswith("https://"):
return target
if target.startswith("http://"):
return "https://" + target.removeprefix("http://")
return f"https://{target}"
def _root_domain(target: str) -> str:
# Best-effort heuristic for homelab/public domains like levkin.ca.
# If target is an IP, just return it.
host = target
if host.startswith("http://") or host.startswith("https://"):
host = re.sub(r"^https?://", "", host)
host = host.split("/")[0]
if re.fullmatch(r"\d{1,3}(\.\d{1,3}){3}", host):
return host
parts = [p for p in host.split(".") if p]
if len(parts) >= 2:
return ".".join(parts[-2:])
return host
def _ctx(target: str, options: ScanOptions, scan_id: str, open_ports: str = "80,443") -> dict[str, str]:
t = shlex.quote(target)
return {
"target": t,
"target_url": shlex.quote(_target_url(target)),
"https_target_url": shlex.quote(_https_target_url(target)),
"root_domain": shlex.quote(_root_domain(target)),
"port_range": shlex.quote(options.port_range),
"open_ports": shlex.quote(open_ports),
"scan_id": shlex.quote(scan_id),
"users": shlex.quote("/usr/share/seclists/Usernames/top-usernames-shortlist.txt"),
"pwds": shlex.quote("/usr/share/wordlists/rockyou.txt"),
}
def render_command(template: str, ctx: dict[str, str]) -> list[str]:
rendered = template.format(**ctx)
return shlex.split(rendered)
PASSIVE: list[PhaseDef] = [
PhaseDef("host_alive", "nmap", "nmap -sn {target}"),
PhaseDef(
"port_scan",
"nmap",
"nmap -sS -p {port_range} --open -T4 -oX - {target}",
),
PhaseDef(
"service_detect",
"nmap",
"nmap -sV -sC -p {open_ports} -oX - {target}",
llm_analyze=True,
),
PhaseDef(
"ssl_check",
"testssl",
"testssl.sh --jsonfile-pretty - {final_https_url}",
requires=("include_ssl",),
success_exit_codes=(0, 1),
timeout_seconds=600,
retries=1,
retry_delay_seconds=2.0,
),
]
STANDARD_EXTRA: list[PhaseDef] = [
PhaseDef("web_fingerprint", "whatweb", "whatweb --log-json=- {final_url}", requires=("include_web",)),
PhaseDef(
"http_headers",
"curl",
"curl -sI -L {final_url}",
requires=("include_web",),
llm_analyze=False,
),
PhaseDef(
"dir_enum",
"gobuster",
"/bin/sh -lc \"set -e; "
"url={final_url}; "
"wl=''; "
"for f in "
"/usr/share/wordlists/dirb/common.txt "
"/usr/share/wordlists/dirb/big.txt "
"/usr/share/seclists/Discovery/Web-Content/common.txt "
"/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt "
"; do "
"if [ -f \\\"$f\\\" ]; then wl=\\\"$f\\\"; break; fi; "
"done; "
"if [ -z \\\"$wl\\\" ]; then "
"echo 'ERROR: no wordlist found (install dirb/seclists or mount one)'; "
"exit 2; "
"fi; "
"echo 'Using wordlist:' $wl; "
"gobuster dir -u \\\"$url\\\" -w \\\"$wl\\\" -o - -q -r --random-agent --delay 100ms -t 10\"",
requires=("include_web",),
timeout_seconds=600,
retries=1,
retry_delay_seconds=2.0,
),
PhaseDef(
"web_misconfig",
"curl",
"/bin/sh -lc \"set -e; "
"base={final_https_url}; "
"echo 'Base:' $base; "
"echo; "
"for p in /.git/ /.git/config /.env /security.txt /.well-known/security.txt /robots.txt /sitemap.xml /sitemap_index.xml /backup /old /admin ; do "
"echo '== '\"'$p'\"; "
"curl -skI --max-time 10 \"$base$p\" | tr -d '\\r' || true; "
"echo; "
"done; "
"echo '== CSP header on /'; "
"curl -skI --max-time 10 \"$base/\" | tr -d '\\r' | (grep -i '^content-security-policy:' || echo '(no Content-Security-Policy header)')\"",
requires=("include_web",),
llm_analyze=False,
timeout_seconds=120,
retries=1,
retry_delay_seconds=1.0,
),
PhaseDef(
"nikto",
"nikto",
"nikto -h {final_url} -Format json -output -",
requires=("include_web",),
),
PhaseDef(
"mail_dns",
"dig",
"/bin/sh -lc \"set -e; "
"d={root_domain}; "
"echo 'Domain:' $d; "
"echo; "
"echo '== SPF (TXT)'; dig +short TXT $d || true; "
"echo; "
"echo '== DMARC (_dmarc)'; dig +short TXT _dmarc.$d || true; "
"echo; "
"echo '== DKIM (common selectors)'; "
"for s in default selector1 selector2 google mail ; do "
"echo '-- '\"'$s'\"; "
"dig +short TXT $s._domainkey.$d || true; "
"done\"",
llm_analyze=False,
timeout_seconds=60,
retries=1,
retry_delay_seconds=1.0,
),
]
REDTEAM_EXTRA: list[PhaseDef] = [
PhaseDef(
"sqlmap_probe",
"sqlmap",
"sqlmap -u {target_url} --batch --crawl=2 --level=2 --risk=1 --output-dir=/tmp/sqlmap-{scan_id}",
requires=("include_web",),
),
PhaseDef(
"ssh_brute",
"hydra",
"hydra -L {users} -P {pwds} -t 4 -f ssh://{target}",
requires=("include_brute",),
llm_analyze=False,
),
PhaseDef(
"searchsploit",
"searchsploit",
"searchsploit --json {target}",
llm_analyze=True,
),
]
_PROFILE_PHASES: dict[ProfileName, list[PhaseDef]] = {
"passive": PASSIVE,
"standard": PASSIVE + STANDARD_EXTRA,
"redteam": PASSIVE + STANDARD_EXTRA + REDTEAM_EXTRA,
}
def get_phase_defs(profile: ProfileName) -> list[PhaseDef]:
return list(_PROFILE_PHASES.get(profile, PASSIVE))
def get_profiles_payload() -> list[dict[str, object]]:
return [
{
"id": "passive",
"label": "Passive Recon",
"description": "Host discovery, port scan, service detection, optional SSL check.",
"phase_count": len(PASSIVE),
},
{
"id": "standard",
"label": "Standard Pentest",
"description": "Passive plus web fingerprinting, directory enum, and Nikto.",
"phase_count": len(PASSIVE) + len(STANDARD_EXTRA),
},
{
"id": "redteam",
"label": "Full Red Team",
"description": "Standard plus sqlmap, optional SSH brute, searchsploit.",
"phase_count": len(PASSIVE) + len(STANDARD_EXTRA) + len(REDTEAM_EXTRA),
},
]