Metadata-Version: 2.4
Name: pycrq
Version: 1.1.0
Summary: A production-quality Open FAIR library for quantitative cyber risk analysis
License: MIT
Project-URL: Homepage, https://github.com/securemetrics/pyCRQ
Project-URL: Repository, https://github.com/securemetrics/pyCRQ
Project-URL: Issues, https://github.com/securemetrics/pyCRQ/issues
Keywords: cyber risk,FAIR,risk quantification,monte carlo,information security
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Topic :: Security
Classifier: Topic :: Office/Business :: Financial
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24.0
Requires-Dist: scipy>=1.10.0
Requires-Dist: matplotlib>=3.7.0
Dynamic: license-file


# pyCRQ

<img width="1622" height="544" alt="pyCRQ Hero" src="https://github.com/user-attachments/assets/a1949375-a4ef-4dc1-84ea-702ad63d8f66" />

[![PyPI version](https://img.shields.io/pypi/v/pycrq.svg)](https://pypi.org/project/pycrq/)
[![Python](https://img.shields.io/pypi/pyversions/pycrq.svg)](https://pypi.org/project/pycrq/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A Python library for quantitative cyber risk analysis using the **Open FAIR** standard. Model threats as probability distributions, run Monte Carlo simulations, and get dollar-denominated risk estimates with full uncertainty ranges.

---

## Table of Contents

1. [How FAIR Works](#how-fair-works)
2. [Installation](#installation)
3. [Your First Scenario in 5 Minutes](#your-first-scenario-in-5-minutes)
4. [Core Concepts](#core-concepts)
   - [Distributions — Expressing Uncertainty](#distributions--expressing-uncertainty)
   - [Frequency Inputs — How Often Does It Happen?](#frequency-inputs--how-often-does-it-happen)
   - [Loss Inputs — How Much Does It Cost?](#loss-inputs--how-much-does-it-cost)
5. [ScenarioBuilder Reference](#scenariobuilder-reference)
6. [Running Simulations](#running-simulations)
7. [Analysis & Reporting](#analysis--reporting)
8. [Common Recipes](#common-recipes)
   - [Risk Register (Multiple Scenarios)](#risk-register-multiple-scenarios)
   - [Control ROI / ROSI](#control-roi--rosi)
   - [Sensitivity Analysis](#sensitivity-analysis)
   - [Export to JSON or CSV](#export-to-json-or-csv)
9. [API Reference](#api-reference)
10. [Examples](#examples)

---

## How FAIR Works

FAIR (Factor Analysis of Information Risk) breaks down cyber risk into two questions:

1. **How often will a loss event occur?** → Loss Event Frequency (LEF)
2. **How much will it cost when it does?** → Loss Magnitude (LM)

Multiply those together and you get **Annual Loss Exposure (ALE)** — the expected annual cost of the risk in dollars.

```
ALE  =  Loss Event Frequency  ×  Loss Magnitude
```

FAIR further decomposes each side of that equation:

```
                         ┌─────────────────────────────────────────────┐
                         │              FAIR Risk Model                 │
                         └─────────────────────────────────────────────┘

            Annual Loss Exposure (ALE)
                       │
          ┌────────────┴───────────────┐
          │                            │
  Loss Event Frequency           Loss Magnitude (LM)
        (LEF)                          │
          │                   ┌────────┴────────────┐
    ┌─────┴──────┐        Primary Loss          Secondary Loss
    │            │             │                     │
   TEF        Vuln    (Productivity, Response,  (SLEF × SLM)
    │            │     Replacement, Competitive
  ┌─┴──┐     ┌──┴──┐   Advantage, Fines, Reputation)
  CF  PoA  TCap  CS
```

| Term | Full Name | What It Means |
|------|-----------|---------------|
| LEF | Loss Event Frequency | How many loss events occur per year |
| TEF | Threat Event Frequency | How often the threat attempts an action |
| CF | Contact Frequency | How often the threat contacts the asset |
| PoA | Probability of Action | Given contact, how likely the threat acts |
| Vuln | Vulnerability | Probability a threat event becomes a loss event |
| TCap | Threat Capability | Attacker skill (0–100 score) |
| CS | Control Strength | Defensive strength (0–100 score) |
| LM | Loss Magnitude | Financial cost per loss event |
| ALE | Annual Loss Exposure | Expected annual cost = LEF × LM |

**You don't need all of these.** Most scenarios only need 3–4 inputs. See [Frequency Inputs](#frequency-inputs--how-often-does-it-happen) for how to choose.

---

## Installation

```bash
pip install pycrq
```

**From source:**
```bash
git clone https://github.com/securemetrics/pyCRQ.git
cd pyCRQ
pip install -e .
```

**Requirements:** Python 3.9+, numpy, scipy, matplotlib (all installed automatically).

---

## Your First Scenario in 5 Minutes

Here is a complete working example. Copy and run it.

```python
from pycrq import ScenarioBuilder, FAIRSimulator, pert, scenario_report

# Step 1: Describe the scenario
scenario = (
    ScenarioBuilder("Ransomware Attack")
    .asset("Customer Database", "Information")
    .threat("External Hacker", "External Adversarial")

    # Step 2: How often? (TEF = attempts per year, Vuln = % of attempts that succeed)
    .tef(pert(low=1, mode=3, high=8))             # 1 to 8 attacks per year
    .vulnerability(pert(low=0.10, mode=0.25, high=0.55))  # 10–55% success rate

    # Step 3: How much does it cost per event?
    .primary_loss(pert(low=50_000, mode=250_000, high=1_500_000))

    .build()
)

# Step 4: Simulate
simulator = FAIRSimulator(n_simulations=10_000, seed=42)
result = simulator.simulate(scenario)

# Step 5: Read the results
print(scenario_report(result))
print(f"\nMean ALE:   ${result.mean_ale:,.0f}")
print(f"90th pct:   ${result.percentile(90):,.0f}")
print(f"VaR (95%):  ${result.var(0.95):,.0f}")
```

**Output looks like:**
```
============================================================
 FAIR Simulation Report — Ransomware Attack
============================================================
 Simulations:    10,000
 Mean ALE:       $245,312
 Median ALE:     $142,891
 P90 ALE:        $621,440
 VaR (95%):      $788,230
 CVaR (95%):     $1,104,560
 Risk Level:     HIGH
============================================================
```

That's it. The rest of this guide explains how to build more detailed and accurate models.

---

## Core Concepts

### Distributions — Expressing Uncertainty

In FAIR, every input is a **range**, not a single number. You express uncertainty using probability distributions. The most common one is `pert()`.

#### The PERT Distribution (Start Here)

`pert(low, mode, high)` is perfect for expert estimates: "I think it's usually around X, but could be as low as Y or as high as Z."

```python
from pycrq import pert

# Threat event frequency: usually 3/year, but anywhere from 1 to 8
tef = pert(low=1, mode=3, high=8)

# Vulnerability: usually 25%, but could be 10% to 55%
vuln = pert(low=0.10, mode=0.25, high=0.55)

# Loss magnitude: usually $250K, but could be $50K to $1.5M
loss = pert(low=50_000, mode=250_000, high=1_500_000)
```

#### All Available Distributions

| Distribution | Function | Best For |
|---|---|---|
| PERT | `pert(low, mode, high)` | Expert elicitation — "optimistic / most-likely / pessimistic" |
| Triangular | `triangular(low, mode, high)` | Like PERT but with sharper edges; use when the tails matter more |
| Uniform | `uniform(low, high)` | Complete uncertainty between two bounds |
| Normal | `normal(mean, std)` | Symmetric uncertainty around a known mean |
| LogNormal | `lognormal(meanlog, sdlog)` | Right-skewed costs and frequencies (never goes negative) |
| Beta | `beta(alpha, beta)` | Probabilities (0–1); flexible shape |
| Poisson | `poisson(lam)` | Discrete event counts when you know the expected annual rate |
| Constant | `constant(value)` | Fix a factor to a known value (e.g., during sensitivity analysis) |

```python
from pycrq import pert, normal, lognormal, uniform, triangular, constant, beta, poisson
from pycrq import LogNormalDistribution

# PERT — workhorse for expert estimates
freq = pert(1, 3, 8)

# Normal — symmetric; good for aggregated costs
cost = normal(mean=500_000, std=100_000)

# LogNormal — right-skewed; great for breach costs that can spike high
cost = LogNormalDistribution.from_moments(mean=500_000, std=300_000)

# Uniform — "anywhere in this range is equally likely"
vuln = uniform(low=0.1, high=0.4)

# Triangular — sharper peak than PERT
freq = triangular(low=1, mode=4, high=12)

# Constant — pin a factor to an exact value
exact = constant(value=50_000)

# Beta — vulnerability as a 0–1 probability with flexible skew
vuln = beta(alpha=2, beta=5)  # skewed toward low values

# Poisson — discrete event counts; mean=3.5 events/year
freq = poisson(lam=3.5)
```

**Quick tip:** All distributions expose `.mean()`, `.std()`, `.percentile(p)`, and `.sample(n)` so you can inspect them before simulating:

```python
d = pert(1, 3, 8)
print(d.mean())          # 3.0
print(d.percentile(90))  # ~5.8
print(d.sample(5))       # array([2.1, 4.7, 3.3, 1.8, 6.2])
```

---

### Frequency Inputs — How Often Does It Happen?

This is the most flexible part of the model. You can provide frequency at three different levels of detail. Pick whichever fits your data.

#### Option A — Direct LEF (simplest)

Use `.lef()` when you have a single estimate of how many **loss events** occur per year — you've already folded in the probability of success.

```python
scenario = (
    ScenarioBuilder("Phishing Campaign")
    .asset("Email System", "System")
    .threat("Phishing Group", "External Adversarial")
    .lef(pert(0.5, 1.5, 4))   # 0.5 to 4 successful phishing incidents/year
    .primary_loss(pert(10_000, 80_000, 400_000))
    .build()
)
```

> ⚠️ **Trade-off:** `.lef()` is quick to model, but bypasses TEF and Vulnerability entirely, so per-factor sensitivity analysis (e.g., "what if we improved our controls?") is unavailable. Use `.tef()` + `.vulnerability()` when you want to model control effectiveness.

---

#### Option B — TEF + Vulnerability (recommended)

This is the standard FAIR approach. You separately model how often attacks are **attempted** and how often they **succeed**.

```python
scenario = (
    ScenarioBuilder("Credential Stuffing")
    .asset("Login Portal", "System")
    .threat("Bot Network", "External Adversarial")
    .tef(pert(50, 200, 1000))         # 50–1,000 attempts per year
    .vulnerability(pert(0.01, 0.05, 0.15))  # 1–15% succeed
    .primary_loss(pert(5_000, 30_000, 200_000))
    .build()
)
```

---

#### Option C — Full Decomposition (most detail)

Break TEF down into Contact Frequency × Probability of Action, and model Vulnerability from Threat Capability vs. Control Strength scores (0–100). Best for comparing control investment scenarios.

```python
scenario = (
    ScenarioBuilder("Insider Threat")
    .asset("Financial Records", "Information")
    .threat("Malicious Insider", "Internal Adversarial")
    .contact_frequency(pert(10, 30, 60))         # contacts/year
    .probability_of_action(pert(0.05, 0.15, 0.30))  # acts on 5–30% of contacts
    .threat_capability(pert(30, 55, 75))          # attacker skill score
    .control_strength(pert(50, 65, 80))           # control effectiveness score
    .primary_loss(pert(100_000, 400_000, 2_000_000))
    .build()
)
```

When both `threat_capability` and `control_strength` are provided, vulnerability is computed via a logistic function:

```
Vuln = sigmoid(6 × (ThreatCapability − ControlStrength) / 100)
```

This means if your controls are stronger than the threat (CS > TCap), vulnerability drops sharply — and vice versa.

#### Priority Order

The simulator uses the first complete input it finds:

```
LEF  →  TEF  →  CF × PoA  →  CF alone
```

For vulnerability:
```
direct vulnerability  →  TCap + CS (logistic)  →  TCap alone  →  CS alone  →  Uniform(0,1)
```

---

### Loss Inputs — How Much Does It Cost?

#### Option A — Single primary loss

The simplest approach — one distribution covers the total cost per event:

```python
.primary_loss(pert(50_000, 250_000, 1_500_000))
```

#### Option B — By loss category

Break loss into FAIR's six primary categories. These are summed automatically:

```python
.loss_productivity(pert(50_000, 200_000, 750_000))    # Revenue/output lost
.loss_response(pert(30_000, 100_000, 400_000))         # IR, forensics, legal
.loss_replacement(pert(10_000, 50_000, 200_000))       # Hardware/software replacement
.loss_competitive_advantage(pert(0, 25_000, 150_000))  # Market position impact
.loss_fines_judgments(pert(0, 75_000, 500_000))        # Regulatory fines
.loss_reputation(pert(0, 50_000, 300_000))             # Brand damage
```

#### Adding Secondary Loss

Secondary loss models costs triggered by external parties reacting to the event (e.g., regulators, customers, press):

```python
.secondary_loss(
    slef=pert(0.10, 0.25, 0.50),          # probability secondary event occurs
    slm=pert(100_000, 500_000, 3_000_000) # cost when it does
)
```

---

## ScenarioBuilder Reference

All methods return `self` for chaining. Call `.build()` at the end.

### Asset & Threat

| Method | Description |
|--------|-------------|
| `.asset(name, asset_type, description)` | Define the asset at risk. `asset_type`: `"Information"`, `"System"`, `"Service"`, `"Physical"`, `"Person"`, `"Other"` |
| `.with_asset(asset_obj)` | Attach a pre-built `Asset` object |
| `.threat(name, threat_type, description)` | Define the threat agent. `threat_type`: `"External Adversarial"`, `"Internal Adversarial"`, `"External Non-Adversarial"`, `"Internal Non-Adversarial"`, `"Natural"` |
| `.with_threat(threat_obj)` | Attach a pre-built `ThreatAgent` object |
| `.add_control(name, description, control_type)` | Document a control (e.g., `"Preventive"`, `"Detective"`) |

### Metadata

| Method | Description |
|--------|-------------|
| `.describe(text)` | Free-text narrative for the scenario |
| `.tag("tag1", "tag2")` | Add string tags for filtering/grouping |
| `.effect("Confidentiality")` | CIA impact type: `"Confidentiality"`, `"Integrity"`, `"Availability"`, `"Safety"` |
| `.time_horizon(years)` | Simulation time window in years (default `1.0`) |

### Frequency

| Method | Use When | Notes |
|--------|----------|-------|
| `.lef(dist)` | You have a direct estimate of annual loss events | Bypasses TEF/Vuln; no per-factor sensitivity analysis |
| `.tef(dist)` | You know how often attacks are attempted | Combine with `.vulnerability()` |
| `.contact_frequency(dist)` | You know how often the threat encounters the asset | Multiply by `.probability_of_action()` to get TEF |
| `.probability_of_action(dist)` | Given contact, probability the threat acts | Used with `.contact_frequency()` |
| `.threat_capability(dist)` | Attacker skill as a 0–100 score | Combined with `.control_strength()` via logistic function |
| `.control_strength(dist)` | Control effectiveness as a 0–100 score | Combined with `.threat_capability()` via logistic function |
| `.vulnerability(dist)` | Direct probability (0–1) that an attempt succeeds | Simpler than TCap/CS; use when you can estimate it directly |

### Loss Magnitude

| Method | Description |
|--------|-------------|
| `.primary_loss(dist)` | Total primary loss per event (alternative to individual categories) |
| `.loss_productivity(dist)` | Revenue or output lost |
| `.loss_response(dist)` | Incident response, forensics, legal costs |
| `.loss_replacement(dist)` | Asset repair or replacement costs |
| `.loss_competitive_advantage(dist)` | Impact on market position |
| `.loss_fines_judgments(dist)` | Regulatory fines and legal judgments |
| `.loss_reputation(dist)` | Brand and reputation damage |
| `.secondary_loss(slef, slm)` | Secondary loss event frequency + magnitude |

---

## Running Simulations

```python
from pycrq import FAIRSimulator

simulator = FAIRSimulator(
    n_simulations=10_000,  # more = more stable results; 10K is a good default
    seed=42,               # set for reproducibility; omit for random results
)

# Single scenario
result = simulator.simulate(scenario)

# Entire risk register
agg = simulator.simulate_register(register)
```

### Reading Results

```python
# Key statistics
result.mean_ale              # mean annual loss exposure
result.std_ale               # standard deviation
result.percentile(10)        # 10th percentile ALE
result.percentile(50)        # median ALE
result.percentile(90)        # 90th percentile ALE
result.var(0.95)             # Value at Risk (95%)
result.cvar(0.95)            # Conditional VaR / Expected Shortfall (95%)

# Raw arrays (one value per simulation)
result.annual_loss_exposure  # np.ndarray, shape (n_simulations,)
result.loss_event_frequency  # np.ndarray
result.loss_magnitude        # np.ndarray
result.threat_event_frequency  # np.ndarray (None if .lef() was used)
result.vulnerability           # np.ndarray (None if .lef() was used)

# Full summary dict
result.summary()
```

---

## Analysis & Reporting

### Print a Report

```python
from pycrq import scenario_report, register_report

print(scenario_report(result))              # single scenario
print(scenario_report(result, verbose=True))  # includes per-category breakdown
print(register_report(agg))                # full risk register
```

### Compute Metrics

```python
from pycrq import compute_metrics, classify_risk_level, format_currency

metrics = compute_metrics(result)
print(metrics.mean_ale)
print(metrics.var_95)
print(metrics.risk_level)   # "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"

# Classify any dollar value
level = classify_risk_level(250_000)   # "MEDIUM"

# Format numbers for display
format_currency(1_500_000)   # "$1.50M"
format_currency(85_000)      # "$85.0K"
```

### Compare Scenarios Side-by-Side

```python
from pycrq import rank_scenarios, summary_table

# Rank by mean ALE (highest first)
ranked = rank_scenarios(results, by="mean_ale")
for name, value in ranked:
    print(f"{name}: {format_currency(value)}")

# ASCII comparison table
print(summary_table(results))
```

### Validate Before You Simulate

```python
from pycrq import validate_scenario

warnings = validate_scenario(scenario)
for w in warnings:
    print(f"[WARN] {w}")
```

This catches common issues like missing frequency inputs, unrealistic vulnerability values, and missing secondary loss definitions.

### Plots

```python
from pycrq import plot_ale_distribution, plot_scenario_comparison, plot_risk_matrix

# ALE histogram with percentile markers
fig = plot_ale_distribution(result, title="Ransomware ALE", show_percentiles=True)
fig.savefig("ale_distribution.png", dpi=150)

# Bar chart comparing multiple scenarios
fig = plot_scenario_comparison(results, metric="mean_ale")

# Risk matrix
fig = plot_risk_matrix(results)
```

---

## Common Recipes

### Risk Register (Multiple Scenarios)

Model an entire threat landscape and analyze the portfolio.

```python
from pycrq import RiskRegister, FAIRSimulator, register_report, rank_scenarios

register = RiskRegister(name="Enterprise Cyber 2026")

register.add_scenario(
    ScenarioBuilder("Ransomware")
    .asset("File Servers", "System")
    .threat("Ransomware Gang", "External Adversarial")
    .tef(pert(0.5, 1, 3))
    .vulnerability(pert(0.2, 0.4, 0.7))
    .primary_loss(pert(200_000, 800_000, 5_000_000))
    .build()
)

register.add_scenario(
    ScenarioBuilder("Phishing — Credential Theft")
    .asset("Email / Identity", "Information")
    .threat("Phishing Group", "External Adversarial")
    .tef(pert(5, 15, 40))
    .vulnerability(pert(0.02, 0.05, 0.12))
    .primary_loss(pert(10_000, 60_000, 300_000))
    .build()
)

register.add_scenario(
    ScenarioBuilder("Insider Data Theft")
    .asset("Customer Records", "Information")
    .threat("Malicious Employee", "Internal Adversarial")
    .lef(pert(0.1, 0.3, 1))   # historical rate — skip TEF/Vuln decomposition
    .primary_loss(pert(50_000, 250_000, 1_000_000))
    .secondary_loss(
        slef=pert(0.2, 0.4, 0.7),
        slm=pert(100_000, 500_000, 2_000_000)
    )
    .build()
)

simulator = FAIRSimulator(n_simulations=10_000, seed=42)
agg = simulator.simulate_register(register)

print(register_report(agg))

# Rank scenarios by mean ALE
from pycrq import rank_scenarios
ranked = rank_scenarios(agg.scenario_results, by="mean_ale")
for name, value in ranked:
    print(f"  {name}: ${value:,.0f}")
```

---

### Control ROI / ROSI

Compare risk before and after a control, then calculate return on security investment.

```python
from pycrq import ScenarioBuilder, FAIRSimulator, control_roi, compute_risk_reduction, pert

simulator = FAIRSimulator(n_simulations=10_000, seed=42)

# Before: no MFA
before = (
    ScenarioBuilder("Account Takeover — No MFA")
    .asset("Customer Portal", "System")
    .threat("Credential Stuffers", "External Adversarial")
    .tef(pert(20, 60, 200))
    .vulnerability(pert(0.05, 0.15, 0.35))
    .primary_loss(pert(5_000, 40_000, 200_000))
    .build()
)

# After: MFA deployed — stronger controls, lower vulnerability
after = (
    ScenarioBuilder("Account Takeover — With MFA")
    .asset("Customer Portal", "System")
    .threat("Credential Stuffers", "External Adversarial")
    .tef(pert(20, 60, 200))
    .vulnerability(pert(0.01, 0.03, 0.08))   # MFA cuts success rate dramatically
    .primary_loss(pert(5_000, 40_000, 200_000))
    .build()
)

before_result = simulator.simulate(before)
after_result  = simulator.simulate(after)

# Risk reduction
reduction = compute_risk_reduction(before_result, after_result)
print(f"ALE reduction: ${reduction['mean_ale_reduction_abs']:,.0f}")
print(f"Reduction %:   {reduction['mean_ale_reduction_pct']:.1f}%")

# ROSI — MFA costs $60K/year to operate
roi = control_roi(before_result, after_result, control_cost=60_000)
print(f"ROSI:            {roi['rosi_pct']:.0f}%")
print(f"Net benefit:     ${roi['net_benefit']:,.0f}")
print(f"Break-even:      {roi['break_even_years']:.1f} years")
print(f"Recommended:     {roi['recommended']}")
```

---

### Sensitivity Analysis

Sweep a parameter across a range to see how much it drives your ALE. Useful for prioritizing control investments.

```python
from pycrq import ScenarioBuilder, FAIRSimulator, sensitivity_analysis, pert, constant

def build_scenario(control_strength_value):
    return (
        ScenarioBuilder("Phishing — Control Sweep")
        .asset("Email", "Information")
        .threat("Phisher", "External Adversarial")
        .contact_frequency(pert(10, 30, 80))
        .probability_of_action(pert(0.2, 0.4, 0.7))
        .threat_capability(pert(40, 60, 80))
        .control_strength(constant(control_strength_value))  # sweep this
        .primary_loss(pert(10_000, 75_000, 400_000))
        .build()
    )

results = sensitivity_analysis(
    scenario_factory=build_scenario,
    param_name="control_strength",
    param_values=[20, 40, 60, 80, 95],
    n_simulations=5_000,
    seed=42,
)

for r in results:
    cs = r.scenario_name.split("=")[-1]
    print(f"Control Strength {cs}: Mean ALE = ${r.mean_ale:,.0f}")
```

---

### Bootstrap Confidence Intervals

Quantify how much uncertainty exists in your simulation estimates themselves.

```python
from pycrq import bootstrap_confidence_interval

lower, upper = bootstrap_confidence_interval(
    result,
    statistic="mean",    # or "p90", "var_95"
    confidence=0.95,
    n_bootstrap=1_000,
)
print(f"Mean ALE 95% CI: ${lower:,.0f} – ${upper:,.0f}")
```

---

### Calibrate from Expert P10/P90

If a SME gives you confidence bounds rather than a three-point estimate, use `calibrate_pert_from_confidence` to convert them:

```python
from pycrq import calibrate_pert_from_confidence, pert

# "I'm 90% sure the loss is between $50K and $2M"
low, mode, high = calibrate_pert_from_confidence(p10=50_000, p90=2_000_000)
loss_dist = pert(low, mode, high)
```

---

### Export to JSON or CSV

```python
from pycrq import export_json, export_csv

export_json(result, "outputs/ransomware.json")
export_csv(result, "outputs/ransomware.csv")

# The JSON captures all metadata and is round-trippable
```

---

## API Reference

### Distributions

| Function | Parameters | Notes |
|----------|-----------|-------|
| `pert(low, mode, high, lam=4.0)` | low ≤ mode ≤ high | Best default choice for expert estimates |
| `normal(mean, std)` | std ≥ 0 | Symmetric; can go negative — use for aggregated costs |
| `lognormal(meanlog, sdlog)` | sdlog > 0 | Log-space params; use `from_moments()` for real-space |
| `uniform(low, high)` | high ≥ low | Flat uncertainty |
| `triangular(low, mode, high)` | low ≤ mode ≤ high | Like PERT with less weight on mode |
| `constant(value)` | any float | Pins a factor to an exact value |
| `beta(alpha, beta, low=0, high=1)` | alpha, beta > 0 | Flexible 0–1 probability shape |
| `poisson(lam)` | lam > 0 | Discrete counts; mean = variance = lam |
| `from_dict(d)` | dict with `"type"` key | Deserialize any distribution from its `to_dict()` output |
| `LogNormalDistribution.from_moments(mean, std)` | mean, std > 0 | Construct from real-space moments |

---

### FAIRSimulator

```python
FAIRSimulator(n_simulations=10_000, seed=None)

.simulate(scenario)           # → SimulationResult
.simulate_register(register)  # → AggregateSimulationResult
```

**SimulationResult:**

| Attribute / Method | Type | Description |
|---|---|---|
| `annual_loss_exposure` | `np.ndarray` | ALE per simulation |
| `loss_event_frequency` | `np.ndarray` | LEF per simulation |
| `loss_magnitude` | `np.ndarray` | LM per simulation |
| `threat_event_frequency` | `np.ndarray \| None` | TEF (None when `.lef()` path used) |
| `vulnerability` | `np.ndarray \| None` | Vuln (None when `.lef()` path used) |
| `primary_loss` | `np.ndarray` | Primary loss component |
| `secondary_loss` | `np.ndarray \| None` | Secondary loss (None if not modelled) |
| `loss_by_category` | `dict` | Per-category arrays when using category methods |
| `mean_ale` | `float` | Mean ALE |
| `std_ale` | `float` | Standard deviation of ALE |
| `.percentile(p)` | `float` | p-th percentile of ALE (0–100) |
| `.var(confidence)` | `float` | Value-at-Risk |
| `.cvar(confidence)` | `float` | Conditional VaR / Expected Shortfall |
| `.summary()` | `dict` | All key statistics as a dict |

---

### Analysis Functions

```python
from pycrq import (
    compute_metrics,           # → RiskMetrics from SimulationResult
    classify_risk_level,       # classify_risk_level(mean_ale) → "LOW"|"MEDIUM"|"HIGH"|"CRITICAL"
    rank_scenarios,            # rank_scenarios(results, by="mean_ale") → [(name, value), ...]
    compute_risk_reduction,    # compute_risk_reduction(before, after) → dict
    control_roi,               # control_roi(before, after, control_cost) → dict
    sensitivity_analysis,      # sensitivity_analysis(factory_fn, param_name, values, ...) → list
    bootstrap_confidence_interval,  # (result, statistic, confidence, n_bootstrap) → (lower, upper)
)
```

---

### Utilities

```python
from pycrq import (
    validate_scenario,               # → List[str] of advisory warnings
    calibrate_pert_from_confidence,  # (p10, p90) → (low, mode, high)
    frequency_to_rate,               # "monthly" → 12.0, "weekly" → 52.0, etc.
    annualize,                       # annualize(5_000, "monthly") → 60_000
    summary_table,                   # summary_table(results) → ASCII str
)
```

---

## Examples

Ready-to-run examples are in the `examples/` directory:

| File | What It Shows |
|------|--------------|
| `examples/01_basic_scenario.py` | Full ransomware scenario — PERT distributions, full report, bootstrap CI |
| `examples/02_risk_register.py` | 5-scenario risk register, aggregate portfolio, rankings |
| `examples/03_control_roi.py` | Before/after MFA comparison, ROSI, break-even analysis |
| `examples/04_sensitivity_analysis.py` | Threat capability sweep 20→80, control strength sweep, logistic vulnerability |

```bash
python examples/01_basic_scenario.py
python examples/02_risk_register.py
python examples/03_control_roi.py
python examples/04_sensitivity_analysis.py
```

---

## License

MIT License. See [LICENSE](LICENSE) for details.

Open FAIR is a standard published by The Open Group. This library is an independent implementation and is not affiliated with or endorsed by The Open Group.
