Skip to main content

A production-quality Open FAIR library for quantitative cyber risk analysis

Project description

Open FAIR Python Library

A production-quality implementation of the Open FAIR (Factor Analysis of Information Risk) standard for quantitative cyber risk analysis. This library provides Monte Carlo simulation, risk metrics, scenario building, control ROI analysis, sensitivity analysis, and rich reporting — all in a clean, well-typed Python API.


Table of Contents

  1. Overview
  2. Installation
  3. Quick Start
  4. Architecture
  5. API Reference
  6. Examples
  7. Open FAIR Model Overview
  8. License

Overview

Open FAIR is a risk taxonomy and measurement standard published by The Open Group. It provides a structured way to decompose and quantify information risk in financial terms. This library implements the core FAIR ontology and adds:

  • Monte Carlo simulation with configurable iteration counts and seeds
  • Eight probability distributions: PERT, Normal, LogNormal, Uniform, Triangular, Constant, Beta, Poisson
  • Fluent scenario builder for readable, maintainable risk models
  • Risk registers for portfolio-level analysis
  • Control ROI (ROSI) calculation
  • Sensitivity analysis for one-at-a-time parameter sweeps
  • Bootstrap confidence intervals for simulation uncertainty
  • Text reports, JSON/CSV export, and matplotlib visualisations
  • Calibration helpers for converting expert estimates to distributions

Installation

Prerequisites

  • Python 3.9 or later
  • pip

Install Dependencies

pip install -r requirements.txt

requirements.txt:

numpy>=1.24.0
scipy>=1.10.0
matplotlib>=3.7.0

Install as a Package (Optional)

If you want to import pycrq from anywhere on your system, install it in development mode from the repository root:

pip install -e .

Or copy the pycrq/ directory into your project.


Quick Start

from pycrq import (
    ScenarioBuilder, FAIRSimulator,
    pert, scenario_report, compute_metrics, format_currency
)

# 1. Build a scenario using the fluent API
scenario = (
    ScenarioBuilder("Ransomware Attack")
    .asset("Customer Database", "Information", "PII and payment records")
    .threat("External Hacker", "External Adversarial", "Financially motivated group")
    .add_control("EDR Solution", "Endpoint detection and response", "Detective")
    .tef(pert(low=1, mode=3, high=8))           # 1-8 attempts/year
    .vulnerability(pert(low=0.10, mode=0.25, high=0.55))
    .loss_productivity(pert(50_000, 200_000, 750_000))
    .loss_response(pert(30_000, 100_000, 400_000))
    .loss_reputation(pert(0, 75_000, 500_000))
    .secondary_loss(
        slef=pert(0.05, 0.15, 0.35),
        slm=pert(50_000, 250_000, 2_000_000)
    )
    .build()
)

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

# 3. Report
print(scenario_report(result))

# 4. Access metrics programmatically
metrics = compute_metrics(result)
print(f"Mean ALE: {format_currency(metrics.mean_ale)}")
print(f"Risk Level: {metrics.risk_level}")
print(f"VaR (95%): {format_currency(metrics.var_95)}")

Architecture

pycrq/
  __init__.py        — Public API exports, version
  distributions.py   — Probability distributions (PERT, Normal, etc.)
  ontology.py        — FAIR data model (Asset, ThreatAgent, FAIRScenario, ...)
  simulation.py      — Monte Carlo engine (FAIRSimulator, SimulationResult)
  scenarios.py       — Fluent ScenarioBuilder API
  analysis.py        — Risk metrics, ranking, ROI, sensitivity
  reporting.py       — Text/JSON/CSV reports and matplotlib plots
  utils.py           — Calibration, validation, unit conversions

examples/
  01_basic_scenario.py       — Single ransomware scenario
  02_risk_register.py        — 5-scenario risk register
  03_control_roi.py          — MFA implementation ROI
  04_sensitivity_analysis.py — Threat capability sweep

API Reference

Distributions

All distributions inherit from Distribution and implement:

Method Description
.sample(n) Draw n samples as np.ndarray
.mean() Theoretical mean
.std() Theoretical standard deviation
.percentile(p) The p-th percentile (0–100)
.to_dict() JSON-serializable representation

