Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| import uuid | |
| import hashlib | |
| import hmac | |
| import secrets | |
| import base64 | |
| import subprocess | |
| from typing import * | |
| import os | |
| import sys | |
| import re | |
| import json | |
| import time | |
| import urllib3 | |
| import requests | |
| import socket | |
| import logging | |
| import threading | |
| import concurrent.futures | |
| import ipaddress | |
| import ssl | |
| from urllib.parse import urlparse, urljoin, urlencode, quote | |
| from collections import defaultdict | |
| from bs4 import BeautifulSoup | |
| from datetime import datetime, timezone | |
| import statistics | |
| """ | |
| anomaly.py — Statistical Anomaly Detectors for Timing & Size Analysis | |
| ====================================================================== | |
| Used by scanners to detect blind injection via timing differentials. | |
| Improvements (June 2026): | |
| ENH: Minimum 5-sample guard before trusting baseline results. | |
| ENH: build_baseline() accepts proper callable signature. | |
| ENH: z_score() is safe against zero stdev and insufficient samples. | |
| ENH: Added AdaptiveThreshold for dynamic z-score tuning. | |
| """ | |
| class AnomalyDetector: | |
| """Base statistical detector. Collects numeric samples and detects outliers.""" | |
| MIN_BASELINE_SAMPLES = 5 # Require at least this many samples for reliable stats | |
| def __init__(self, baseline_samples: list[float] | None = None): | |
| self._baseline = list(baseline_samples) if baseline_samples else [] | |
| def record(self, value: float) -> None: | |
| self._baseline.append(value) | |
| def mean(self) -> float: | |
| return statistics.mean(self._baseline) if self._baseline else 0.0 | |
| def stdev(self) -> float: | |
| if len(self._baseline) >= 2: | |
| return statistics.stdev(self._baseline) | |
| return 0.0 | |
| def has_baseline(self) -> bool: | |
| return len(self._baseline) >= self.MIN_BASELINE_SAMPLES | |
| def z_score(self, value: float) -> float: | |
| """ | |
| Return z-score of `value` relative to baseline. | |
| Returns 0.0 if stdev is zero or baseline is insufficient. | |
| """ | |
| sd = self.stdev | |
| if sd == 0 or not self._baseline: | |
| return 0.0 | |
| return (value - self.mean) / sd | |
| def is_anomalous(self, value: float, threshold: float = 2.5) -> bool: | |
| """ | |
| Returns True only if we have enough baseline AND the z-score exceeds threshold. | |
| Guard against false positives from insufficient data. | |
| """ | |
| if not self.has_baseline: | |
| return False | |
| return abs(self.z_score(value)) >= threshold | |
| def reset(self) -> None: | |
| """Clear all recorded samples.""" | |
| self._baseline.clear() | |
| class TimingAnomalyDetector(AnomalyDetector): | |
| """ | |
| Specialized detector for HTTP response timing analysis. | |
| Used for blind SQLi, CMDi, SSTI, SSRF timing-based detection. | |
| """ | |
| def __init__(self, baseline_samples: list[float] | None = None): | |
| super().__init__(baseline_samples) | |
| self._timing_records: list[tuple[str, float, str]] = [] | |
| def record_timing(self, label: str, elapsed: float, payload: str = "") -> None: | |
| self.record(elapsed) | |
| self._timing_records.append((label, elapsed, payload)) | |
| def build_baseline( | |
| self, | |
| request_fn, | |
| url: str, | |
| n: int = 5, | |
| headers: dict | None = None, | |
| method: str = "GET", | |
| ) -> None: | |
| """ | |
| Build timing baseline by making `n` requests to `url`. | |
| FIX: Accepts request_fn with signature (url, method, data, headers, timeout). | |
| Passes all positional args to avoid keyword-mismatch errors when | |
| callers pass `self._make_request` directly. | |
| """ | |
| for _ in range(n): | |
| t0 = time.monotonic() | |
| try: | |
| # Use positional args to match BaseScanner._make_request signature: | |
| # (url, method="GET", data=None, headers=None, timeout=8) | |
| request_fn(url, method, None, headers or {}, 8) | |
| except Exception: | |
| pass # Network errors are expected during baseline | |
| self.record(time.monotonic() - t0) | |
| def test_payload( | |
| self, | |
| label: str, | |
| elapsed: float, | |
| payload: str = "", | |
| z_threshold: float = 3.0, | |
| ) -> bool: | |
| """ | |
| Record a timed payload request and test if it's anomalous. | |
| Returns True if response time is statistically abnormal. | |
| """ | |
| self.record_timing(label, elapsed, payload) | |
| return self.is_anomalous(elapsed, z_threshold) | |
| class SizeAnomalyDetector(AnomalyDetector): | |
| """ | |
| Specialized detector for HTTP response size analysis. | |
| Used for boolean-based blind injection (different sizes for true/false conditions). | |
| """ | |
| MIN_BASELINE_SAMPLES = 3 # Size detection can work with fewer samples | |
| def __init__(self, baseline_sizes: list[int] | None = None): | |
| sizes = [float(s) for s in (baseline_sizes or [])] | |
| super().__init__(sizes) | |
| def record_size(self, size: int) -> None: | |
| self.record(float(size)) | |
| def test_size(self, size: int, z_threshold: float = 2.5) -> bool: | |
| return self.is_anomalous(float(size), z_threshold) | |
| def seed_pair(self, true_len: int, false_len: int) -> None: | |
| """ | |
| Seed with true/false response sizes to initialize comparison. | |
| Adds both as baseline samples. | |
| """ | |
| self.record_size(true_len) | |
| self.record_size(false_len) | |
| def pair_differs(self, true_len: int, false_len: int, min_diff: int = 30) -> bool: | |
| """ | |
| Simple heuristic: return True if the two response sizes differ | |
| by at least `min_diff` bytes — used before enough baseline exists. | |
| """ | |
| return abs(true_len - false_len) >= min_diff | |