FastAPI + Kali tools UI for authorized scans of levkin.ca / LAN targets. Gitignore local data/.env; add pytest + gitleaks CI.
242 lines
7.4 KiB
Python
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),
|
|
},
|
|
]
|