CoolFace
Apppublic

mihir2007/Cyber-Risk

sourceHugging Faceupdated 16d agoView on Hugging Face
0likes
optimizer.py221 linesDownload Raw Back to root
1"""2optimizer.py3------------4Budget-Constrained Security Investment Optimizer.5 6Formulates and solves a 0/1 knapsack-style Mixed-Integer Linear Program7(MILP) with PuLP: given a budget B (₹) and a catalogue of candidate8`SecurityControl` rows, choose the subset of controls that maximizes total9financial risk reduction (ΔEAL) without exceeding the budget.10 11Because the underlying likelihood model in `quant_engine.py` combines12control efficacies *multiplicatively* (stacking survival probabilities),13the true joint ΔEAL of a control set is not a simple linear sum of14per-control effects. To keep the optimization tractable as a linear15program while still respecting this non-linearity, we:16 17  1. Compute each control's *standalone* marginal ΔEAL (its effect if it18     were the only control deployed).19  2. Apply a "marginal efficiency discount factor" to controls that20     overlap in scope with many other candidates targeting the same asset21     tier -- an overlap-based proxy for diminishing returns, since several22     controls hardening the same tier will realize less *combined* benefit23     than the sum of their standalone effects.24  3. Solve the MILP on these discounted, linear coefficients.25  4. Re-run the full Monte Carlo simulation once more with the *actual*26     selected control set turned on together, to report the true27     (non-linear) projected EAL and ROSI -- the MILP's linear objective is28     used purely to *select* a good portfolio, not to report the final29     numbers.30"""31 32from __future__ import annotations33 34from dataclasses import dataclass35 36import pulp37from sqlalchemy.orm import Session38 39from models import Asset, SecurityControl40from quant_engine import MonteCarloRiskEngine, N_ITERATIONS41 42# Discount strength: each additional overlapping control (same target tier,43# or "All"-scoped) reduces this control's linear objective coefficient by44# roughly this fraction, modeling saturating security returns.45OVERLAP_DISCOUNT_RATE = 0.1246 47# In-memory caches to make interactive optimization instantaneous (<5ms)48_marginal_cache: dict[str, tuple[float, dict[str, ControlMarginalValue]]] = {}49_joint_simulation_cache: dict[tuple[str, ...], float] = {}50 51 52def clear_optimizer_cache() -> None:53    """Flushes cached marginal values and joint simulations when catalogue/assets change."""54    _marginal_cache.clear()55    _joint_simulation_cache.clear()56 57 58def _get_catalogue_key(assets: list[Asset], candidate_controls: list[SecurityControl]) -> str:59    asset_sig = tuple(sorted((a.id, getattr(a, 'tier', '')) for a in assets))60    ctrl_sig = tuple(sorted((c.code, c.cost_inr, c.likelihood_reduction) for c in candidate_controls))61    return f"{hash(asset_sig)}_{hash(ctrl_sig)}"62 63 64@dataclass65class ControlMarginalValue:66    control: SecurityControl67    standalone_reduction_inr: float68    overlap_count: int69    discount_factor: float70    adjusted_value_inr: float71 72 73@dataclass74class OptimizationResult:75    selected_controls: list[SecurityControl]76    control_marginal_values: dict[str, ControlMarginalValue]77    total_spent_inr: float78    remaining_budget_inr: float79    baseline_eal_inr: float80    projected_eal_inr: float81    net_risk_reduction_inr: float82    rosi_percent: float83 84 85def _scope_overlaps(a: SecurityControl, b: SecurityControl) -> bool:86    """True if two controls' target tiers meaningfully overlap in scope."""87    if a.code == b.code:88        return False89    if a.target_tier == "All" or b.target_tier == "All":90        return True91    return a.target_tier == b.target_tier92 93 94def _get_or_compute_marginal_values(95    assets: list[Asset],96    candidate_controls: list[SecurityControl],97) -> tuple[float, dict[str, ControlMarginalValue]]:98    """99    Computes (and caches) each candidate control's standalone marginal ΔEAL100    and overlap-based diminishing-returns discount. Because candidate controls101    and assets are invariant with respect to the user's budget, this is cached102    to make subsequent knapsack solves run in single-digit milliseconds.103    """104    cat_key = _get_catalogue_key(assets, candidate_controls)105    if cat_key in _marginal_cache:106        return _marginal_cache[cat_key]107 108    baseline_engine = MonteCarloRiskEngine(assets=assets, active_controls=[], n_iterations=N_ITERATIONS)109    baseline_result = baseline_engine.run()110    baseline_eal = baseline_result.total_eal_inr111 112    marginal_values: dict[str, ControlMarginalValue] = {}113 114    for control in candidate_controls:115        engine = MonteCarloRiskEngine(assets=assets, active_controls=[control], n_iterations=2500)116        result = engine.run()117        standalone_reduction = max(baseline_eal - result.total_eal_inr, 0.0)118 119        overlap_count = sum(120            1 for other in candidate_controls if _scope_overlaps(control, other)121        )122        discount_factor = 1.0 / (1.0 + OVERLAP_DISCOUNT_RATE * overlap_count)123        adjusted_value = standalone_reduction * discount_factor124 125        marginal_values[control.code] = ControlMarginalValue(126            control=control,127            standalone_reduction_inr=standalone_reduction,128            overlap_count=overlap_count,129            discount_factor=discount_factor,130            adjusted_value_inr=adjusted_value,131        )132 133    _marginal_cache[cat_key] = (baseline_eal, marginal_values)134    return baseline_eal, marginal_values135 136 137def optimize_budget_allocation(138    db: Session,139    assets: list[Asset],140    candidate_controls: list[SecurityControl],141    budget_inr: float,142) -> OptimizationResult:143    """144    Solves the 0/1 knapsack MILP: maximize discounted ΔEAL subject to a145    total cost constraint, then re-simulates the chosen portfolio jointly146    to report true (non-linear) projected EAL and ROSI.147    """148    baseline_eal, marginal_values = _get_or_compute_marginal_values(assets, candidate_controls)149 150    # --- Build and solve the 0/1 knapsack MILP --------------------------- #151    problem = pulp.LpProblem("Security_Investment_Optimization", pulp.LpMaximize)152 153    decision_vars: dict[str, pulp.LpVariable] = {154        control.code: pulp.LpVariable(f"select_{control.code}", cat="Binary")155        for control in candidate_controls156    }157 158    # Objective: maximize total discounted risk reduction (ΔEAL, in ₹).159    problem += pulp.lpSum(160        decision_vars[control.code] * marginal_values[control.code].adjusted_value_inr161        for control in candidate_controls162    ), "Total_Discounted_Risk_Reduction"163 164    # Constraint: total deployment cost must not exceed the budget.165    problem += (166        pulp.lpSum(decision_vars[control.code] * control.cost_inr for control in candidate_controls)167        <= budget_inr,168        "Budget_Constraint",169    )170 171    solver = pulp.PULP_CBC_CMD(msg=False)172    problem.solve(solver)173 174    selected_controls = [175        control176        for control in candidate_controls177        if decision_vars[control.code].value() == 1178    ]179    total_spent = sum(c.cost_inr for c in selected_controls)180 181    # --- Re-simulate the actual joint portfolio for true reported numbers --#182    selected_tuple = tuple(sorted(c.code for c in selected_controls))183    if not selected_controls:184        projected_eal = baseline_eal185    elif selected_tuple in _joint_simulation_cache:186        projected_eal = _joint_simulation_cache[selected_tuple]187    else:188        joint_engine = MonteCarloRiskEngine(assets=assets, active_controls=selected_controls, n_iterations=N_ITERATIONS)189        joint_result = joint_engine.run()190        projected_eal = joint_result.total_eal_inr191        _joint_simulation_cache[selected_tuple] = projected_eal192 193    net_risk_reduction = baseline_eal - projected_eal194    rosi_percent = (195        ((net_risk_reduction - total_spent) / total_spent) * 100.0 if total_spent > 0 else 0.0196    )197 198    return OptimizationResult(199        selected_controls=selected_controls,200        control_marginal_values=marginal_values,201        total_spent_inr=total_spent,202        remaining_budget_inr=budget_inr - total_spent,203        baseline_eal_inr=baseline_eal,204        projected_eal_inr=projected_eal,205        net_risk_reduction_inr=net_risk_reduction,206        rosi_percent=rosi_percent,207    )208 209 210def prewarm_optimizer_cache(db: Session, presets: list[float] | None = None) -> None:211    """Pre-computes marginal values and popular budget presets so interactive UI responds in <5ms."""212    assets = db.query(Asset).all()213    candidate_controls = db.query(SecurityControl).all()214    if not assets or not candidate_controls:215        return216    _get_or_compute_marginal_values(assets, candidate_controls)217    default_presets = presets or [2_000_000, 5_000_000, 10_000_000, 20_000_000, 35_000_000]218    for p in default_presets:219        optimize_budget_allocation(db, assets, candidate_controls, float(p))220 221