#!/usr/bin/env python3
"""
Lab 5.2 - Control-Matrix Crosswalk Helper
Chapter 5: Cybersecurity Compliance and Management

This script loads a NIST CSF 2.0 crosswalk CSV (either the seed excerpt
included in this pack or the full export from the NIST CSF 2.0 Reference Tool)
and provides three analysis functions matching the lab steps:

  Step 1: View all rows for the five controls you chose.
  Step 2: Show the multi-framework mapping for MFA (PR.AA-03).
  Step 3: Reverse lookup - find everything that maps to NIS2 Article 21 Measure 10.
  Step 4: Summary answers for the one-page deliverable.

Usage (from within the container, working dir /workdir):
  python crosswalk_analysis.py

To use the full NIST CSF 2.0 Reference Tool export instead of the seed file:
  1. Download the CSV from https://csrc.nist.gov/Projects/cybersecurity-framework/Filters
  2. Copy it to /workdir/ (via the mounted volume: ./crosswalk-tools/workdir/)
  3. Set CROSSWALK_FILE below to the filename.
"""

import sys
import pandas as pd
from tabulate import tabulate

# ── Configuration ────────────────────────────────────────────────────────────

# Use the seed CSV included in this pack (representative excerpt).
# Switch to the full NIST export file name if you have downloaded it.
CROSSWALK_FILE = "csf2_crosswalk_seed.csv"

# The five controls the lab asks you to start with
FIVE_CONTROLS = [
    "PR.AA-03",   # MFA / authentication (the lab's explicit starting point)
    "PR.DS-01",   # Data-at-rest confidentiality/integrity/availability
    "PR.DS-02",   # Data-in-transit
    "ID.RA-01",   # Vulnerability management
    "RS.MA-01",   # Incident response plan execution
]

# NIS2 Article 21 measure 10 text fragment (as it appears in the seed)
NIS2_MEASURE_10_FRAGMENT = "10"


# ── Load ──────────────────────────────────────────────────────────────────────

def load_crosswalk(path: str) -> pd.DataFrame:
    df = pd.read_csv(path)
    df.columns = df.columns.str.strip()
    # Fill NaN with empty string for display
    df = df.fillna("")
    return df


# ── Analysis functions ────────────────────────────────────────────────────────

def show_five_controls(df: pd.DataFrame) -> None:
    print("\n" + "=" * 72)
    print("STEP 1: Five-control crosswalk (all framework columns)")
    print("=" * 72)
    subset = df[df["csf_subcategory_code"].isin(FIVE_CONTROLS)]
    if subset.empty:
        print("None of the five target controls found in this CSV.")
        print("(If using the seed file this is unexpected; check column names.)")
        return
    display_cols = [
        "csf_subcategory_code",
        "csf_subcategory_title",
        "iso27001_annex_a",
        "nis2_article21_measure",
        "gdpr_article32_element",
    ]
    print(tabulate(subset[display_cols], headers="keys", tablefmt="grid", showindex=False))
    missing = [c for c in FIVE_CONTROLS if c not in subset["csf_subcategory_code"].values]
    if missing:
        print(f"\nNOTE: These controls are not in this extract: {missing}")
        print("Download the full NIST CSF 2.0 Reference Tool export for complete coverage.")


def show_mfa_mapping(df: pd.DataFrame) -> None:
    print("\n" + "=" * 72)
    print("STEP 2: MFA multi-framework mapping (PR.AA-03)")
    print("=" * 72)
    row = df[df["csf_subcategory_code"] == "PR.AA-03"]
    if row.empty:
        print("PR.AA-03 not found in this CSV.")
        return
    for _, r in row.iterrows():
        print(f"  CSF Function  : {r['csf_function']}")
        print(f"  Category      : {r['csf_category']}")
        print(f"  Subcategory   : {r['csf_subcategory_code']} - {r['csf_subcategory_title']}")
        print(f"  ISO 27001 A.  : {r['iso27001_annex_a']}")
        print(f"  NIS2 Art.21   : {r['nis2_article21_measure']}")
        print(f"  GDPR Art.32   : {r['gdpr_article32_element']}")
    frameworks_satisfied = sum([
        bool(row.iloc[0]["iso27001_annex_a"]),
        bool(row.iloc[0]["nis2_article21_measure"]),
        bool(row.iloc[0]["gdpr_article32_element"]),
    ])
    print(f"\n  -> This single control maps into {frameworks_satisfied} framework columns simultaneously.")
    print("     (The full NIST Reference Tool export will show additional SP 800-53 / CIS Controls mappings.)")


