CoolFace
Apppublic

mihir2007/Cyber-Risk

sourceHugging Faceupdated 16d agoView on Hugging Face
0likes
app.py1192 linesDownload Raw Back to root
1"""2app.py3------4Production-grade Streamlit frontend dashboard for the AI-Powered Continuous5Cyber Risk Quantification (CRQ) Platform.6 7Connects to the FastAPI backend (default: http://127.0.0.1:8000) and exposes:8- Tab 1: Executive Risk Overview (EAL, VaR 95/99, Loss Exceedance Curve, Riskiest Assets)9- Tab 2: Budget Optimization (MILP Knapsack solver with ROSI and control recommendations)10- Tab 3: Regulatory Compliance (RBI CSF, SEBI CSCRF, NIST CSF 2.0 readiness & gap analysis)11- Tab 4: Asset Inventory (Enterprise IT/OT telemetry, vulnerabilities, and financial loss rates)12"""13 14from __future__ import annotations15 16import json17from typing import Any18 19import pandas as pd20import plotly.express as px21import plotly.graph_objects as go22import requests23import streamlit as st24 25# --------------------------------------------------------------------------- #26# Page Configuration & Global Styling27# --------------------------------------------------------------------------- #28st.set_page_config(29    page_title="Cyber Risk Quantification Platform",30    page_icon="๐Ÿ›ก๏ธ",31    layout="wide",32    initial_sidebar_state="expanded",33)34 35# Custom CSS for executive-grade aesthetics and high-contrast rendering36st.markdown(37    """38    <style>39    /* Metric Card container styling - Explicit light container */40    div[data-testid="stMetric"] {41        background: #FFFFFF !important;42        border: 1px solid #E2E8F0 !important;43        border-radius: 10px !important;44        padding: 16px 20px !important;45        box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06) !important;46        transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease !important;47    }48    div[data-testid="stMetric"]:hover {49        border-color: #CBD5E1 !important;50        box-shadow: 0 4px 12px rgba(15, 23, 42, 0.10) !important;51        transform: translateY(-2px);52    }53    div[data-testid="stMetricLabel"] {54        font-size: 0.88rem !important;55        font-weight: 600 !important;56        color: #475569 !important;57        text-transform: uppercase;58        letter-spacing: 0.05em;59    }60    div[data-testid="stMetricLabel"] p,61    div[data-testid="stMetricLabel"] label,62    div[data-testid="stMetricLabel"] > div {63        color: #475569 !important;64    }65    div[data-testid="stMetricValue"] {66        font-size: 1.85rem !important;67        font-weight: 700 !important;68        color: #0F172A !important;69    }70    div[data-testid="stMetricValue"] > div,71    div[data-testid="stMetricValue"] span {72        color: #0F172A !important;73    }74    75    /* Explicit delta tag styling */76    div[data-testid="stMetricDelta"] {77        font-weight: 600 !important;78    }79    div[data-testid="stMetricDelta"] svg {80        fill: currentColor !important;81    }82    div[data-testid="stMetricDelta"] svg[data-testid="stMetricDeltaIcon-Up"],83    div[data-testid="stMetricDelta"]:has(svg[data-testid="stMetricDeltaIcon-Up"]),84    div[data-testid="stMetricDelta"]:has(svg[data-testid="stMetricDeltaIcon-Up"]) > div {85        color: #16A34A !important;86    }87    div[data-testid="stMetricDelta"] svg[data-testid="stMetricDeltaIcon-Down"],88    div[data-testid="stMetricDelta"]:has(svg[data-testid="stMetricDeltaIcon-Down"]),89    div[data-testid="stMetricDelta"]:has(svg[data-testid="stMetricDeltaIcon-Down"]) > div {90        color: #DC2626 !important;91    }92    93    /* Gaps & Alert Callout Box - High Contrast */94    .gap-card {95        background: #FEF2F2 !important;96        border: 1px solid #FCA5A5 !important;97        border-left: 4px solid #DC2626 !important;98        border-radius: 8px;99        padding: 14px 18px;100        margin-bottom: 12px;101        box-shadow: 0 1px 3px rgba(153, 27, 27, 0.05);102    }103    .gap-header {104        font-weight: 700;105        font-size: 0.95rem;106        color: #991B1B !important;107        display: flex;108        justify-content: space-between;109    }110    .gap-desc {111        color: #7F1D1D !important;112        font-size: 0.88rem;113        margin-top: 6px;114        line-height: 1.4;115    }116    .gap-controls {117        margin-top: 8px;118        font-size: 0.82rem;119        color: #991B1B !important;120        font-weight: 500;121    }122    .gap-controls b {123        color: #7F1D1D !important;124    }125    126    /* Tier Badges */127    .badge-critical {128        background-color: rgba(239, 68, 68, 0.12);129        color: #DC2626;130        border: 1px solid rgba(239, 68, 68, 0.35);131        padding: 2px 8px;132        border-radius: 4px;133        font-weight: 600;134    }135    .badge-medium {136        background-color: rgba(245, 158, 11, 0.12);137        color: #D97706;138        border: 1px solid rgba(245, 158, 11, 0.35);139        padding: 2px 8px;140        border-radius: 4px;141        font-weight: 600;142    }143    .badge-low {144        background-color: rgba(16, 185, 129, 0.12);145        color: #059669;146        border: 1px solid rgba(16, 185, 129, 0.35);147        padding: 2px 8px;148        border-radius: 4px;149        font-weight: 600;150    }151    </style>152    """,153    unsafe_allow_html=True,154)155 156 157 158# --------------------------------------------------------------------------- #159# Helper Functions: Currency Formatting & Safe HTTP Client160# --------------------------------------------------------------------------- #161def format_inr(value: float | int | None) -> str:162    """163    Formats a raw numeric value into Rupee notation:164    - Crores (โ‚น Cr) for values >= โ‚น1,00,00,000 (10M)165    - Lakhs (โ‚น L) for values >= โ‚น1,00,000 (100K)166    - Indian comma grouping for smaller numbers167    """168    if value is None:169        return "โ‚น0.00"170    171    val = float(value)172    sign = "-" if val < 0 else ""173    abs_val = abs(val)174 175    if abs_val >= 10_000_000:176        return f"{sign}โ‚น{abs_val / 10_000_000:.2f} Cr"177    elif abs_val >= 100_000:178        return f"{sign}โ‚น{abs_val / 100_000:.2f} L"179    elif abs_val >= 1_000:180        integer_part, _, decimal_part = f"{abs_val:.2f}".partition(".")181        if len(integer_part) > 3:182            last_three = integer_part[-3:]183            remaining = integer_part[:-3]184            groups = []185            while len(remaining) > 2:186                groups.insert(0, remaining[-2:])187                remaining = remaining[:-2]188            if remaining:189                groups.insert(0, remaining)190            formatted = ",".join(groups + [last_three])191        else:192            formatted = integer_part193        return f"{sign}โ‚น{formatted}.{decimal_part}"194    else:195        return f"{sign}โ‚น{abs_val:.2f}"196 197 198def safe_get(url: str, params: dict[str, Any] | None = None, timeout: float = 12.0) -> tuple[bool, Any, str]:199    """200    Executes a safe GET request with custom timeout and structured error handling.201    Returns: (is_success, response_json_or_none, error_message_string)202    """203    try:204        response = requests.get(url, params=params, timeout=timeout)205        if response.status_code == 200:206            return True, response.json(), ""207        else:208            detail = ""209            try:210                detail = response.json().get("detail", response.text)211            except Exception:212                detail = response.text213            return False, None, f"HTTP {response.status_code}: {detail}"214    except requests.exceptions.ConnectionError:215        return False, None, f"Connection refused to backend at {url}. Ensure FastAPI is running."216    except requests.exceptions.Timeout:217        return False, None, f"Request timed out after {timeout}s while connecting to {url}."218    except requests.exceptions.RequestException as exc:219        return False, None, f"Network error: {str(exc)}"220 221 222def safe_post(url: str, json_data: dict[str, Any] | None = None, timeout: float = 60.0) -> tuple[bool, Any, str]:223    """224    Executes a safe POST request with custom timeout (default 60s) and structured error handling.225    Returns: (is_success, response_json_or_none, error_message_string)226    """227    try:228        response = requests.post(url, json=json_data, timeout=timeout)229        if response.status_code == 200:230            return True, response.json(), ""231        else:232            detail = ""233            try:234                detail = response.json().get("detail", response.text)235            except Exception:236                detail = response.text237            return False, None, f"HTTP {response.status_code}: {detail}"238    except requests.exceptions.ConnectionError:239        return False, None, f"Connection refused to backend at {url}. Ensure FastAPI is running."240    except requests.exceptions.Timeout:241        return False, None, f"Request timed out after {timeout}s while connecting to {url}."242    except requests.exceptions.RequestException as exc:243        return False, None, f"Network error: {str(exc)}"244 245 246@st.cache_data(ttl=300)247def get_cached_budget_optimization(248    backend_url: str, budget_inr: float249) -> tuple[bool, Any, str]:250    """251    Caches budget solver responses keyed on budget_inr with 5-minute TTL252    to prevent redundant backend solves on previously queried spend limits.253    """254    return safe_post(255        f"{backend_url}/api/optimize/budget",256        json_data={"budget_inr": float(budget_inr)},257        timeout=60.0,258    )259 260 261# --------------------------------------------------------------------------- #262# Sidebar: Configuration & Health Telemetry263# --------------------------------------------------------------------------- #264with st.sidebar:265    st.markdown("### ๐Ÿ›ก๏ธ CRQ Platform")266    st.caption("AI-Powered Continuous Cyber Risk Quantification")267    st.markdown("---")268 269    backend_url = st.text_input(270        "FastAPI Backend URL",271        value="http://127.0.0.1:8000",272        help="Base URL of the FastAPI server providing quantification and optimization APIs.",273    ).rstrip("/")274 275    # Check connection health276    is_healthy, health_data, health_err = safe_get(f"{backend_url}/", timeout=3.0)277 278    if is_healthy:279        st.success("๐ŸŸข **Backend Connected**")280        with st.expander("Server Telemetry", expanded=False):281            st.json(health_data)282    else:283        st.error("๐Ÿ”ด **Backend Offline**")284        st.caption(f"`{health_err}`")285        st.info("Start the API server via:\n```bash\nuvicorn main:app --reload --port 8000\n```")286 287    st.markdown("---")288    st.markdown("#### Database Administration")289    if st.button("๐ŸŒฑ Seed Synthetic Telemetry", use_container_width=True):290        with st.spinner("Seeding enterprise database..."):291            ok, seed_res, seed_err = safe_post(f"{backend_url}/api/seed", timeout=60.0)292            if ok:293                st.toast("Database seeded successfully!", icon="โœ…")294                st.cache_data.clear()295                st.rerun()296            else:297                st.error(f"Seeding failed: {seed_err}")298 299    if st.button("๐Ÿ“ฅ Ingest Kaggle CVE Dataset", use_container_width=True):300        with st.spinner("Ingesting Kaggle CVE, CISA KEV & EPSS dataset..."):301            ok, kaggle_res, kaggle_err = safe_post(302                f"{backend_url}/api/seed/kaggle", timeout=60.0303            )304            if ok:305                count = kaggle_res.get("vulnerabilities_ingested", 0)306                mapped = len(kaggle_res.get("mapped_assets", []))307                st.toast(308                    f"Ingested {count} Kaggle CVEs across {mapped} assets!",309                    icon="โœ…",310                )311                st.cache_data.clear()312                st.rerun()313            else:314                st.error(f"Kaggle ingestion failed: {kaggle_err}")315 316    if st.button("๐Ÿ”„ Refresh Application Data", use_container_width=True):317        st.cache_data.clear()318        st.rerun()319 320 321    st.markdown("---")322    st.caption("Standards: RBI CSF โ€ข SEBI CSCRF โ€ข NIST CSF 2.0")323 324 325# --------------------------------------------------------------------------- #326# Main Header327# --------------------------------------------------------------------------- #328st.title("๐Ÿ›ก๏ธ Cyber Risk Quantification Platform")329st.markdown(330    "Enterprise financial loss modeling via **Monte Carlo simulations**, "331    "**Dijkstra attack path exposure**, and **MILP budget optimization**."332)333 334# --------------------------------------------------------------------------- #335# Application Tabs336# --------------------------------------------------------------------------- #337tab1, tab2, tab3, tab4 = st.tabs(338    [339        "๐Ÿ“Š Executive Risk Overview",340        "๐ŸŽฏ Budget Optimization",341        "โš–๏ธ Regulatory Compliance",342        "๐Ÿ–ฅ๏ธ Asset Inventory & Lineage",343    ]344)345 346 347# =========================================================================== #348# TAB 1: EXECUTIVE RISK OVERVIEW349# =========================================================================== #350with tab1:351    st.markdown("### ๐Ÿ“ˆ Enterprise Financial Risk Exposure")352    st.caption("Aggregate loss metrics quantified in INR (โ‚น) across 10,000 Monte Carlo stochastic scenarios.")353 354    ok, summary_data, err = safe_get(f"{backend_url}/api/quantification/enterprise")355 356    if not ok:357        st.warning(f"โš ๏ธ Unable to load enterprise quantification: {err}")358        if "No assets found" in (err or ""):359            st.info("The database appears empty. Click **'Seed Synthetic Telemetry'** in the sidebar to populate initial data.")360    else:361        # Indirect Data Leak Detection Alert Callout362        unauth_leaks = [363            f for f in summary_data.get("identified_leak_vectors", [])364            if (not f.get("is_authorized", True) or not f.get("has_user_consent", True) or "Unauthorized" in f.get("detection_status", ""))365        ]366        if unauth_leaks or summary_data.get("unauthorized_subprocessor_count", 0) > 0:367            first_leak = unauth_leaks[0] if unauth_leaks else {}368            origin_h = first_leak.get("origin_hostname", "cust-db-primary")369            inter_h = first_leak.get("intermediary_hostname", "analytics-integration-gw")370            dest_h = first_leak.get("destination_hostname", "third-party-marketing-sync")371            st.error(372                f"โš ๏ธ **Indirect Data Leak Detected:** Node C (`{dest_h}`) is exfiltrating records originating "373                f"from Node A (`{origin_h}`) via unauthorized delegation through Node B (`{inter_h}`)."374            )375 376        # Top KPI Metric Cards377        col1, col2, col3, col4, col5 = st.columns(5)378        379        with col1:380            st.metric(381                label="Expected Annual Loss (EAL)",382                value=format_inr(summary_data.get("total_eal_inr")),383                help="Mean annualized financial loss across direct disruption, data theft, and response costs.",384            )385        with col2:386            st.metric(387                label="95% Value at Risk (VaR 95)",388                value=format_inr(summary_data.get("var_95_inr")),389                help="Maximum expected loss within a single year at a 95% confidence level (1-in-20 year loss).",390            )391        with col3:392            st.metric(393                label="99% Value at Risk (VaR 99)",394                value=format_inr(summary_data.get("var_99_inr")),395                help="Severe catastrophic single-year loss threshold at a 99% confidence level (1-in-100 year event).",396            )397        with col4:398            st.metric(399                label="Regulatory Fine Exposure",400                value=format_inr(summary_data.get("total_regulatory_fine_exposure_inr")),401                help="Total statutory penalty exposure under RBI CSF, SEBI CSCRF, and DPDP Act provisions.",402            )403        with col5:404            shadow_exp = summary_data.get("shadow_leakage_exposure_inr", 0.0)405            unauth_cnt = summary_data.get("unauthorized_subprocessor_count", 0)406            st.metric(407                label="Shadow Leakage Risk Exposure",408                value=format_inr(shadow_exp),409                delta=f"{unauth_cnt} Unauthorized Leak{'s' if unauth_cnt != 1 else ''}" if unauth_cnt > 0 else "0 Leaks",410                delta_color="inverse" if unauth_cnt > 0 else "normal",411                help="Annualized financial loss exposure attributable to unauthorized third-party sub-processor delegation and indirect data leakage under DPDPA Section 8(4).",412            )413 414        st.markdown("---")415 416        # Visualizations: Loss Exceedance Curve & Riskiest Assets417        chart_col1, chart_col2 = st.columns([3, 2])418 419        with chart_col1:420            st.markdown("#### Loss Exceedance Curve (LEC)")421            st.caption("Probability of enterprise losses exceeding designated financial thresholds over varying return horizons.")422 423            lec_points = summary_data.get("loss_exceedance_curve", [])424            if lec_points:425                df_lec = pd.DataFrame(lec_points)426                df_lec["loss_formatted"] = df_lec["loss_inr"].apply(format_inr)427 428                fig_lec = go.Figure()429                fig_lec.add_trace(430                    go.Scatter(431                        x=df_lec["return_period_years"],432                        y=df_lec["loss_inr"],433                        mode="lines+markers",434                        name="Aggregate Loss Exceedance",435                        line=dict(color="#6366F1", width=3),436                        marker=dict(size=7, color="#818CF8"),437                        fill="tozeroy",438                        fillcolor="rgba(99, 102, 241, 0.12)",439                        hovertemplate="<b>Return Period:</b> %{x} Years<br><b>Loss:</b> %{customdata}<extra></extra>",440                        customdata=df_lec["loss_formatted"],441                    )442                )443                fig_lec.update_layout(444                    template="plotly_white",445                    paper_bgcolor="#FFFFFF",446                    plot_bgcolor="#FFFFFF",447                    font=dict(color="#1E293B"),448                    margin=dict(l=20, r=20, t=30, b=30),449                    xaxis=dict(450                        title="Return Period (Years)",451                        showgrid=True,452                        gridcolor="#E2E8F0",453                        color="#1E293B",454                        type="log",455                    ),456                    yaxis=dict(457                        title="Loss Threshold (โ‚น)",458                        showgrid=True,459                        gridcolor="#E2E8F0",460                        color="#1E293B",461                    ),462                    height=360,463                )464                st.plotly_chart(fig_lec, use_container_width=True)465            else:466                st.info("No loss exceedance curve data available.")467 468        with chart_col2:469            st.markdown("#### Top Riskiest Assets by Criticality")470            st.caption("Assets contributing highest individual Expected Annual Loss (EAL) in โ‚น.")471 472            top_assets = summary_data.get("top_5_riskiest_assets", [])473            if top_assets:474                df_top = pd.DataFrame(top_assets)475                df_top["formatted_eal"] = df_top["eal_inr"].apply(format_inr)476 477                color_map = {478                    "Critical": "#EF4444",479                    "Medium": "#F59E0B",480                    "Low": "#10B981",481                }482 483                fig_top = px.bar(484                    df_top,485                    x="eal_inr",486                    y="hostname",487                    orientation="h",488                    color="tier",489                    color_discrete_map=color_map,490                    labels={"eal_inr": "EAL (โ‚น)", "hostname": "Asset", "tier": "Tier"},491                    hover_data={492                        "eal_inr": False,493                        "formatted_eal": True,494                        "asset_type": True,495                        "annual_event_frequency": ":.3f",496                    },497                )498                fig_top.update_layout(499                    template="plotly_white",500                    paper_bgcolor="#FFFFFF",501                    plot_bgcolor="#FFFFFF",502                    font=dict(color="#1E293B"),503                    margin=dict(l=20, r=20, t=30, b=30),504                    xaxis=dict(showgrid=True, gridcolor="#E2E8F0", color="#1E293B"),505                    yaxis=dict(autorange="reversed", color="#1E293B"),506                    height=360,507                    showlegend=True,508                    legend=dict(509                        orientation="h",510                        yanchor="bottom",511                        y=1.02,512                        xanchor="right",513                        x=1,514                        font=dict(color="#1E293B"),515                    ),516                )517                st.plotly_chart(fig_top, use_container_width=True)518            else:519                st.info("No asset risk contributions returned.")520 521 522# =========================================================================== #523# TAB 2: BUDGET OPTIMIZATION (MILP KNAPSACK)524# =========================================================================== #525with tab2:526    st.markdown("### ๐ŸŽฏ Security Budget Allocation & Knapsack Optimization")527    st.caption("Solves a 0/1 Mixed-Integer Linear Program (MILP) to identify the optimal security investments maximizing ROSI under budget caps.")528 529    if "current_budget" not in st.session_state:530        st.session_state["current_budget"] = 5_000_000531 532    # Budget Optimization Form to eliminate slider request flooding533    with st.form("budget_optimization_form"):534        slider_col, _ = st.columns([3, 1])535        with slider_col:536            selected_budget = st.slider(537                "Target Security Investment Budget (โ‚น)",538                min_value=500_000,539                max_value=15_000_000,540                value=int(st.session_state["current_budget"]),541                step=500_000,542                format="%d",543                help="Select budget ceiling for PuLP Mixed Integer Linear Programming solver.",544            )545            st.markdown(f"**Selected Spend Limit:** `{format_inr(selected_budget)}`")546 547        submitted = st.form_submit_button("๐Ÿš€ Run Portfolio Optimization", type="primary")548        if submitted:549            st.session_state["current_budget"] = selected_budget550 551    active_budget = float(st.session_state["current_budget"])552 553    # Trigger optimization with client-side caching554    with st.spinner("Solving Mixed Integer Linear Program (MILP)..."):555        opt_ok, opt_data, opt_err = get_cached_budget_optimization(556            backend_url, active_budget557        )558 559    if not opt_ok:560        st.warning(f"โš ๏ธ Optimization failed: {opt_err}")561    else:562        # Optimization Metrics563        m1, m2, m3, m4 = st.columns(4)564        with m1:565            st.metric(566                label="Total Capital Deployed",567                value=format_inr(opt_data.get("total_spent_inr")),568                delta=f"Remaining: {format_inr(opt_data.get('remaining_budget_inr'))}",569                delta_color="off",570            )571        with m2:572            st.metric(573                label="Baseline EAL (Pre-Investment)",574                value=format_inr(opt_data.get("baseline_eal_inr")),575            )576        with m3:577            st.metric(578                label="Net Risk Reduction (ฮ”EAL)",579                value=format_inr(opt_data.get("net_risk_reduction_inr")),580                delta=f"-{format_inr(opt_data.get('net_risk_reduction_inr'))}",581                delta_color="inverse",582            )583        with m4:584            st.metric(585                label="Return on Security Investment (ROSI)",586                value=f"{opt_data.get('rosi_percent', 0.0):.1f}%",587                help="Net Risk Reduction minus Capital Cost divided by Capital Cost.",588            )589 590        st.markdown("---")591 592        # Portfolio Comparison Chart & Selected Controls Table593        p_col1, p_col2 = st.columns([2, 3])594 595        with p_col1:596            st.markdown("#### Portfolio Risk Reduction Comparison")597            comp_df = pd.DataFrame(598                [599                    {"Stage": "Baseline Risk", "Amount": opt_data.get("baseline_eal_inr", 0)},600                    {"Stage": "Projected Risk", "Amount": opt_data.get("projected_eal_inr", 0)},601                    {"Stage": "Capital Deployed", "Amount": opt_data.get("total_spent_inr", 0)},602                ]603            )604            comp_df["Formatted"] = comp_df["Amount"].apply(format_inr)605 606            fig_comp = px.bar(607                comp_df,608                x="Stage",609                y="Amount",610                color="Stage",611                color_discrete_sequence=["#EF4444", "#10B981", "#6366F1"],612                text="Formatted",613            )614            fig_comp.update_layout(615                template="plotly_white",616                paper_bgcolor="#FFFFFF",617                plot_bgcolor="#FFFFFF",618                font=dict(color="#1E293B"),619                showlegend=False,620                height=350,621                yaxis=dict(title="INR (โ‚น)", showgrid=True, gridcolor="#E2E8F0", color="#1E293B"),622                xaxis=dict(color="#1E293B"),623                margin=dict(l=10, r=10, t=20, b=20),624            )625            st.plotly_chart(fig_comp, use_container_width=True)626 627        with p_col2:628            st.markdown("#### Recommended Security Controls")629            controls = opt_data.get("selected_controls", [])630            if controls:631                df_ctrl = pd.DataFrame(controls)632                df_ctrl["Cost"] = df_ctrl["cost_inr"].apply(format_inr)633                df_ctrl["Efficacy"] = df_ctrl["likelihood_reduction"].apply(lambda v: f"{v*100:.0f}%")634                df_ctrl["Marginal Benefit"] = df_ctrl["marginal_eal_reduction_inr"].apply(format_inr)635 636                display_df = df_ctrl[["code", "name", "target_tier", "Cost", "Efficacy", "Marginal Benefit"]].rename(637                    columns={638                        "code": "Code",639                        "name": "Control Name",640                        "target_tier": "Scope",641                    }642                )643                st.dataframe(display_df, use_container_width=True, hide_index=True)644            else:645                st.info("No security controls fit within the allocated spend constraint.")646 647 648# =========================================================================== #649# TAB 3: REGULATORY COMPLIANCE (RBI / SEBI / NIST)650# =========================================================================== #651with tab3:652    st.markdown("### โš–๏ธ Regulatory Readiness & Compliance Gap Audit")653    st.caption("Continuous benchmarking against Reserve Bank of India (RBI CSF), SEBI CSCRF, NIST CSF 2.0, ISO 27001:2022, and ISO 42001:2023 frameworks.")654 655    comp_ok, comp_data, comp_err = safe_get(f"{backend_url}/api/compliance/status")656 657    if not comp_ok:658        st.warning(f"โš ๏ธ Unable to load compliance assessment: {comp_err}")659    else:660        # Compliance KPI Cards661        rbi_pct = comp_data.get("rbi_compliance_index_percent", 0.0)662        sebi_pct = comp_data.get("sebi_compliance_index_percent", 0.0)663        nist_pct = comp_data.get("nist_csf_compliance_index_percent", 0.0)664        iso27001_pct = comp_data.get("iso27001_compliance_index_percent", 0.0)665        iso42001_pct = comp_data.get("iso42001_compliance_index_percent", 0.0)666        overall_pct = comp_data.get("overall_compliance_index_percent", 0.0)667        penalty_exposure = comp_data.get("estimated_regulatory_penalty_exposure_inr", 0.0)668 669        c1, c2, c3, c4, c5, c6 = st.columns(6)670        with c1:671            st.metric(672                label="RBI CSF Readiness",673                value=f"{rbi_pct:.1f}%",674                delta=f"{rbi_pct - 100:.1f}% to target",675                delta_color="normal" if rbi_pct >= 85 else "inverse",676            )677        with c2:678            st.metric(679                label="SEBI CSCRF Readiness",680                value=f"{sebi_pct:.1f}%",681                delta=f"{sebi_pct - 100:.1f}% to target",682                delta_color="normal" if sebi_pct >= 85 else "inverse",683            )684        with c3:685            st.metric(686                label="NIST CSF 2.0 Maturity",687                value=f"{nist_pct:.1f}%",688                delta=f"{nist_pct - 100:.1f}% to target",689                delta_color="normal" if nist_pct >= 85 else "inverse",690            )691        with c4:692            st.metric(693                label="ISO 27001:2022 Readiness",694                value=f"{iso27001_pct:.1f}%",695                delta=f"{iso27001_pct - 100:.1f}% to target",696                delta_color="normal" if iso27001_pct >= 85 else "inverse",697            )698        with c5:699            st.metric(700                label="ISO 42001:2023 AI Readiness",701                value=f"{iso42001_pct:.1f}%",702                delta=f"{iso42001_pct - 100:.1f}% to target",703                delta_color="normal" if iso42001_pct >= 85 else "inverse",704            )705        with c6:706            st.metric(707                label="Estimated Penalty Exposure",708                value=format_inr(penalty_exposure),709                help="Potential regulatory fines for non-compliance with critical directives.",710            )711 712        # Statutory Data Residency & Cross-Border Governance KPI Cards713        st.markdown("<div style='height: 10px;'></div>", unsafe_allow_html=True)714        cb_pct = comp_data.get("cross_border_compliance_index_percent", 100.0)715        rbi_loc_status = comp_data.get("rbi_localization_status", "Compliant")716 717        sb1, sb2, sb3 = st.columns(3)718        with sb1:719            st.metric(720                label="Cross-Border Transfer Readiness",721                value=f"{cb_pct:.1f}%",722                delta="Compliant" if cb_pct >= 90 else f"{100.0 - cb_pct:.1f}% Non-Compliant",723                delta_color="normal" if cb_pct >= 90 else "inverse",724                help="Statutory compliance index of cross-border data transfers evaluated under DPDPA 2023 & RBI directives.",725            )726        with sb2:727            st.metric(728                label="RBI Data Localization Status",729                value="Compliant โœ…" if rbi_loc_status == "Compliant" else "Violation Detected ๐Ÿšจ",730                delta="Domestic Storage Mandate" if rbi_loc_status == "Compliant" else "Non-Compliant Data Location",731                delta_color="normal" if rbi_loc_status == "Compliant" else "inverse",732                help="Storage of Payment System Data within India (RBI Directive April 2018 / Master Direction).",733            )734        with sb3:735            st.metric(736                label="DPDPA Section 16 Safeguards",737                value="Enforced (SCC/BCR)" if cb_pct == 100.0 else "Action Required",738                delta="Valid Safeguards" if cb_pct == 100.0 else "Unapproved Transfer Rails",739                delta_color="normal" if cb_pct == 100.0 else "inverse",740                help="Requires approved legal transfer mechanisms (SCC, BCR, adequacy, or consent) for personal data export.",741            )742 743        # Sub-Processor Consent & Delegation Governance Audit Check (DPDPA Section 8 & ISO 27001 Clause A.5)744        st.markdown("<div style='height: 10px;'></div>", unsafe_allow_html=True)745        gaps_list = comp_data.get("gaps", [])746        lineage_gaps = [747            g for g in gaps_list748            if "Section 8(4)" in g.get("category", "") or "Clause A.5" in g.get("category", "") or "Sub-Processor" in g.get("category", "")749        ]750 751        sub_col1, sub_col2 = st.columns(2)752        with sub_col1:753            has_dpdpa_gap = any("Section 8(4)" in g.get("category", "") for g in lineage_gaps)754            st.metric(755                label="Sub-Processor Governance (DPDPA Sec 8)",756                value="Non-Compliant ๐Ÿšจ" if has_dpdpa_gap else "Compliant โœ…",757                delta="Mandatory Lineage Tracer Missing" if has_dpdpa_gap else "Oversight Active",758                delta_color="inverse" if has_dpdpa_gap else "normal",759                help="DPDPA Section 8(4) statutory duty of data fiduciary to verify and audit downstream sub-processors.",760            )761        with sub_col2:762            has_iso_gap = any("Clause A.5" in g.get("category", "") for g in lineage_gaps)763            st.metric(764                label="ISO 27001 Clause A.5 Delegation Verifier",765                value="Action Required โš ๏ธ" if has_iso_gap else "Governed โœ…",766                delta="Zero-Trust Consent Gap" if has_iso_gap else "Consent Enforced",767                delta_color="inverse" if has_iso_gap else "normal",768                help="ISO 27001:2022 Clause A.5 organizational control requirement for sub-processor consent delegation.",769            )770 771        st.markdown("---")772 773        rad_col, gap_col = st.columns([1, 1])774 775        with rad_col:776            st.markdown("#### Framework Readiness Radar")777            radar_categories = ["RBI CSF", "SEBI CSCRF", "NIST CSF 2.0", "ISO 27001", "ISO 42001"]778            radar_values = [rbi_pct, sebi_pct, nist_pct, iso27001_pct, iso42001_pct]779 780            fig_radar = go.Figure()781            # Current compliance trace782            fig_radar.add_trace(783                go.Scatterpolar(784                    r=radar_values + [radar_values[0]],785                    theta=radar_categories + [radar_categories[0]],786                    fill="toself",787                    name="Current Readiness",788                    line=dict(color="#0284C7", width=2.5),789                    fillcolor="rgba(2, 132, 199, 0.20)",790                )791            )792            # Target 100% trace793            fig_radar.add_trace(794                go.Scatterpolar(795                    r=[100, 100, 100, 100, 100, 100],796                    theta=radar_categories + [radar_categories[0]],797                    mode="lines",798                    name="Regulatory Target (100%)",799                    line=dict(color="#94A3B8", dash="dash", width=1.5),800                )801            )802            fig_radar.update_layout(803                template="plotly_white",804                paper_bgcolor="#FFFFFF",805                plot_bgcolor="#FFFFFF",806                font=dict(color="#1E293B"),807                polar=dict(808                    bgcolor="#FFFFFF",809                    radialaxis=dict(810                        visible=True,811                        range=[0, 100],812                        showticklabels=True,813                        ticks="outside",814                        tickfont=dict(color="#1E293B", size=10),815                        gridcolor="#E2E8F0",816                        linecolor="#CBD5E1",817                    ),818                    angularaxis=dict(819                        gridcolor="#E2E8F0",820                        linecolor="#CBD5E1",821                        tickfont=dict(color="#1E293B", size=11, family="sans-serif"),822                    ),823                ),824                height=380,825                margin=dict(l=30, r=30, t=30, b=30),826                legend=dict(827                    orientation="h",828                    yanchor="bottom",829                    y=-0.22,830                    xanchor="center",831                    x=0.5,832                    font=dict(color="#1E293B"),833                ),834            )835            st.plotly_chart(fig_radar, use_container_width=True)836 837        with gap_col:838            st.markdown("#### Identified Gaps & Actionable Remediations")839            gaps = comp_data.get("gaps", [])840            if gaps:841                for idx, gap in enumerate(gaps):842                    rec_controls = ", ".join(gap.get("recommended_control_codes", [])) or "None specified"843                    st.markdown(844                        f"""845                        <div class="gap-card">846                            <div class="gap-header">847                                <span>โš ๏ธ {gap.get('framework')} โ€ข {gap.get('category')}</span>848                            </div>849                            <div class="gap-desc">{gap.get('description')}</div>850                            <div class="gap-controls"><b>Recommended Fixes:</b> {rec_controls}</div>851                        </div>852                        """,853                        unsafe_allow_html=True,854                    )855            else:856                st.success("โœ… Zero active regulatory gaps detected. Enterprise meets baseline standards.")857 858        # Cross-Border Data Transfer Audit Register Table859        st.markdown("---")860        st.markdown("#### ๐ŸŒ Cross-Border Data Transfer & Statutory Jurisdiction Audit")861        st.caption("Live statutory verification of data flows against RBI Payment Data Localization Directive and DPDPA 2023 Section 16 restrictions.")862 863        asset_flow_ok, asset_flow_data, _ = safe_get(f"{backend_url}/api/assets")864        if asset_flow_ok and asset_flow_data:865            audit_rows = []866            for item in asset_flow_data:867                residency = item.get("data_residency_country", "IN")868                cb_enabled = item.get("cross_border_transfer_enabled", False)869                dest = item.get("destination_countries") or "โ€”"870                mech = item.get("transfer_legal_mechanism") or "None"871                rbi_loc = item.get("is_rbi_localization_compliant", True)872                is_rbi = item.get("is_rbi_regulated", False)873                fin_cnt = item.get("financial_records_count", 0)874                pii_cnt = item.get("pii_records_count", 0)875                is_tp = item.get("is_third_party", False)876 877                is_rbi_viol = (is_rbi and not rbi_loc) or (is_rbi and fin_cnt > 0 and residency != "IN") or (cb_enabled and not rbi_loc)878                is_dpdpa_viol = cb_enabled and pii_cnt > 0 and mech in (None, "None")879                is_tp_viol = is_tp and residency != "IN"880 881                status_label = "Compliant โœ…"882                if is_rbi_viol:883                    status_label = "RBI Localization Violation ๐Ÿšจ"884                elif is_dpdpa_viol:885                    status_label = "DPDPA Sec 16 Violation โš ๏ธ"886                elif is_tp_viol:887                    status_label = "Foreign Vendor Review โ„น๏ธ"888 889                audit_rows.append({890                    "Hostname": item.get("hostname"),891                    "Origin Country": f"{residency} ๐Ÿ‡ฎ๐Ÿ‡ณ" if residency == "IN" else f"{residency} ๐ŸŒ",892                    "Cross-Border Transfer": "Active ๐ŸŒ" if cb_enabled else "Domestic Only ๐Ÿ”’",893                    "Destination Countries": dest,894                    "Applied Legal Safeguard": mech,895                    "RBI Localization": "Compliant โœ…" if (rbi_loc and (not is_rbi or residency == "IN")) else "Non-Compliant ๐Ÿšจ",896                    "Statutory Compliance Status": status_label,897                })898            st.dataframe(pd.DataFrame(audit_rows), use_container_width=True, hide_index=True)899 900 901# =========================================================================== #902# TAB 4: ASSET INVENTORY & TELEMETRY903# =========================================================================== #904with tab4:905    st.markdown("### ๐Ÿ–ฅ๏ธ IT/OT Asset Inventory & Third-Party Risk (TPRM)")906    st.caption("Live enterprise asset register enriched with financial impact metrics, sensitive data counts, ISO compliance scope, and third-party vendor risk.")907 908    asset_ok, asset_data, asset_err = safe_get(f"{backend_url}/api/assets")909 910    if not asset_ok:911        st.warning(f"โš ๏ธ Unable to load asset inventory: {asset_err}")912    else:913        if not asset_data:914            st.info("No assets found. Seed the database to display inventory.")915        else:916            df_assets = pd.DataFrame(asset_data)917 918            # Summary Metrics for Inventory & TPRM919            a1, a2, a3, a4, a5, a6 = st.columns(6)920            with a1:921                st.metric("Total Enterprise Hosts", len(df_assets))922            with a2:923                crit_count = len(df_assets[df_assets["tier"] == "Critical"]) if "tier" in df_assets else 0924                st.metric("Crown Jewel Assets", crit_count)925            with a3:926                tp_count = len(df_assets[df_assets["is_third_party"] == True]) if "is_third_party" in df_assets else 0927                st.metric("Third-Party Vendors", tp_count)928            with a4:929                ai_count = len(df_assets[df_assets["is_iso42001_regulated"] == True]) if "is_iso42001_regulated" in df_assets else 0930                st.metric("AI/ML Pipelines", ai_count)931            with a5:932                total_pii = df_assets["pii_records_count"].sum() if "pii_records_count" in df_assets else 0933                st.metric("Total PII Records", f"{total_pii:,}")934            with a6:935                total_fin = df_assets["financial_records_count"].sum() if "financial_records_count" in df_assets else 0936                st.metric("Total Financial Records", f"{total_fin:,}")937 938            st.markdown("---")939 940            # Filters941            f_col1, f_col2, f_col3, f_col4, f_col5 = st.columns([1, 1.1, 1.1, 1.2, 1.5])942            with f_col1:943                tier_filter = st.selectbox("Criticality Tier", ["All", "Critical", "Medium", "Low"])944            with f_col2:945                class_filter = st.selectbox(946                    "Classification Type",947                    [948                        "All",949                        "Core IT",950                        "AI/ML Model Pipeline",951                        "Third-Party SaaS",952                        "Third-Party Vendor API",953                        "Cloud Infrastructure",954                        "Data Repository",955                    ],956                )957            with f_col3:958                std_filter = st.selectbox(959                    "Standard & Governance Scope",960                    [961                        "All",962                        "ISO 27001 In-Scope",963                        "ISO 42001 In-Scope",964                        "Third-Party Only",965                        "RBI Regulated",966                        "SEBI Regulated",967                    ],968                )969            with f_col4:970                residency_scope = st.selectbox(971                    "Data Residency Scope",972                    [973                        "All",974                        "Domestic (India Only)",975                        "Cross-Border Active",976                        "Localization Non-Compliant",977                    ],978                )979            with f_col5:980                search_term = st.text_input("Search Hostname, Vendor, Unit, or Country", "").strip().lower()981 982            filtered_df = df_assets.copy()983            if tier_filter != "All":984                filtered_df = filtered_df[filtered_df["tier"] == tier_filter]985            if class_filter != "All" and "classification_type" in filtered_df:986                filtered_df = filtered_df[filtered_df["classification_type"] == class_filter]987            if std_filter == "ISO 27001 In-Scope" and "is_iso27001_regulated" in filtered_df:988                filtered_df = filtered_df[filtered_df["is_iso27001_regulated"] == True]989            elif std_filter == "ISO 42001 In-Scope" and "is_iso42001_regulated" in filtered_df:990                filtered_df = filtered_df[filtered_df["is_iso42001_regulated"] == True]991            elif std_filter == "Third-Party Only" and "is_third_party" in filtered_df:992                filtered_df = filtered_df[filtered_df["is_third_party"] == True]993            elif std_filter == "RBI Regulated" and "is_rbi_regulated" in filtered_df:994                filtered_df = filtered_df[filtered_df["is_rbi_regulated"] == True]995            elif std_filter == "SEBI Regulated" and "is_sebi_regulated" in filtered_df:996                filtered_df = filtered_df[filtered_df["is_sebi_regulated"] == True]997 998            # Data Residency Scope filter999            if residency_scope == "Domestic (India Only)" and "data_residency_country" in filtered_df:1000                filtered_df = filtered_df[1001                    (filtered_df["data_residency_country"] == "IN")1002                    & (filtered_df["cross_border_transfer_enabled"] == False)1003                ]1004            elif residency_scope == "Cross-Border Active" and "cross_border_transfer_enabled" in filtered_df:1005                filtered_df = filtered_df[1006                    (filtered_df["cross_border_transfer_enabled"] == True)1007                    | (filtered_df["data_residency_country"] != "IN")1008                ]1009            elif residency_scope == "Localization Non-Compliant" and "is_rbi_localization_compliant" in filtered_df:1010                filtered_df = filtered_df[1011                    (filtered_df["is_rbi_localization_compliant"] == False)1012                    | ((filtered_df["is_rbi_regulated"] == True) & (filtered_df["data_residency_country"] != "IN"))1013                ]1014 1015            if search_term:1016                host_match = filtered_df["hostname"].astype(str).str.lower().str.contains(search_term)1017                bu_match = filtered_df["business_unit"].astype(str).str.lower().str.contains(search_term)1018                type_match = filtered_df["asset_type"].astype(str).str.lower().str.contains(search_term)1019                class_match = (1020                    filtered_df["classification_type"].fillna("").astype(str).str.lower().str.contains(search_term)1021                    if "classification_type" in filtered_df1022                    else False1023                )1024                vendor_match = (1025                    filtered_df["vendor_name"].fillna("").astype(str).str.lower().str.contains(search_term)1026                    if "vendor_name" in filtered_df1027                    else False1028                )1029                country_match = (1030                    filtered_df["data_residency_country"].fillna("").astype(str).str.lower().str.contains(search_term)1031                    if "data_residency_country" in filtered_df1032                    else False1033                )1034                filtered_df = filtered_df[host_match | bu_match | type_match | class_match | vendor_match | country_match]1035 1036            # Format DataFrame for Display with Third-Party & Residency Indicators1037            display_table = pd.DataFrame()1038            display_table["Hostname"] = filtered_df["hostname"]1039            display_table["Classification"] = (1040                filtered_df["classification_type"] if "classification_type" in filtered_df else "Internal IT"1041            )1042            display_table["Type"] = filtered_df["asset_type"]1043            display_table["Tier"] = filtered_df["tier"]1044            display_table["Residency (ISO)"] = (1045                filtered_df["data_residency_country"].apply(lambda c: f"{c} ๐Ÿ‡ฎ๐Ÿ‡ณ" if c == "IN" else f"{c} ๐ŸŒ")1046                if "data_residency_country" in filtered_df1047                else "IN ๐Ÿ‡ฎ๐Ÿ‡ณ"1048            )1049            display_table["Cross-Border Flow"] = (1050                filtered_df["cross_border_transfer_enabled"].apply(lambda b: "Yes ๐ŸŒ" if b else "No")1051                if "cross_border_transfer_enabled" in filtered_df1052                else "No"1053            )1054            display_table["Transfer Mechanism"] = (1055                filtered_df["transfer_legal_mechanism"].fillna("None")1056                if "transfer_legal_mechanism" in filtered_df1057                else "None"1058            )1059            display_table["RBI Localized"] = (1060                filtered_df.apply(1061                    lambda row: "Non-Compliant ๐Ÿšจ"1062                    if (not row.get("is_rbi_localization_compliant", True)) or (row.get("is_rbi_regulated", False) and row.get("data_residency_country") != "IN")1063                    else ("Compliant โœ…" if row.get("is_rbi_regulated", False) else "Domestic ๐Ÿ‡ฎ๐Ÿ‡ณ"),1064                    axis=1,1065                )1066                if "is_rbi_localization_compliant" in filtered_df1067                else "Domestic ๐Ÿ‡ฎ๐Ÿ‡ณ"1068            )1069            display_table["Third-Party"] = (1070                filtered_df["is_third_party"].apply(lambda b: "External ๐ŸŒ" if b else "Internal ๐Ÿข")1071                if "is_third_party" in filtered_df1072                else "Internal ๐Ÿข"1073            )1074            display_table["Vendor Name"] = (1075                filtered_df["vendor_name"].fillna("โ€”") if "vendor_name" in filtered_df else "โ€”"1076            )1077            display_table["Vendor Risk Tier"] = (1078                filtered_df["vendor_risk_tier"].apply(1079                    lambda v: f"๐Ÿšจ {v}"1080                    if v == "Tier-1 Critical"1081                    else (f"โš ๏ธ {v}" if v == "Tier-2 High" else (f"โ„น๏ธ {v}" if pd.notna(v) and v else "โ€”"))1082                )1083                if "vendor_risk_tier" in filtered_df1084                else "โ€”"1085            )1086            display_table["SOC 2 Status"] = (1087                filtered_df["soc2_attestation"].apply(lambda b: "Attested โœ…" if b else "Uncertified โš ๏ธ")1088                if "soc2_attestation" in filtered_df1089                else "Uncertified โš ๏ธ"1090            )1091            display_table["ISO 27001"] = (1092                filtered_df["is_iso27001_regulated"].apply(lambda b: "In-Scope" if b else "Exempt")1093                if "is_iso27001_regulated" in filtered_df1094                else "Exempt"1095            )1096            display_table["ISO 42001"] = (1097                filtered_df["is_iso42001_regulated"].apply(lambda b: "In-Scope ๐Ÿค–" if b else "Exempt")1098                if "is_iso42001_regulated" in filtered_df1099                else "Exempt"1100            )1101            display_table["Business Unit"] = filtered_df["business_unit"]1102            display_table["Revenue Loss / Min"] = filtered_df["revenue_per_minute"].apply(format_inr)1103            display_table["PII Records"] = filtered_df["pii_records_count"].apply(lambda n: f"{n:,}")1104            display_table["Financial Records"] = filtered_df["financial_records_count"].apply(lambda n: f"{n:,}")1105            display_table["Network Hops"] = filtered_df["network_hops_from_internet"]1106            display_table["Estimated EAL"] = filtered_df["estimated_eal_inr"].apply(format_inr)1107 1108            st.dataframe(1109                display_table,1110                use_container_width=True,1111                hide_index=True,1112            )1113 1114            # Vulnerability Inspection Drawer1115            with st.expander("๐Ÿ” Deep Dive: CVEs & Exploitation Scores for Selected Host", expanded=False):1116                host_list = sorted(filtered_df["hostname"].unique().tolist())1117                if host_list:1118                    chosen_host = st.selectbox("Select Host", host_list)1119                    host_record = next((a for a in asset_data if a["hostname"] == chosen_host), None)1120                    if host_record and host_record.get("vulnerabilities"):1121                        v_df = pd.DataFrame(host_record["vulnerabilities"])1122                        v_display = v_df[["cve_id", "cvss_score", "epss_score", "cisa_kev", "patch_available", "is_patched"]].rename(1123                            columns={1124                                "cve_id": "CVE ID",1125                                "cvss_score": "CVSS 3.1",1126                                "epss_score": "EPSS Score",1127                                "cisa_kev": "CISA KEV Listed",1128                                "patch_available": "Patch Available",1129                                "is_patched": "Remediated",1130                            }1131                        )1132                        st.dataframe(v_display, use_container_width=True, hide_index=True)1133                    else:1134                        st.info(f"No active vulnerabilities mapped to {chosen_host}.")1135 1136            # ---------------------------------------------------------------- #1137            # Unauthorized Data Flow & Lineage Tracer Interactive Table1138            # ---------------------------------------------------------------- #1139            st.markdown("---")1140            st.markdown("#### ๐Ÿ•ต๏ธ Unauthorized Data Flow & Lineage Tracer")1141            st.caption(1142                "Breadth-first taint propagation ($A \\rightarrow B \\rightarrow C$) pinpointing indirect data leakage, "1143                "unconsented sub-processor delegations, and causal liability attribution under DPDPA Section 8(4)."1144            )1145 1146            lineage_ok, lineage_data, _ = safe_get(f"{backend_url}/api/lineage/flows")1147            if not lineage_ok or not lineage_data:1148                # Fallback to summary data identified vectors1149                lineage_data = summary_data.get("identified_leak_vectors", []) if ok and summary_data else []1150 1151            if lineage_data:1152                unauth_count = len([1153                    f for f in lineage_data1154                    if not f.get("is_authorized", True) or not f.get("has_user_consent", True) or "Unauthorized" in f.get("detection_status", "")1155                ])1156                if unauth_count > 0:1157                    st.error(1158                        f"๐Ÿšจ **Critical Indirect Data Leak Identified:** {unauth_count} unauthorized delegation flow(s) detected. "1159                        f"Core banking records originating from internal databases are traversing intermediary bridges to unconsented third-party sinks."1160                    )1161                else:1162                    st.success("โœ… All data provenance flows have recorded authorization and user consent boundaries verified.")1163 1164                flow_rows = []1165                for f in lineage_data:1166                    origin = f.get("origin_hostname", "โ€”")1167                    inter = f.get("intermediary_hostname") or "โ€” (Direct)"1168                    dest = f.get("destination_hostname", "โ€”")1169                    pii_exposed = f.get("records_exposed_pii", 0)1170                    fin_exposed = f.get("records_exposed_financial", 0)1171                    is_auth = f.get("is_authorized", True)1172                    has_consent = f.get("has_user_consent", True)1173                    is_leak = not is_auth or not has_consent or "Unauthorized" in f.get("detection_status", "")1174 1175                    consent_status = "Approved โœ…" if (is_auth and has_consent) else "Unauthorized Delegation ๐Ÿšจ"1176                    culprit = f"๐Ÿšจ {dest} (Exfiltration Sink)" if is_leak else "None (Compliant)"1177 1178                    flow_rows.append({1179                        "Originating Asset (A)": origin,1180                        "Intermediate Gateway (B)": inter,1181                        "Exfiltration / Sink Asset (C)": dest,1182                        "Data Transferred (PII / Financial)": f"{pii_exposed:,} PII / {fin_exposed:,} Financial",1183                        "Consent Verification Status": consent_status,1184                        "Detection Status": f.get("detection_status", "Legitimate Flow"),1185                        "Identified Causal Leak Culprit (C)": culprit,1186                    })1187 1188                df_flows = pd.DataFrame(flow_rows)1189                st.dataframe(df_flows, use_container_width=True, hide_index=True)1190            else:1191                st.info("No data lineage flows registered. Ensure attack graph topology and assets are seeded.")1192