An exception is a temporary, owned decision to run with a known failure; it is not a pass. Record the scenario, affected assets and versions, interim controls with test evidence, an expiry and the role authorized to accept the risk, and keep the finding in open-risk reports. Credit a compensating control only when a test shows it breaks the attack path. At expiry, require remediation, a renewal on fresh evidence or a restriction, never a silent extension.
- An approved exception does not turn a failed control into a pass: the finding becomes “exception active” and stays open.
- Only the role that owns the business consequence may accept the risk, and never the person requesting the exception.
- A compensating control earns credit only if a test of the deployed route shows it interrupts the scenario.
- Expected loss is probability times impact: an illustration with stated assumptions, not a forecast.
- Measure on-time resolution against the cohort whose deadline fell in the period, and keep a renewed exception’s original age.
- Check that every active exception has an expiry, a scope and a risk owner who is not the requester.
- Retest each claimed compensating control on the deployed route, including direct backend access.
- Confirm that expired exceptions still appear as open risk with their original opening date.
- Recompute the on-time resolution rate using only exceptions whose deadline fell in the period.
- Applies to
Time-bounded exceptions and outcome measures. An approved exception does not turn a failed control into a pass.
- Part of
- Learning path: Build an evidence-driven security program (step 8 of 11). Workflow: Build an AppSec program
Learning goals and prerequisites
What you will learn
- Calculate and qualify a local expected-loss comparison.
- Deny preventive credit when deployed evidence contradicts the control.
- Keep acceptance authority, expiry and cohort metrics distinct.
Before you start
Engineering and security leads who approve, track or report security exceptions
- Describe a concrete risk, affected assets, compensating controls and who may accept it.
Refresh first: Run an AppSec program: owners, finding states, deadlines and champions
Go to the guided practiceOne security exception from request to expiry
- RequestThe service team records the scenario, affected assets and versions, interim controls and a proposed expiry; a request is not an approval, and the finding stays open.
- Test the interim controlsSecurity tests each compensating control on the deployed route, including direct backend access; only a control shown to break the attack path earns preventive credit.
- DecideThe authorized risk owner, never the requester, approves with an expiry and reopen triggers or rejects; an approved exception is “exception active”, not a pass.
- Track as open riskThe register keeps the finding in open-risk reports with its original opening date, and a daily report flags exceptions that are expired or expire soon.
- Decide again at expiryRemediate and verify, renew on fresh evidence, or restrict the feature; a renewal keeps the original age, and a reopen trigger forces the decision early.
A security exception is a temporary decision to operate with a known unmet requirement. It should describe the scenario, affected assets and versions, credible consequence, exposure, interim controls, expiry and authorized risk owner. A severity label or a statement that the business accepts risk is too vague to support a later review. The owner must understand what remains possible and have authority over the relevant business consequence.
Use risk calculations only when their assumptions are explicit. The exercise supplies illustrative annualized incident probabilities and loss estimates to compare two options. Expected loss is probability multiplied by consequence; it is not a prediction of the exact future and cannot capture every tail outcome or legal duty. An uncertain estimate should be presented as a range. A local acceptance threshold helps structure discussion, but no arithmetic can authorize a decision reserved to another role or waive an applicable legal requirement.
Encode the record so tools can enforce it. The JSON Schema below rejects a missing expiry, an approval without a date and an approved record whose interim controls have not all passed. JSON Schema cannot compare two fields, so check separately that riskOwner differs from requestedBy and holds authority for the scenario. NIST IR 8286 Rev. 1 (December 2025) describes how such records feed an enterprise risk register, and CSF 2.0 outcome GV.RM-02 asks for the risk appetite and tolerance statements that bound who may accept what.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Security exception record",
"type": "object",
"additionalProperties": false,
"required": [
"id",
"findingId",
"release",
"scenario",
"requestedBy",
"riskOwner",
"status",
"firstOpened",
"expires",
"interimControls",
"reopenTriggers"
],
"properties": {
"id": { "type": "string", "pattern": "^EX[0-9]+$" },
"renews": {
"type": "string",
"pattern": "^EX[0-9]+$",
"description": "Earlier record; keep its firstOpened date"
},
"findingId": { "type": "string", "minLength": 1 },
"release": { "type": "string", "minLength": 1 },
"scenario": { "type": "string", "minLength": 20 },
"requestedBy": { "type": "string", "minLength": 1 },
"riskOwner": {
"type": "string",
"minLength": 1,
"description": "The role authorized to accept this risk"
},
"status": { "enum": ["requested", "approved", "rejected", "expired", "closed"] },
"firstOpened": { "type": "string", "format": "date" },
"approvedAt": { "type": "string", "format": "date" },
"expires": { "type": "string", "format": "date" },
"interimControls": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["control", "evidence", "result"],
"properties": {
"control": { "type": "string" },
"evidence": { "type": "string" },
"result": { "enum": ["pass", "fail", "not tested"] }
}
}
},
"reopenTriggers": { "type": "array", "minItems": 1, "items": { "type": "string" } }
},
"if": { "required": ["status"], "properties": { "status": { "const": "approved" } } },
"then": {
"required": ["approvedAt"],
"properties": {
"interimControls": {
"items": { "properties": { "result": { "const": "pass" } } }
}
}
}
}A compensating measure should break a step in the described attack path. For an exposed administrative export endpoint, requiring a constrained access gateway can reduce who reaches it; adding an alert only improves detection and response after access. Logging does not prevent data disclosure. Test the actual deployed route, including alternate hostnames and direct backend access, before assigning preventive credit.
In the fixture, a gateway reduces the assessed annual incident probability from 0.20 to 0.05 only if direct access is denied. Test E2 shows the direct backend remains reachable. Therefore the lower estimate is unsupported and must not be used in the approval request. The engineering team can repair that route, retest it and update the evidence; /domains/runtime/api-security/api-gateway-inventory/ covers gateway bypass. The business tradeoff is restricted access and gateway availability dependency during the exception period. If the gateway fails, the exception conditions should fail closed or trigger an explicit escalation, not silently restore public access.
First American shows the failure these controls prevent: a penetration test finding was entered at a lower severity by a clerical error, missed its deadline and was never escalated for risk acceptance, until a journalist found the same flaw (/domains/appsec/authorization/case-study-first-american/).
Exception metrics should include active, expired, renewed and overdue-remediation populations. If twelve of twenty exceptions expire this month and nine are resolved before expiry, the on-time resolution rate is 9/12, not 9/20. The denominator is the cohort whose deadline occurred. Three unresolved expired exceptions remain visible even if a new exception ID is created. Renewal should retain the original age and reason so repeated deferral cannot reset performance history.
Risk-weighted dashboards can help prioritize but should not erase raw counts and age. Closing ten low-consequence findings may improve average closure time while one critical exposure remains unchanged. Define timestamps and status semantics before comparing teams, as /frameworks/appsec-metrics-and-slas/ does. NIST CSF, SSDF and SAMM provide risk and improvement context; the calculations and thresholds here are a local teaching policy. None of those sources prescribes this specific expected-loss threshold or grants a universal exception period.
At expiry, the accountable owner chooses between verified remediation, renewed acceptance with updated evidence, or suspension/restriction of the affected feature. A review should compare the original conditions with current deployed facts. Changes in exploit availability, data volume, access paths or ownership may invalidate the earlier decision before its calendar expiry. Record immediate reopening triggers as well as the planned review date.
The complete decision record links exception EX7 to release E5, finding F2, tests E1/E2, the estimate assumptions and the authorized owner. An engineer can draft this record without pretending approval has occurred. Keep requested and approved states distinct. The exercise asks you to deny the current proposal under its own assumptions, calculate what a repaired-control scenario would mean, and correct a misleading metric. That combination tests causal evidence, quantitative reasoning and accountability rather than form completion.
Run a daily report over the exception records. The script validates each record against the schema above (with format checking), rejects a requester accepting their own risk, flags approved exceptions that have expired or expire within 14 days, lists every requested or approved exception as open risk with its age, and catches renewals that move firstOpened forward, which would reset the age and hide repeated deferral.
On 10 October 2026, with four sample records, it lists EX7 as a nine-day-old open request, warns that EX3 expires on 20 October and needs a decision from Dana, and reports that EX9 renews EX4 but moved firstOpened from 1 April to 30 September. Keep the original date: the finding has been open since April. Tested with Python 3.14 and jsonschema 4.26.0.
"""Validate exception records and list the decisions due.
Usage: python exception_report.py exception.schema.json exceptions.json 2026-10-10
"""
import json
import sys
from datetime import date, timedelta
from jsonschema import Draft202012Validator, FormatChecker
def report(schema, records, today, soon=timedelta(days=14)):
validator = Draft202012Validator(schema, format_checker=FormatChecker())
by_id = {r["id"]: r for r in records}
for r in records:
for error in validator.iter_errors(r):
print(f"INVALID {r['id']}: {error.message}")
if r["riskOwner"] == r["requestedBy"]:
print(f"INVALID {r['id']}: the requester cannot accept their own risk")
earlier = by_id.get(r.get("renews"))
if earlier and r["firstOpened"] != earlier["firstOpened"]:
print(f"AGE RESET {r['id']}: renews {earlier['id']} but firstOpened moved from "
f"{earlier['firstOpened']} to {r['firstOpened']}")
expires = date.fromisoformat(r["expires"])
if r["status"] == "approved" and expires < today:
print(f"EXPIRED {r['id']}: expired {expires}; remediate, renew on fresh evidence or restrict")
elif r["status"] == "approved" and expires - today <= soon:
print(f"DUE SOON {r['id']}: expires {expires}; schedule the decision with {r['riskOwner']}")
age = (today - date.fromisoformat(r["firstOpened"])).days
if r["status"] in ("requested", "approved"):
print(f"OPEN RISK {r['id']}: finding {r['findingId']} open {age} days, status {r['status']}")
if __name__ == "__main__":
schema = json.load(open(sys.argv[1], encoding="utf-8"))
records = json.load(open(sys.argv[2], encoding="utf-8"))
report(schema, records, date.fromisoformat(sys.argv[3]))Guided practice
Evaluate a 30-day exception request for a fictional administrative export endpoint. All financial values and probabilities are supplied teaching assumptions.
Supplied scenario material
EX7 scope: release E5; export endpoint may disclose a dataset valued at $400,000 per modeled incident.
Baseline annual incident probability p=0.20. Proposed gateway estimate p=0.05 only if all direct access is blocked. For this exercise scale annual expected loss linearly by 30/365; this is an illustrative approximation, not a probability model guarantee.
E1 gateway requires approved staff account. E2 direct backend hostname returns exports without gateway authentication.
Local policy: 30-day modeled expected loss <=$2,000; tested preventive conditions; authorized owner Dana; expiry mandatory.
Request: p=0.05, approved by engineer Ben, expiry blank.
Monthly cohort: 20 total active/closed exception records; 12 had expiry this month; 9 remediated before expiry; 3 expired unresolved. Two of the three were renamed with new IDs.
No approval from Dana supplied.1. Calculate the baseline
Compute annual and 30-day modeled expected loss.
Expected result and reasoning
Annual baseline = 0.20 × $400,000 = $80,000. Thirty-day approximation = $80,000 × 30/365 = $6,575.34, above the $2,000 local threshold.
2. Evaluate the control evidence
Decide whether the request may use p=0.05.
Expected result and reasoning
No. E2 contradicts the required complete gateway boundary. Retain p=0.20 for this comparison until direct access is blocked and tested; E1 alone does not establish the preventive assumption.
3. Calculate the conditional alternative
Compute the result if a future valid test establishes the gateway condition.
Expected result and reasoning
Conditional annual value = 0.05 × $400,000 = $20,000; 30-day value = $1,643.84, below $2,000. This would satisfy only the numeric condition, not ownership, expiry or legal applicability.
4. Make the actual decision
Apply every acceptance condition to the current request.
Expected result and reasoning
Do not approve EX7: numeric risk currently exceeds threshold, preventive evidence fails, Ben lacks the supplied acceptance authority, and expiry is blank. Draft a repair/retest request for Dana; no real acceptance is claimed.
5. Correct the metric
Calculate on-time resolution and account for renamed records.
Expected result and reasoning
9/12 = 75% on-time resolution. Keep all three expired unresolved records in the overdue population, retaining original age despite new IDs. Using 9/20 = 45% answers a different question and concealing two renamed items is misleading.
Probabilities, loss estimates and threshold are fictional assumptions. Linear annual-to-period scaling is illustrative.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Gate an image build on a CIS scan and a deviation register
Repair a CI gate that reads a real OpenSCAP scan of ubuntu:24.04 against the CIS Level 1 Server profile, until unregistered failures, errors, expired deviations and scans of the wrong profile or content version fail the build.
Needs: The offline tests need Python 3.11 or later and nothing else; two register-file tests also need PyYAML, which the scanner image has, and are skipped without it. Live scans need Docker and internet access to build the scanner image and pull the images to scan.
In the extracted folder, run:
python3 -m unittest -v test_gate.py
docker build -t image-scanner .
sh scan.sh IMAGE@sha256:DIGEST ubuntu2404 cis_level1_server out12 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.
The fixture is the TestResult of a real scan with OpenSCAP 1.4.4 and SSG 0.1.82, whose Ubuntu profile follows CIS v1.0.0. An image on disk cannot show running services, kernel settings or mounts; scan hosts and nodes as well.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
Reference information
- SDLC phase
- MaintenanceSecurity program
- Role
- Security leaders