OSCAL and CRML
CRML and OSCAL solve adjacent problems:
- OSCAL (NIST Open Security Controls Assessment Language) is a standard interchange format for controls, assessments, and assessment results.
- CRML is a risk modeling language aimed at quantification (distributions, simulation, calibration, currencies), and at making those assumptions reviewable and reproducible.
Why OSCAL is not “built for quantification”
OSCAL’s assessment results model is intentionally designed to support:
- traceability (what control/objective/component an assessment result applies to),
- evidence linking (observations),
- lifecycle and workflow (status),
- interoperability across tools.
It is intentionally not designed to standardize quantitative risk math:
- Findings do not have native numeric fields for severity, likelihood, impact.
- Risk “characterizations” are open strings; OSCAL does not enforce units, scales, or calculations.
That’s why OSCAL by itself is not a quantitative risk modeling standard.
How CRML relates to OSCAL
CRML can still interoperate with OSCAL because CRML can:
- ingest OSCAL-like assessment artifacts (findings/risks) as structured inputs to a portfolio and governance workflow, and
- export CRML outcomes into OSCAL Assessment Results structures so other governance/compliance tooling can consume them.
In practice, teams often position them like this:
- OSCAL is well-suited as an interchange format for compliance artifacts and organizational posture: what controls exist, how they’re implemented, what was assessed, and what evidence supports the assessment.
- CRML acts as the bridge from posture to the threat landscape for risk modeling: it can reference and normalize threat/control catalogs and mappings (e.g., ATT&CK/CAPEC/internal catalogs) into modelable scenarios, then quantify outcomes.
The key idea:
- Use OSCAL for interchange and auditability.
- Use CRML for quantification and computation.
CRML’s quantitative outputs can be preserved in OSCAL using:
risk.characterizations(preferred for likelihood/impact-like concepts), andprops(for structured extensions, units, currencies, scale definitions, and tool-specific metadata).
Practical limitation: framework catalogs, licensing, and identifiers
In the real world, OSCAL representations of major control frameworks are often:
- published only on official standards bodies’ websites,
- sometimes behind paywalls or licensing terms, and
- may contain copyrighted material.
CRML documentation and examples in this repository therefore SHOULD NOT embed or redistribute copyrighted framework text.
For quantitative risk modeling, the human-readable control description text is usually not the critical input anyway. What matters most for modeling workflows is:
- stable control identifiers,
- and the relationships and mappings (control-to-control, control-to-objective, control-to-threat/technique, coverage/overlap, inheritance).
Normative identifier rules
CRML does not try to ship or maintain a global registry of every control framework. Instead, CRML’s rule is: every control you reference must exist (by id) in at least one control catalog you include.
In practice, catalogs look like the CISv8 example in this repo (note the cisv8: namespace):
catalog:
id: "cisv8" # optional document grouping identifier (organization-owned)
framework: "CIS v8" # human label
controls:
- id: "cisv8:1.1"
- id: "cisv8:1.2"
CRML’s canonical join key is the control entry id in the form namespace:key.
OSCAL → CRML skeleton catalogs (practical ingestion)
If you have an OSCAL Catalog (JSON or YAML) for a framework, CRML can ingest it and generate a redistributable skeleton CRML control catalog.
The skeleton intentionally keeps only:
- control identifiers (
id), - optional
oscal_uuid(the OSCAL control UUID), - short
title(if present), - a reference
url(first OSCAL link, if present).
It intentionally strips detailed statements/parts and any long copyrighted prose.
CLI (requires optional dependency):
pip install "crml-lang[oscal]"
crml-lang oscal-import-catalog \
<in-oscal-catalog.json-or.yaml> \
<out-crml-control-catalog.yaml> \
--namespace cisv8 \
--framework "CIS v8" \
--catalog-id cisv8
Notes:
--namespacebecomes thenamespacepart in generated control ids likecisv8:1.1.- The OSCAL control
idbecomes thekeypart. - OSCAL control UUIDs are preserved in the CRML field
oscal_uuidwhen present.
Normative rules:
- Each control catalog entry MUST have an
id(namespace:key) that is stable over time. - The
idvalues inside a catalog MUST be unique (no duplicates). - Mapping work MUST be performed against these identifiers (not titles, not prose descriptions).
Practical guidance:
- If you build cross-framework mappings (e.g., CISv8 → Org), it is usually simplest to include a complete catalog for the source framework so every
source_idin the mapping has a corresponding catalog entry.
When an original framework author/publisher provides an OSCAL representation, exporters/importers SHOULD treat the framework’s published identifier as canonical:
- Prefer the framework’s human-readable identifier (the control ID that humans cite, e.g.,
AC-2) as the primary mapping key. - If OSCAL uses UUIDs internally, the UUID SHOULD be preserved alongside the human-readable identifier.
In CRML, store that UUID in
oscal_uuidon the control catalog entry (and optionally on assessment entries when assessments are shared without catalogs).
When no canonical OSCAL representation exists (or cannot be redistributed), CRML workflows SHOULD still operate by:
- minting stable IDs in an explicit namespace (e.g.,
org:,vendor:, or a URN scheme), and - keeping catalogs minimal: identifiers + relationship structure + any non-copyrightable metadata needed for mapping.
“Skeleton” catalogs and licensed enrichment
CRML examples and reference catalogs are intentionally skeleton models: they focus on identifiers and relationship structure, and avoid embedding copyrighted framework prose.
Organizations that have the appropriate rights MAY enrich these skeleton catalogs locally by attaching the full, licensed control text and metadata sourced from the standards bodies themselves — either via:
- official OSCAL catalogs published by the framework owner, or
- licensed CRML control catalogs (if a standards body publishes CRML-native representations).
This keeps the public CRML ecosystem redistributable while still enabling full-fidelity internal catalogs where licensing permits.
Mapping rules (normative)
This section defines rules for mapping CRML concepts to OSCAL Assessment Results-style finding and risk objects.
Conventions
- UUID stability: OSCAL requires stable
uuidvalues. When CRML does not already have a UUID for an artifact, exporters SHOULD generate deterministic UUIDv5 values from stable CRML identifiers. - Namespace for CRML extensions: Use
props[].ns = "urn:crml"for CRML-specific values. - Name convention: Use
props[].namewithout acrml:prefix (becausensalready carries the namespace). - Units and scales: Any numeric value exported into OSCAL MUST include units in the same string, or accompany it with unit metadata in
props(see below).
1) Finding object — fields and mapping
A finding documents what was observed during an assessment.
OSCAL finding shape (summary)
finding:
uuid: uuid
title: string
description: markup
rationale: markup
remark: string
target:
type: component | objective | control
target-id: uuid
status:
state: satisfied | not-satisfied | partial | not-applicable
reason: string
related-observations:
- observation-uuid
related-risks:
- risk-uuid
origins:
- actor:
type: assessor | tool | party
actor-uuid: uuid
props:
- name: string
value: string
ns: uri
class: string
links:
- href: uri
rel: string
media-type: string
Field mapping rules
| OSCAL finding field | CRML source | Rule |
|---|---|---|
finding.uuid |
CRML stable ID (preferred) | MUST be stable across exports. If CRML has no UUID, SHOULD compute deterministic UUIDv5 from (assessment_id, target_id, finding_kind). |
finding.title |
Control/objective/component label | SHOULD be a short label. Example: "Control assessment: org:iam.mfa". |
finding.description |
CRML assessment notes / observed condition | SHOULD describe what was observed. Markup allowed. |
finding.rationale |
CRML rationale for why it matters | MAY explain why this finding affects risk/compliance. |
finding.remark |
Freeform | MAY hold additional context. |
finding.target.type |
CRML reference kind | MUST be control when finding is about a control; component when about a system component; objective when about a requirement/objective. |
finding.target.target-id |
Identifier of target | MUST be a UUID in OSCAL. If CRML uses non-UUID IDs (e.g., org:iam.mfa), MUST also store that original ID in props (see below). |
finding.target.status.state |
CRML posture state | MUST map to one of: satisfied, not-satisfied, partial, not-applicable. |
finding.target.status.reason |
CRML explanation | SHOULD capture “why” (e.g., missing evidence, not implemented). |
finding.related-observations[] |
Evidence references | MAY link to evidence/telemetry observations if available. |
finding.related-risks[] |
Derived risk UUIDs | MAY reference risks created/updated due to this finding. |
finding.origins[].actor.type |
Producer | MUST be tool for CRML engine exports; MAY be assessor for human-entered findings. |
finding.origins[].actor.actor-uuid |
Tool/assessor identity | SHOULD be stable for the same producer. |
finding.links[] |
Traceability links | SHOULD link to CRML artifacts (scenario/portfolio path, run report URL, commit). |
CRML extension props for findings
Because OSCAL findings have no native severity/likelihood/impact fields:
- Quantitative or categorical values attached to a finding MUST be represented using
props.
Recommended CRML property names:
target.external_id— original CRML target identifier if OSCALtarget-idis a generated UUIDconfidence— e.g.,"0.7"or"high"severity— if you need a single roll-up, use a string with scale info (e.g.,"high (org-scale-v1)")
2) Risk object — fields and mapping
A risk represents potential loss or harm, not an observation.
OSCAL risk shape (summary)
risk:
uuid: uuid
title: string
description: markup
statement: markup
status: open | investigating | mitigated | accepted | closed
origins:
- source:
type: finding | observation | threat | vulnerability
uuid-ref: uuid
threat-ids:
- string
characterizations:
- facet: likelihood | impact | confidence | exposure | severity
value: string
mitigation:
uuid: uuid
description: markup
implementation:
uuid: uuid
description: markup
props:
- name: string
value: string
ns: uri
class: string
links:
- href: uri
rel: string
media-type: string
Field mapping rules
| OSCAL risk field | CRML source | Rule |
|---|---|---|
risk.uuid |
CRML stable risk ID | MUST be stable across exports. If derived from a scenario, SHOULD be deterministic from (scenario_id, portfolio_id, risk_kind). |
risk.title |
CRML scenario/output label | SHOULD be a short label (e.g., "Ransomware loss risk"). |
risk.description |
CRML narrative summary | MAY summarize key assumptions and scope. |
risk.statement |
Formal risk statement | SHOULD follow “If X then Y resulting in Z”. |
risk.status |
Workflow status | If CRML has no lifecycle state, MUST default to open. |
risk.origins[].source.type |
Source type | MUST be one of finding, observation, threat, vulnerability. For CRML simulation results, SHOULD use observation. For posture-driven items, SHOULD use finding. |
risk.origins[].source.uuid-ref |
Source UUID | SHOULD reference the upstream object (finding/observation) when known. |
risk.threat-ids[] |
Threat catalog refs | MAY include external IDs like ATT&CK technique IDs. |
risk.mitigation |
CRML mitigation/control strategy | MAY reference the mitigation plan; use implementation for what is actually in place. |
risk.links[] |
Traceability | SHOULD link to CRML run outputs and model artifacts. |
3) Risk characterizations (very important)
OSCAL does not define numeric scales.
Example:
characterizations:
- facet: likelihood
value: high
- facet: impact
value: medium
Allowed facet values are open strings, but typical facets include:
likelihoodimpactseverityconfidenceexposure
Quantitative export rule
If CRML produces numeric likelihood/impact outputs, exporters MUST NOT pretend OSCAL defines math for those numbers.
Instead:
1) Prefer encoding numeric values as strings with units inside risk.characterizations[].
Examples:
facet: likelihood,value: "0.15 / year"facet: impact,value: "USD 250000 (median)"facet: severity,value: "USD 1.2M (P95)"
2) For machine-readability, also include structured values in risk.props[].
Recommended property set:
likelihood.annual_rate→"0.15"impact.median.currency→"USD"impact.median.value→"250000"severity.p95.currency→"USD"severity.p95.value→"1200000"time_basis→"per_organization_per_year"
This preserves CRML’s quantitative results while keeping OSCAL semantics honest.
Optional qualitative bucketing rule
If an organization requires high/medium/low buckets, exporters MUST also export the bucket definition as props, e.g.:
scale.likelihood→"org-scale-v1: low<0.05, medium<0.2, high>=0.2 (/year)"
Minimal example: CRML → OSCAL risk
risk:
uuid: 2ad9c1ee-6f05-5d2b-8a49-9d9a7c5aaf5f
title: "Ransomware loss risk"
statement: "If ransomware occurs, then operations are disrupted resulting in financial loss."
status: open
origins:
- source:
type: observation
uuid-ref: 6d3aefc6-1e7a-5df0-9c70-4f8d0c54a6f3
characterizations:
- facet: likelihood
value: "0.15 / year"
- facet: impact
value: "USD 250000 (median)"
- facet: severity
value: "USD 1200000 (P95)"
props:
- name: "likelihood.annual_rate"
value: "0.15"
ns: "urn:crml"
class: "number"
- name: "impact.median.currency"
value: "USD"
ns: "urn:crml"
class: "string"
- name: "impact.median.value"
value: "250000"
ns: "urn:crml"
class: "number"
Summary
- OSCAL is excellent for interoperable assessment result interchange and traceability.
- CRML is designed for quantified modeling and computation.
- CRML can export results into OSCAL by:
- using
risk.characterizationsfor likelihood/impact-like summaries, - using
propsfor precise numeric values, units, currencies, and scale definitions.