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
- Overview
- Installation
- Quick Start
- Architecture
- API Reference
- Examples
- Open FAIR Model Overview
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dc8cd67a4d7294f4ce2aa08bcbdf53d4908b8ed4bd8cef76798a70f8957f3b49
|
|
| MD5 |
a314d3b4f8a7e43d3a054cf0b4496e2c
|
|
| BLAKE2b-256 |
801c6549f77b314a0929a513ebc067d53b5b2dca3ab74979ca638ed6847147cc
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fe5344093cfa3fb2014e454303ac1d3c7e85e7206c1e781bccc05b828a66ff2
|
|
| MD5 |
c225d55289e01258bd8aa0d74879ac1c
|
|
| BLAKE2b-256 |
63c882a7ead10a62b064758ac107ba307ccb540e902c756eb2345dc0b2c1c9a1
|