def show_nis2_measure10_reverse(df: pd.DataFrame) -> None:
    print("\n" + "=" * 72)
    print("STEP 3: Reverse lookup - NIS2 Article 21 Measure 10 (MFA / secured comms)")
    print("=" * 72)
    # Match rows where the NIS2 column contains "10" or "multi-factor"
    mask = (
        df["nis2_article21_measure"].str.contains("10", na=False)
        | df["nis2_article21_measure"].str.lower().str.contains("multi-factor", na=False)
    )
    hits = df[mask]
    if hits.empty:
        print("No rows match Measure 10 in this extract.")
        return
    display_cols = [
        "csf_function",
        "csf_subcategory_code",
        "csf_subcategory_title",
        "iso27001_annex_a",
        "gdpr_article32_element",
    ]
    print(tabulate(hits[display_cols], headers="keys", tablefmt="grid", showindex=False))
    print(f"\n  -> Implementing a well-scoped MFA programme satisfies {len(hits)} subcategories")
    print("     drawn from multiple frameworks with a single implementation.")


def show_summary(df: pd.DataFrame) -> None:
    print("\n" + "=" * 72)
    print("STEP 4: One-page summary prompts")
    print("=" * 72)

    print("\n(a) Which framework provides the most specific implementation guidance?")
    print("    ISO/IEC 27002:2022 Annex A controls typically provide the most specific")
    print("    implementation guidance (each control has a corresponding 27002 guidance section).")
    print("    NIS2 Art.21 names the measure category but not the technical implementation.")
    print("    GDPR Art.32 names properties (confidentiality/integrity/availability) but")
    print("    deliberately avoids prescribing specific controls ('appropriate to the risk').")

    print("\n(b) Which obligation would remain unsatisfied with only ISO 27001 controls?")
    nis2_only = df[
        (df["nis2_article21_measure"] != "") & (df["iso27001_annex_a"] == "")
    ]
    if not nis2_only.empty:
        print("    These NIS2 measures have no direct ISO 27001 mapping in this extract:")
        for _, r in nis2_only.iterrows():
            print(f"      {r['csf_subcategory_code']}: {r['nis2_article21_measure']}")
    else:
        print("    In this seed extract, all NIS2-mapped rows also have an ISO 27001 mapping.")
        print("    In the full NIST export, check for NIS2-specific measures that ISO does not")
        print("    explicitly cover (e.g. the specific 72-hour reporting clock of Art.23).")

    print("\n(c) What the crosswalk reveals about compliance vs. security:")
    print("    The same underlying control (e.g. MFA) satisfies multiple frameworks simultaneously.")
    print("    This shows that compliance regimes are largely outcome-based: they describe *what*")
    print("    properties your systems must have, not *which product* to buy. Mapping the union")
    print("    of obligations is not five separate projects; it is one control set with multiple labels.")
    print("    The crosswalk exposes gaps (an empty cell) that reveal where a framework has an")
    print("    obligation with no corresponding control implemented, which is a genuine risk finding,")
    print("    not just an administrative gap.")

    print()


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    try:
        df = load_crosswalk(CROSSWALK_FILE)
    except FileNotFoundError:
        print(f"ERROR: '{CROSSWALK_FILE}' not found in /workdir.")
        print("Make sure you have either the seed CSV or the full NIST export in /workdir/.")
        sys.exit(1)

    print(f"\nLoaded {len(df)} rows from '{CROSSWALK_FILE}'.")
    print(f"Columns: {list(df.columns)}")

    show_five_controls(df)
    show_mfa_mapping(df)
    show_nis2_measure10_reverse(df)
    show_summary(df)

    print("=" * 72)
    print("Lab 5.2 complete. Produce your one-page summary from the STEP 4 output above.")
    print("=" * 72)


if __name__ == "__main__":
    main()
