#!/usr/bin/env python3
"""
Lab 4 - Data classification and DLP policy design tool.

Reads the synthetic_customers.csv produced by synthetic_dataset.py,
applies the column classification scheme described in the book
(ISO/IEC 27001:2022 A.5.12/A.5.13), runs the DLP detectors
(regex, Luhn, label), and prints:
  1. The classified data inventory table
  2. The DLP detection results on each row's sensitive fields
  3. The DLP action grid (sensitivity x confidence -> action)

All data must be synthetic. Never run against real personal data
outside a sanctioned environment you are authorised to use.

Usage:
    python classify_and_policy.py [--csv synthetic_customers.csv]
"""

import csv
import re
import argparse
from dataclasses import dataclass
from typing import Literal

# ---------------------------------------------------------------------------
# Classification scheme (ISO/IEC 27001:2022 A.5.12)
# ---------------------------------------------------------------------------

Classification = Literal["Public", "Internal", "Confidential", "Restricted"]

COLUMN_CLASSIFICATION: dict[str, Classification] = {
    "customer_id":           "Internal",
    "full_name":             "Confidential",
    "email":                 "Confidential",
    "date_of_birth":         "Restricted",
    "postal_address":        "Confidential",
    "payment_card_number":   "Restricted",
    "passport_number":       "Restricted",
    "order_total":           "Internal",
    "marketing_opt_in":      "Internal",
    "internal_notes":        "Internal",
}

RETENTION_POLICY: dict[str, str] = {
    "customer_id":           "Duration of customer relationship + 7 years (legal hold)",
    "full_name":             "Duration of customer relationship + 7 years",
    "email":                 "Duration of customer relationship + 3 years",
    "date_of_birth":         "Duration of customer relationship only",
    "postal_address":        "Duration of customer relationship + 3 years",
    "payment_card_number":   "90 days post-transaction (PCI DSS 3.4 scope)",
    "passport_number":       "Verification only - DELETE after KYC check passes",
    "order_total":           "7 years (financial records)",
    "marketing_opt_in":      "Until opt-out or relationship end",
    "internal_notes":        "Duration of customer relationship",
}

PROCESSING_PURPOSE: dict[str, str] = {
    "customer_id":           "Unique identifier for order management",
    "full_name":             "Order fulfilment and customer correspondence",
    "email":                 "Order notifications and marketing (if opted in)",
    "date_of_birth":         "Age verification for restricted products",
    "postal_address":        "Delivery and billing",
    "payment_card_number":   "Payment processing",
    "passport_number":       "KYC identity verification (once only)",
    "order_total":           "Financial reporting and fraud detection",
    "marketing_opt_in":      "Marketing consent management",
    "internal_notes":        "Internal CRM - customer service notes",
}


# ---------------------------------------------------------------------------
# Luhn check (mirrors the book's description)
# ---------------------------------------------------------------------------

def luhn_valid(pan: str) -> bool:
    """Return True if pan passes the Luhn (mod-10) check."""
    digits = [int(c) for c in pan if c.isdigit()]
    if len(digits) < 13:
        return False
    total = 0
    for i, d in enumerate(reversed(digits)):
        if i % 2 == 1:
            doubled = d * 2
            total += doubled - 9 if doubled > 9 else doubled
        else:
            total += d
    return total % 10 == 0


# ---------------------------------------------------------------------------
# DLP detectors
# ---------------------------------------------------------------------------

# Pattern: 13-19 digits (covers Visa/MC/Amex/Discover)
PAN_REGEX = re.compile(r"\b\d{13,19}\b")

# Plausible passport pattern: 2 letters + 6-9 digits
PASSPORT_REGEX = re.compile(r"\b[A-Z]{2}\d{6,9}\b")

# ISO 8601 date pattern used by the synthetic generator
DOB_REGEX = re.compile(r"\b\d{4}-\d{2}-\d{2}\b")


@dataclass
class DLPFinding:
    column: str
    value: str
    detector: str
    confidence: Literal["high", "medium", "low"]
    classification: Classification


def detect_dlp(row: dict) -> list[DLPFinding]:
    """Run DLP detectors over a single data row. Return a list of findings."""
    findings: list[DLPFinding] = []

    pan = row.get("payment_card_number", "")
    if PAN_REGEX.fullmatch(pan):
        if luhn_valid(pan):
            findings.append(DLPFinding(
                column="payment_card_number",
                value=f"{pan[:6]}******{pan[-4:]}",
                detector="regex+Luhn",
                confidence="high",
                classification="Restricted",
            ))
        else:
            findings.append(DLPFinding(
                column="payment_card_number",
                value=pan[:6] + "...",
                detector="regex-only (Luhn fail)",
                confidence="low",
                classification="Restricted",
            ))

    passport = row.get("passport_number", "")
    if PASSPORT_REGEX.fullmatch(passport):
        findings.append(DLPFinding(
            column="passport_number",
            value=passport[:3] + "****",
            detector="regex (passport pattern)",
            confidence="medium",
            classification="Restricted",
        ))

    dob = row.get("date_of_birth", "")
    if DOB_REGEX.fullmatch(dob):
        findings.append(DLPFinding(
            column="date_of_birth",
            value=dob,
            detector="regex (ISO date)",
            confidence="medium",
            classification="Restricted",
        ))

    # Label-based detection for Confidential columns
    for col in ("full_name", "email", "postal_address"):
        val = row.get(col, "")
        if val:
            findings.append(DLPFinding(
                column=col,
                value=val[:20] + ("..." if len(val) > 20 else ""),
                detector="classification-label (Confidential)",
                confidence="high",
                classification="Confidential",
            ))

    return findings


