Connect vulnerability, asset and business process
An APS team receives a fictional vulnerability affecting a transfer component used during funds closing. Before choosing a window, confirm version, affected configuration, exposure, owner and dependencies. A similar inventory name does not prove applicability, and absence of an agent does not prove absence of the component. NIST SP 800-40 Rev.4 frames patching as continuing maintenance including identification, response, verification and monitoring. Define who obtains the fix, validates origin and compatibility, and accepts residual risk. Include images, templates and recovery mechanisms: fixing current instances while retaining a vulnerable template can reintroduce the problem at the next scale-out. Inventory needs to follow asset creation, replacement and retirement.
Combine severity, exploitation and context
A CVSS Base score informs technical severity but does not alone determine service priority. CVSS 4.0 distinguishes threat and environmental context. Known exploitation, effective exposure, criticality, existing controls and interruption cost provide additional inputs. KEV is a CISA source on known exploited vulnerabilities; absence of an entry does not prove a flaw unexploitable or absent from the environment. In this research, the main page returned 403 and the official cisagov mirror was inspected. No actual CVEs were assigned to fictional cases. Do not carry dates or obligations from an external program into every organization without checking applicability and policy. Document the decision and reasons for any temporary treatment.
Define acceptance before the change window
For a middleware fix, prepare dependencies, compatibility, representative testing, rollback and stopping criteria. An urgent change needs appropriate decisions and coordination; urgency does not remove the need to know who may interrupt operations. Successful installer completion is only part of the evidence. Confirm that the effective version changed, required restarts occurred and business flows remain correct. If mitigation is temporary isolation, verify the attack path it should block and retain an expiry and owner. A filtering rule is not automatically equivalent to a patch. Rollback may also restore the vulnerability, so it should include risk assessment and temporary measures.
Reconcile denominators and evidence freshness
The Python lab uses a generated inventory of 100 assets and 80 fictional observations. Of those 80,72 say patched, five failed and three pending-reboot. The superficial rate is 72/80, or 90%. Four of the 72 patched observations fall outside the fictional 72-hour window, leaving 68 recently patched, five failures, three pending restarts, four stale observations and 20 unknowns. Observed coverage is 80%; the inventory fraction with a recent patched assertion is 68%. Even that assertion is not independent proof of effective remediation. The script rejects duplicate identities, orphan observations and naive or future timestamps instead of overwriting conflicts. The evidence window was chosen for the exercise and is neither a universal remediation deadline nor a GDPR notification deadline.
Hand RUN an actionable state
Run the code and inspect counts, per-asset states and checks while retaining original observations. The exercise creates only a local temporary file, hashes it and removes its own directory at the end. It performs no scans, exploits, installations or restarts. In an actual service, relate the report to authorized inventory, observation time and confirmation of the running fix. For each outstanding item, identify impact, next action, owner and deadline; do not close unknowns as compliant to improve the percentage. Also verify that templates and restoration procedures remain corrected. Summary: priority depends on context and completion depends on evidence with known scope and age. Connect this lesson with incidents, change, obsolescence, observability and supplier management.
"""Original offline reconciliation of synthetic patch observations, not a vulnerability scan."""
import copy,datetime,hashlib,json,pathlib,tempfile
UTC=datetime.timezone.utc
NOW=datetime.datetime(2026,10,7,12,tzinfo=UTC)
WINDOW=datetime.timedelta(hours=72)
checks=[]
def check(name,condition):
checks.append({'id':name,'pass':bool(condition)})
def timestamp(value):
t=datetime.datetime.fromisoformat(value)
if t.tzinfo is None:raise ValueError('timezone required')
return t.astimezone(UTC)
def reconcile(inventory,observations):
if len(inventory)!=len(set(inventory)):raise ValueError('duplicate inventory identity')
index={}
for row in observations:
if row['asset'] not in inventory:raise ValueError('unknown inventory identity')
if row['asset'] in index:raise ValueError('duplicate observation identity')
if row['status'] not in ['patched','failed','pending-reboot']:raise ValueError('unsupported status')
t=timestamp(row['observedAt'])
if t>NOW:raise ValueError('future observation')
index[row['asset']]=row
result={}
for asset in inventory:
row=index.get(asset)
if row is None:result[asset]='unknown'
elif NOW-timestamp(row['observedAt'])>WINDOW:result[asset]='stale'
else:result[asset]=row['status']
counts={s:list(result.values).count(s)for s in ['patched','failed','pending-reboot','stale','unknown']}
return {'states':result,'counts':counts,'inventory':len(inventory),'observed':len(index),'observedCoverage':len(index)/len(inventory)if inventory else None,'verifiedPatchedFraction':counts['patched']/len(inventory)if inventory else None}
def rejects(name,inventory,observations):
try:reconcile(inventory,observations)
except (ValueError,KeyError):check(name,True)
else:check(name,False)
inventory=[f'asset-{i:03}'for i in range(1,101)]
observations=[{'asset':asset,'status':'patched'if i<=72 else 'failed'if i<=77 else 'pending-reboot','observedAt':'2026-10-01T12:00:00Z'if 69<=i<=72 else '2026-10-06T12:00:00Z'}for i,asset in enumerate(inventory[:80],1)]
original=copy.deepcopy(observations)
with tempfile.TemporaryDirectory(prefix='dr-patch-fixture-')as tmp:
directory=pathlib.Path(tmp);source=directory/'observations.json'source.write_text(json.dumps(observations,sort_keys=True)+'\n');raw=source.read_bytes;source_hash=hashlib.sha256(raw).hexdigest;loaded=json.loads(raw);report=reconcile(inventory,loaded)
check('inventory100',report['inventory']==100);check('observed80',report['observed']==80)
check('fresh-patched68',report['counts']['patched']==68);check('failed5',report['counts']['failed']==5)
check('pending-reboot3',report['counts']['pending-reboot']==3);check('stale4',report['counts']['stale']==4)
check('unknown20',report['counts']['unknown']==20);check('counts-cover-inventory',sum(report['counts'].values)==100)
check('coverage80percent',report['observedCoverage']==0.8);check('verified-patched68percent',report['verifiedPatchedFraction']==0.68)
check('observed-claimed-patched90percent',sum(r['status']=='patched'for r in loaded)/len(loaded)==0.9)
check('raw-file-unchanged',source.read_bytes==raw);check('input-records-unchanged',loaded==original)
check('equivalent-offset',timestamp('2026-10-06T13:00:00+01:00')==timestamp('2026-10-06T12:00:00Z'))
boundary=[{'asset':'x','status':'patched','observedAt':'2026-10-04T12:00:00Z'}]
check('exact72hours-accepted',reconcile(['x'],boundary)['counts']['patched']==1)
older=copy.deepcopy(boundary);older[0]['observedAt']='2026-10-04T11:59:59Z'
check('older-than72hours-stale',reconcile(['x'],older)['counts']['stale']==1)
rejects('duplicate-inventory-rejected',['x','x'],[])
rejects('duplicate-observation-rejected',inventory,loaded+[loaded[0]])
conflict=copy.deepcopy(loaded[0]);conflict['status']='failed'rejects('conflicting-observation-rejected',inventory,loaded+[conflict])
orphan=copy.deepcopy(loaded[0]);orphan['asset']='unmanaged'rejects('orphan-observation-rejected',inventory,[orphan])
naive=copy.deepcopy(loaded[0]);naive['observedAt']='2026-10-06T12:00:00'rejects('naive-time-rejected',inventory,[naive])
future=copy.deepcopy(loaded[0]);future['observedAt']='2026-10-08T12:00:00Z'rejects('future-time-rejected',inventory,[future])
unknown=copy.deepcopy(loaded[0]);unknown['status']='success-ish'rejects('unknown-status-rejected',inventory,[unknown])
missing=copy.deepcopy(loaded[0]);del missing['asset'];rejects('missing-identity-rejected',inventory,[missing])
check('empty-inventory-rate-not-invented',reconcile([],[])['verifiedPatchedFraction'] is None)
check('no-observation-not-patched',reconcile(['x'],[])['states']['x']=='unknown')
check('reordering-does-not-change-report',reconcile(inventory,list(reversed(loaded)))==report)
check('pending-reboot-not-counted-patched',report['states']['asset-080']=='pending-reboot')
check('stale-success-not-counted-patched',report['states']['asset-069']=='stale')
check('owned-temporary-directory-removed',not directory.exists)
result={'runtime':__import__('platform').python_version,'scriptSHA256':hashlib.sha256(pathlib.Path(__file__).read_bytes).hexdigest,'sourceSHA256':source_hash,'passed':sum(c['pass']for c in checks),'failed':sum(not c['pass']for c in checks),'checks':checks,'report':report,'clock':NOW.isoformat,'freshnessHours':72,'limits':['Generated inventory and status records, not real infrastructure or scanner output.','No vulnerability discovery, exploit, patch installation, restart or provider access.','The72-hour evidence window is a fictional local rule, not a universal remediation deadline.','Patched is an input assertion within this teaching snapshot, not independent proof of effective remediation.','Status arithmetic and identity reconciliation do not demonstrate a service is secure.']}
print(json.dumps(result,indent=2));raise SystemExit(1 if result['failed'] else 0)
72/80 gives90% among observations, but only68 of100 assets have a recent patched assertion;20 remain unknown.
Common pitfalls
Percentage without denominator; installer success as confirmed effect; KEV absence as safety; old template; unknowns as compliant.
Related topics: Change, inventory and operational evidence · Data, suppliers and responsibilities
Prioritize vulnerabilities in service context and demonstrate coverage, remediation effect and still-unknown work.
Reference: Enterprise Patch Management Planning · CCSP examination outline effective 2026-08-01; January2026 V2 PDF