Available distributions:

from pycrq import (
    pert, normal, lognormal, uniform, triangular, constant, beta, poisson
)

# PERT — the workhorse for expert elicitation
tef_dist = pert(low=1, mode=3, high=8)          # (lam=4.0 default)

# Normal
loss_dist = normal(mean=500_000, std=100_000)

# LogNormal — from real-space moments
from pycrq import LogNormalDistribution
loss_dist = LogNormalDistribution.from_moments(mean=500_000, std=300_000)

# Others
vuln_dist  = uniform(low=0.1, high=0.4)
freq_dist  = triangular(low=1, mode=4, high=12)
fixed_val  = constant(value=50_000)
score_dist = beta(alpha=2, beta=5, low=0, high=100)
count_dist = poisson(lam=3.5)

# Deserialize from dict
from pycrq import from_dict
d = tef_dist.to_dict()   # {"type": "pert", "low": 1, "mode": 3, "high": 8, "lam": 4.0}
tef_dist2 = from_dict(d)

Ontology (Data Model)

from pycrq import (
    Asset, ThreatAgent, Control,
    FrequencyFactors, LossMagnitudeFactors,
    FAIRScenario, RiskRegister,
    AssetType, ThreatType, EffectType
)

# Asset
asset = Asset(
    name="Customer Database",
    asset_type=AssetType.INFORMATION,
    description="Stores PII and payment records.",
    tags=["critical", "pii"],
)

# ThreatAgent
threat = ThreatAgent(
    name="External Hacker",
    threat_type=ThreatType.EXTERNAL_ADVERSARIAL,
)

# FrequencyFactors
ff = FrequencyFactors(
    tef=pert(1, 3, 8),
    vulnerability=pert(0.1, 0.25, 0.55),
)

# OR decomposed as:
ff = FrequencyFactors(
    contact_frequency=pert(5, 10, 20),
    probability_of_action=pert(0.2, 0.4, 0.7),
    threat_capability=pert(40, 60, 80),
    control_strength=pert(50, 65, 80),
)

# LossMagnitudeFactors
lmf = LossMagnitudeFactors(
    primary_loss=pert(100_000, 500_000, 2_000_000),
    # OR by category:
    productivity=pert(50_000, 200_000, 750_000),
    response=pert(30_000, 100_000, 400_000),
    fines_judgments=pert(0, 75_000, 500_000),
    # Secondary loss:
    secondary_loss_event_frequency=pert(0.1, 0.25, 0.5),
    secondary_loss_magnitude=pert(100_000, 500_000, 3_000_000),
)

# FAIRScenario
scenario = FAIRScenario(
    name="Ransomware Attack",
    asset=asset,
    threat_agent=threat,
    frequency_factors=ff,
    loss_magnitude_factors=lmf,
    time_horizon_years=1.0,
)

# RiskRegister
register = RiskRegister(name="Enterprise Cyber 2026")
register.add_scenario(scenario)
register.remove_scenario("Ransomware Attack")
s = register.get_scenario("Some Scenario")

ScenarioBuilder

The fluent builder API is the recommended way to construct scenarios:

from pycrq import ScenarioBuilder, pert