# ---------------------------------------------------------------------------
# DLP action grid
# ---------------------------------------------------------------------------

ACTION_GRID: dict[tuple[Classification, str], str] = {
    ("Restricted",   "high"):   "BLOCK",
    ("Restricted",   "medium"): "QUARANTINE",
    ("Restricted",   "low"):    "LOG",
    ("Confidential", "high"):   "QUARANTINE",
    ("Confidential", "medium"): "QUARANTINE",
    ("Confidential", "low"):    "LOG",
    ("Internal",     "high"):   "LOG",
    ("Internal",     "medium"): "LOG",
    ("Internal",     "low"):    "LOG",
    ("Public",       "high"):   "LOG",
    ("Public",       "medium"): "LOG",
    ("Public",       "low"):    "LOG",
}


def recommended_action(finding: DLPFinding) -> str:
    return ACTION_GRID.get((finding.classification, finding.confidence), "LOG")


# ---------------------------------------------------------------------------
# Report printers
# ---------------------------------------------------------------------------

def print_inventory() -> None:
    print("\n" + "=" * 80)
    print("DATA INVENTORY AND CLASSIFICATION (ISO/IEC 27001:2022 A.5.12 / A.5.13)")
    print("=" * 80)
    header = f"{'Column':<26} {'Class':<14} {'Purpose':<42} {'Retention'}"
    print(header)
    print("-" * 120)
    for col, cls in COLUMN_CLASSIFICATION.items():
        purpose = PROCESSING_PURPOSE[col]
        retention = RETENTION_POLICY[col]
        # Flag potential over-retention
        flag = " [!]" if col == "passport_number" else ""
        print(f"{col:<26} {cls:<14} {purpose[:42]:<42} {retention}{flag}")
    print()
    print("[!] passport_number: Optus-style latent risk - retained beyond stated purpose.")
    print("    Recommendation: delete after KYC check passes; do not store indefinitely.")


def print_dlp_findings(rows: list[dict], max_rows: int = 5) -> None:
    print("\n" + "=" * 80)
    print(f"DLP DETECTION RESULTS (first {max_rows} rows)")
    print("=" * 80)
    for row in rows[:max_rows]:
        print(f"\n  customer_id={row['customer_id']}")
        findings = detect_dlp(row)
        for f in findings:
            action = recommended_action(f)
            print(
                f"    [{f.classification:<12}] [{f.confidence:<6}] "
                f"col={f.column:<24} detector={f.detector:<35} "
                f"action={action}"
            )


def print_action_grid() -> None:
    print("\n" + "=" * 80)
    print("DLP ACTION GRID  (sensitivity x confidence -> action)")
    print("Channels: network egress, endpoint USB/paste, cloud upload")
    print("=" * 80)
    print(f"{'Classification':<16} {'Confidence':<10} {'Action':<14} {'Rationale'}")
    print("-" * 80)
    explanations = {
        ("Restricted",   "high"):   "High-confidence Restricted to unsanctioned dest -> block immediately",
        ("Restricted",   "medium"): "Medium confidence -> human review before release",
        ("Restricted",   "low"):    "Low confidence -> log for tuning; block would cause alert fatigue",
        ("Confidential", "high"):   "Label-matched Confidential -> quarantine; most are legitimate",
        ("Confidential", "medium"): "Medium confidence Confidential -> quarantine for review",
        ("Confidential", "low"):    "Low confidence -> log only",
        ("Internal",     "high"):   "Internal data - log for audit trail; no block",
        ("Internal",     "medium"): "Internal data - log",
        ("Internal",     "low"):    "Internal data - log",
        ("Public",       "high"):   "Public data - log",
        ("Public",       "medium"): "Public data - log",
        ("Public",       "low"):    "Public data - log",
    }
    for (cls, conf), action in ACTION_GRID.items():
        print(f"{cls:<16} {conf:<10} {action:<14} {explanations[(cls, conf)]}")
    print()
    print("Note: start ALL rules in MONITOR (log) mode first. Measure false-positive")
    print("rate for 2-4 weeks, then promote high-confidence Restricted rules to BLOCK.")
    print("A DLP policy that blocks legitimate work gets disabled, which is its own")
    print("data-loss event.")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(description="Lab 4: classify dataset and design DLP policy.")
    parser.add_argument("--csv", default="synthetic_customers.csv", help="Input CSV (default: synthetic_customers.csv)")
    args = parser.parse_args()

    try:
        with open(args.csv, newline="", encoding="utf-8") as f:
            rows = list(csv.DictReader(f))
    except FileNotFoundError:
        print(f"[!] {args.csv} not found. Run synthetic_dataset.py first.")
        raise SystemExit(1)

    print(f"[+] Loaded {len(rows)} rows from {args.csv}")

    print_inventory()
    print_dlp_findings(rows, max_rows=5)
    print_action_grid()

    print("\n[+] Lab 4 complete.")
    print("[+] Deliverable: classified inventory table + detection rules + action grid above.")
    print("[+] All data processed was synthetic; no real PII was involved.")


if __name__ == "__main__":
    main()
