Cybersecurity-Projects/PROJECTS/advanced/ai-threat-detection/backend/ml/data_loader.py

263 lines
6.5 KiB
Python

"""
©AngelaMos | 2026
data_loader.py
CSIC 2010 HTTP dataset loader with feature extraction for
ML training
parse_csic_file reads a CSIC dataset file, splits on HTTP
request line boundaries, and produces CSICRequest objects
(method, path, query_string, headers, body, label).
csic_to_parsed_entry converts CSICRequests to
ParsedLogEntrys with synthetic defaults (private IP,
random timestamp over 90 days, 200 status). load_csic_
dataset loads normal (label=0) and attack (label=1)
files, extracts 23 per-request features, zeros 12
windowed features, encodes to 35-dim vectors, and returns
(X, y) numpy arrays. load_csic_normal loads a single
normal-only file
Connects to:
core/features/extractor - extract_request_features
core/features/encoder - encode_for_inference
core/features/mappings - WINDOWED_FEATURE_NAMES
core/ingestion/parsers - ParsedLogEntry
cli/main - loaded in train command
"""
import logging
import random
import re
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
import numpy as np
from app.core.features.encoder import encode_for_inference
from app.core.features.extractor import extract_request_features
from app.core.features.mappings import WINDOWED_FEATURE_NAMES
from app.core.ingestion.parsers import ParsedLogEntry
logger = logging.getLogger(__name__)
_REQUEST_LINE_RE = re.compile(
r"^(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|TRACE)"
r"\s+(\S+)\s+(HTTP/\d\.\d)\s*$")
_DEFAULT_IP = "192.168.1.100"
_DEFAULT_UA = ("Mozilla/5.0 (compatible; Konqueror/3.5; Linux)"
" KHTML/3.5.8 (like Gecko)")
_BASE_TIMESTAMP = datetime(2010, 6, 1, tzinfo=UTC)
_TRAINING_WINDOW_DAYS = 90
def _synthetic_timestamp() -> datetime:
"""
Generate a realistic training timestamp spread over 90 days
"""
offset_secs = random.randint(0, _TRAINING_WINDOW_DAYS * 86400)
return _BASE_TIMESTAMP + timedelta(seconds=offset_secs)
@dataclass
class CSICRequest:
"""
Single HTTP request parsed from CSIC 2010 dataset format
"""
method: str
path: str
query_string: str
protocol: str
headers: dict[str, str]
body: str
label: int
def parse_csic_file(
path: Path,
label: int,
) -> list[CSICRequest]:
"""
Parse a CSIC 2010 dataset file into a list of CSICRequest objects
"""
text = path.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines()
blocks: list[list[str]] = []
current: list[str] = []
for line in lines:
match = _REQUEST_LINE_RE.match(line)
if match and current:
blocks.append(current)
current = [line]
elif match:
current = [line]
elif current:
current.append(line)
if current:
blocks.append(current)
results: list[CSICRequest] = []
for block in blocks:
req = _parse_request_block(block, label)
if req is not None:
results.append(req)
logger.info(
"Parsed %d requests from %s (label=%d)",
len(results),
path.name,
label,
)
return results
def _parse_request_block(
lines: list[str],
label: int,
) -> CSICRequest | None:
"""
Parse a single request block into a CSICRequest
"""
if not lines:
return None
match = _REQUEST_LINE_RE.match(lines[0])
if not match:
return None
method = match.group(1)
full_uri = match.group(2)
protocol = match.group(3)
if "?" in full_uri:
path, query_string = full_uri.split("?", 1)
else:
path = full_uri
query_string = ""
headers: dict[str, str] = {}
body_start = len(lines)
for i, line in enumerate(lines[1:], 1):
if not line.strip():
body_start = i + 1
break
if ": " in line:
key, value = line.split(": ", 1)
headers[key] = value
body_lines = [ln for ln in lines[body_start:] if ln.strip()]
body = "\n".join(body_lines)
return CSICRequest(
method=method,
path=path,
query_string=query_string,
protocol=protocol,
headers=headers,
body=body,
label=label,
)
def csic_to_parsed_entry(req: CSICRequest) -> ParsedLogEntry:
"""
Convert a CSICRequest to a ParsedLogEntry with synthesized defaults
for fields not present in the CSIC dataset
"""
ua = req.headers.get("User-Agent", _DEFAULT_UA)
query = req.query_string
if req.body:
query = (f"{query}&{req.body}" if query else req.body)
return ParsedLogEntry(
ip=_DEFAULT_IP,
timestamp=_synthetic_timestamp(),
method=req.method,
path=req.path,
query_string=query,
status_code=200,
response_size=0,
referer="",
user_agent=ua,
raw_line="",
)
def load_csic_dataset(
normal_path: Path,
attack_path: Path,
) -> tuple[np.ndarray, np.ndarray]:
"""
Load CSIC 2010 normal and attack files, extract features,
and return (X, y) arrays ready for model training
"""
normal_reqs = parse_csic_file(normal_path, label=0)
attack_reqs = parse_csic_file(attack_path, label=1)
all_reqs = normal_reqs + attack_reqs
vectors: list[list[float]] = []
labels: list[int] = []
for req in all_reqs:
entry = csic_to_parsed_entry(req)
features = extract_request_features(entry)
for name in WINDOWED_FEATURE_NAMES:
features[name] = 0.0
vector = encode_for_inference(features)
vectors.append(vector)
labels.append(req.label)
X = np.array(vectors, dtype=np.float32)
y = np.array(labels, dtype=np.int32)
logger.info(
"Dataset loaded: X=%s, y=%s (normal=%d, attack=%d)",
X.shape,
y.shape,
np.sum(y == 0),
np.sum(y == 1),
)
return X, y
def load_csic_normal(
path: Path,
) -> tuple[np.ndarray, np.ndarray]:
"""
Load a CSIC 2010 normal traffic file and return (X, y) arrays
with all labels set to 0
"""
reqs = parse_csic_file(path, label=0)
vectors: list[list[float]] = []
for req in reqs:
entry = csic_to_parsed_entry(req)
features = extract_request_features(entry)
for name in WINDOWED_FEATURE_NAMES:
features[name] = 0.0
vectors.append(encode_for_inference(features))
X = np.array(vectors, dtype=np.float32)
y = np.zeros(len(vectors), dtype=np.int32)
logger.info(
"Loaded %d normal samples from %s",
len(vectors),
path.name,
)
return X, y