scenario = (
    ScenarioBuilder("My Scenario")
    # Asset
    .asset("Name", "Information", "Description")
    .with_asset(existing_asset_object)           # alternative

    # Threat
    .threat("Name", "External Adversarial", "Description")
    .with_threat(existing_threat_object)          # alternative

    # Controls
    .add_control("Control Name", "Description", "Preventive")
    .with_control(existing_control_object)        # alternative

    # Metadata
    .describe("Scenario narrative text")
    .tag("tag1", "tag2")
    .effect("Confidentiality")                    # or EffectType enum
    .time_horizon(1.0)                            # years

    # Frequency (use one of these groups)
    .tef(pert(1, 3, 8))                           # Direct TEF
    .contact_frequency(pert(5, 10, 20))           # or CF × POA
    .probability_of_action(pert(0.2, 0.5, 0.8))
    .threat_capability(pert(40, 60, 80))          # for logistic vuln
    .control_strength(pert(50, 65, 80))
    .vulnerability(pert(0.1, 0.25, 0.55))         # or direct vuln

    # Loss magnitude (use primary_loss OR category methods)
    .primary_loss(pert(100_000, 500_000, 2_000_000))
    .loss_productivity(pert(50_000, 200_000, 750_000))
    .loss_response(pert(30_000, 100_000, 400_000))
    .loss_replacement(pert(10_000, 50_000, 200_000))
    .loss_competitive_advantage(pert(0, 25_000, 150_000))
    .loss_fines_judgments(pert(0, 75_000, 500_000))
    .loss_reputation(pert(0, 50_000, 300_000))
    .secondary_loss(
        slef=pert(0.05, 0.20, 0.45),             # probability
        slm=pert(50_000, 250_000, 2_000_000),    # magnitude
    )

    .build()                                      # returns FAIRScenario
)

FAIRSimulator

from pycrq import FAIRSimulator

simulator = FAIRSimulator(
    n_simulations=10_000,   # iterations (default 10,000)
    seed=42,                 # optional for reproducibility
)

# Single scenario
result = simulator.simulate(scenario)          # -> SimulationResult

# Full risk register
agg_result = simulator.simulate_register(register)  # -> AggregateSimulationResult

SimulationResult attributes:

Attribute Type Description
scenario_name str Scenario identifier
n_simulations int Number of iterations
annual_loss_exposure np.ndarray Per-iteration ALE
loss_event_frequency np.ndarray Per-iteration LEF
loss_magnitude np.ndarray Per-iteration LM
threat_event_frequency np.ndarray Per-iteration TEF
vulnerability np.ndarray Per-iteration vulnerability
primary_loss np.ndarray Per-iteration primary loss
secondary_loss np.ndarray Per-iteration secondary loss
loss_by_category Dict[str, np.ndarray] Per-category losses
mean_ale float (property) Mean ALE
std_ale float (property) Std dev of ALE
.percentile(p) method p-th percentile of ALE
.var(confidence) method Value-at-Risk
.cvar(confidence) method Conditional VaR
.summary() method Dict of all key stats

Analysis Functions

from pycrq import (
    compute_metrics,
    classify_risk_level,
    rank_scenarios,
    compute_risk_reduction,
    control_roi,
    sensitivity_analysis,
    bootstrap_confidence_interval,
)

# Full metrics from a SimulationResult
metrics = compute_metrics(result)
# metrics.mean_ale, metrics.var_95, metrics.cvar_95, metrics.risk_level, ...

# Risk level from mean ALE
level = classify_risk_level(mean_ale)
# Returns: "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"

# Rank a list of SimulationResult objects
ranked = rank_scenarios(results, by="mean_ale")
# Returns: [("scenario_name", value), ...] sorted descending

# Compare before/after a control
reduction = compute_risk_reduction(before_result, after_result)
# reduction["mean_ale_reduction_abs"], reduction["mean_ale_reduction_pct"], ...

# Compute ROSI
roi = control_roi(before_result, after_result, control_cost=60_000)
# roi["rosi_pct"], roi["net_benefit"], roi["break_even_years"], roi["recommended"]

# Sensitivity analysis (one-at-a-time)
results = sensitivity_analysis(
    scenario_factory=lambda cap: build_scenario(cap),
    param_name="threat_capability",
    param_values=[20, 40, 60, 80],
    n_simulations=5_000,
    seed=42,
)

# Bootstrap confidence interval
lower, upper = bootstrap_confidence_interval(
    result, statistic="mean", confidence=0.95, n_bootstrap=1_000
)

Reporting

from pycrq import (
    scenario_report,
    register_report,
    export_json,
    export_csv,
    plot_ale_distribution,
    plot_scenario_comparison,
    plot_risk_matrix,
    format_currency,
    format_frequency,
)

# Text reports
print(scenario_report(result, verbose=True))
print(register_report(agg_result))

# Data export
export_json(result, "output/my_scenario.json")
export_csv(result, "output/my_scenario.csv")

# Plots (return matplotlib Figure objects)
fig1 = plot_ale_distribution(result, title="My Scenario ALE", show_percentiles=True)
fig1.savefig("ale_distribution.png", dpi=150)

fig2 = plot_scenario_comparison(results, metric="mean_ale")
fig3 = plot_risk_matrix(results)

# Formatting helpers
format_currency(1_500_000)   # "$1.50M"
format_currency(85_000)      # "$85.0K"
format_frequency(0.25)       # "Once per 4.0 years"
format_frequency(12.0)       # "12.0x/year"

Utilities

from pycrq import (
    validate_scenario,
    calibrate_pert_from_confidence,
    frequency_to_rate,
    annualize,
    summary_table,
)

# Validate a scenario before simulation
warnings = validate_scenario(scenario)
for w in warnings:
    print(f"[WARN] {w}")

# Convert P10/P90 expert estimates to PERT parameters
low, mode, high = calibrate_pert_from_confidence(p10=10_000, p90=500_000)
dist = pert(low, mode, high)

# Convert natural language to annual frequency
rate = frequency_to_rate("monthly")    # 12.0
rate = frequency_to_rate("weekly")     # 52.0
rate = frequency_to_rate("quarterly")  # 4.0

# Annualize a periodic value
annual_cost = annualize(5_000, period="monthly")   # 60_000.0
annual_cost = annualize(1_200, period="weekly")    # 62_400.0

# ASCII comparison table
print(summary_table(list_of_simulation_results))

Examples

File Description
examples/01_basic_scenario.py Full ransomware scenario with all PERT distributions, report, bootstrap CI
examples/02_risk_register.py 5-scenario risk register, aggregate portfolio analysis, rankings
examples/03_control_roi.py Before/after MFA comparison, ROSI calculation, break-even analysis
examples/04_sensitivity_analysis.py Threat capability sweep (20→80), control strength sweep, logistic vulnerability insight

Run any example from the repository root:

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

Open FAIR Model Overview

The Open FAIR model decomposes risk as:

Risk = Loss Event Frequency (LEF) × Loss Magnitude (LM)

LEF = Threat Event Frequency (TEF) × Vulnerability (Vuln)

TEF = Contact Frequency (CF) × Probability of Action (PoA)

Vuln = f(Threat Capability (TCap), Control Strength (CS))
     = logistic(6 × (TCap - CS) / 100)   [when using capability scores]

LM = Primary Loss (PL) + Secondary Loss (SL)

Primary Loss = Productivity + Response + Replacement
             + Competitive Advantage + Fines & Judgments + Reputation

Secondary Loss = SL Event Frequency × SL Magnitude

Annual Loss Exposure (ALE) is the primary output metric:

ALE = LEF × LM × Time Horizon (years)

The Monte Carlo simulation samples each distribution independently per iteration, producing an empirical ALE distribution from which any statistic (mean, percentile, VaR, CVaR) can be derived.


License

MIT License. See 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pycrq-1.0.0.tar.gz (39.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pycrq-1.0.0-py3-none-any.whl (37.2 kB view details)

Uploaded Python 3

File details

Details for the file pycrq-1.0.0.tar.gz.

File metadata

  • Download URL: pycrq-1.0.0.tar.gz
  • Upload date:
  • Size: 39.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for pycrq-1.0.0.tar.gz
Algorithm Hash digest
SHA256 dc8cd67a4d7294f4ce2aa08bcbdf53d4908b8ed4bd8cef76798a70f8957f3b49
MD5 a314d3b4f8a7e43d3a054cf0b4496e2c
BLAKE2b-256 801c6549f77b314a0929a513ebc067d53b5b2dca3ab74979ca638ed6847147cc

See more details on using hashes here.

File details

Details for the file pycrq-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pycrq-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 37.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for pycrq-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2fe5344093cfa3fb2014e454303ac1d3c7e85e7206c1e781bccc05b828a66ff2
MD5 c225d55289e01258bd8aa0d74879ac1c
BLAKE2b-256 63c882a7ead10a62b064758ac107ba307ccb540e902c756eb2345dc0b2c1c9a1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page