Lab 06 β Vulnerability Data Analysis and Prioritization
Mission Information
Section titled βMission InformationβDifficulty: Intermediate β Advanced
Estimated Time: 120β180 minutes
Primary Language: Python
Security Domain: Vulnerability Management / Security Operations / Risk Management
Environment: Local controlled lab
Automation Type: Defensive Vulnerability Analytics
Mission
Section titled βMissionβYour task is to build a Python-based vulnerability analysis and prioritization pipeline.
You will start with:
RAW SCANNER EXPORTcontaining:
ASSETS
VULNERABILITIES
SEVERITIES
CVSS SCORES
DUPLICATE FINDINGS
MISSING OWNERS
INTERNET EXPOSURE
REMEDIATION DATESThen transform it into:
RAW FINDINGS βVALIDATE βNORMALIZE βDEDUPLICATE βENRICH WITH ASSET CONTEXT βCALCULATE PRIORITY βMAP OWNER βIDENTIFY OVERDUE ITEMS βBUILD REMEDIATION QUEUE βREPORTWhy This Lab Matters
Section titled βWhy This Lab MattersβA vulnerability scanner may produce:
10 FINDINGS
1,000 FINDINGS
100,000 FINDINGSThe security problem is rarely:
HOW MANY VULNERABILITIES EXIST?The more important questions are:
WHICH FINDINGS MATTER MOST?
WHICH ASSETS ARE MOST IMPORTANT?
WHICH SYSTEMS ARE INTERNET EXPOSED?
WHICH FINDINGS ARE OVERDUE?
WHO OWNS THE AFFECTED SYSTEM?
WHICH ITEMS SHOULD BE FIXED FIRST?That is the difference between:
VULNERABILITY SCANNINGand:
VULNERABILITY MANAGEMENTLearning Objectives
Section titled βLearning ObjectivesβBy completing this lab, you should be able to:
READ VULNERABILITY CSV DATA
VALIDATE REQUIRED FIELDS
NORMALIZE SEVERITY VALUES
NORMALIZE ASSET NAMES
PARSE CVSS SCORES
REMOVE DUPLICATE FINDINGS
LOAD ASSET CONTEXT
MAP ASSET OWNERS
IDENTIFY INTERNET-EXPOSED SYSTEMS
IDENTIFY HIGH-CRITICALITY SYSTEMS
IDENTIFY OVERDUE REMEDIATION
BUILD A TRAINING RISK MODEL
GENERATE A REMEDIATION QUEUE
EXPORT CSV
EXPORT JSON
GENERATE MARKDOWN REPORTS
DOCUMENT ANALYSIS LIMITATIONSFinal Architecture
Section titled βFinal Architectureβ SCANNER EXPORT β CSV INGEST β VALIDATE β NORMALIZE β DEDUPLICATE β ASSET INVENTORY JOIN β βββββββββββΌββββββββββ β β β CVSS CRITICALITY EXPOSURE β β β βββββββββββΌββββββββββ β DUE DATE β PRIORITY ENGINE β OWNER MAPPING β REMEDIATION QUEUE βββββββββΌββββββββ β β β CSV JSON MDAuthorization and Safety
Section titled βAuthorization and SafetyβThis lab uses:
SYNTHETIC VULNERABILITY DATADo not scan, assess, or prioritize systems outside your authorized environment.
The lab focuses on:
DATA ANALYSIS
RISK CONTEXT
REPORTING
REMEDIATION PLANNINGnot exploitation.
01 β Create the Lab Workspace
Section titled β01 β Create the Lab WorkspaceβCreate:
vulnerability-prioritization/|+-- data/|+-- reports/|+-- src/|+-- tests/|+-- README.mdLinux/macOS:
mkdir -p vulnerability-prioritization/{data,reports,src,tests}cd vulnerability-prioritizationPowerShell:
mkdir vulnerability-prioritization
cd vulnerability-prioritization
mkdir datamkdir reportsmkdir srcmkdir tests02 β Verify Python
Section titled β02 β Verify PythonβRun:
python --versionor:
python3 --versionRecommended:
Python 3.10+03 β Create the Asset Inventory
Section titled β03 β Create the Asset InventoryβCreate:
data/assets.csvAdd:
asset_id,hostname,environment,criticality,owner,internet_exposed1,WEB01,Production,Critical,Web Team,true2,DB01,Production,Critical,Database Team,false3,APP01,Production,High,Application Team,false4,DEV01,Development,Medium,Development Team,false5,OLD01,Production,High,,false6,VPN01,Production,Critical,Network Team,true7,JUMP01,Production,Critical,Security Team,false04 β Understand the Asset Context
Section titled β04 β Understand the Asset ContextβEach asset includes:
HOSTNAME
ENVIRONMENT
CRITICALITY
OWNER
INTERNET EXPOSUREThese fields will influence remediation priority.
05 β Create the Vulnerability Export
Section titled β05 β Create the Vulnerability ExportβCreate:
data/vulnerabilities.csvAdd:
finding_id,hostname,vulnerability_name,severity,cvss,status,discovered_date,remediation_due_dateVULN-001,WEB01,Outdated Web Framework,Critical,9.8,Open,2026-08-01,2026-08-15VULN-002,WEB01,Weak TLS Configuration,HIGH,8.1,Open,2026-08-02,2026-08-20VULN-003,DB01,Missing Security Update,critical,9.5,Open,2026-08-03,2026-08-17VULN-004,APP01,Outdated Application Library,High,7.8,Open,2026-08-05,2026-08-25VULN-005,DEV01,Development Package Finding,Medium,5.0,Closed,2026-08-07,2026-09-07VULN-006,OLD01,Legacy Service Exposure,Critical,9.0,Open,2026-07-15,2026-07-30VULN-007,VPN01,Remote Access Security Update,Critical,9.1,Open,2026-08-10,2026-08-22VULN-008,JUMP01,Administrative Tool Update,High,7.5,Open,2026-08-14,2026-09-05VULN-009,WEB01,Outdated Web Framework,Critical,9.8,Open,2026-08-01,2026-08-15VULN-010,APP01,Missing Application Header,medium,5.4,Open,2026-08-20,2026-09-20VULN-011,UNKNOWN01,Unmapped Host Finding,High,8.0,Open,2026-08-21,2026-09-10VULN-012,DB01,Invalid Score Example,High,not-a-score,Open,2026-08-22,2026-09-1206 β Why This Dataset Is Useful
Section titled β06 β Why This Dataset Is UsefulβIt includes:
DUPLICATE FINDING
INCONSISTENT SEVERITY CASE
OPEN AND CLOSED FINDINGS
CRITICAL ASSETS
INTERNET-EXPOSED ASSETS
MISSING OWNER
UNKNOWN ASSET
INVALID CVSS VALUE
OVERDUE REMEDIATION07 β Create the Python Script
Section titled β07 β Create the Python ScriptβCreate:
src/vulnerability_analyzer.pyStart with:
from pathlib import Pathfrom datetime import datetime, datefrom collections import Counter, defaultdictimport csvimport json08 β Define Project Paths
Section titled β08 β Define Project PathsβAdd:
BASE_DIR = Path(__file__).resolve().parent.parent
ASSET_FILE = BASE_DIR / "data" / "assets.csv"
VULNERABILITY_FILE = ( BASE_DIR / "data" / "vulnerabilities.csv")
REPORT_DIR = BASE_DIR / "reports"
REPORT_DIR.mkdir( parents=True, exist_ok=True)09 β Define the Lab Reference Date
Section titled β09 β Define the Lab Reference DateβFor reproducible results, use:
REFERENCE_DATE = date( 2026, 8, 29)Why Use a Fixed Date?
Section titled βWhy Use a Fixed Date?βIf you use:
date.today()your results will change over time.
A fixed lab date provides:
REPRODUCIBLE OUTPUT10 β Normalize Boolean Values
Section titled β10 β Normalize Boolean ValuesβCreate:
def parse_boolean(value): return ( str(value) .strip() .lower() in { "true", "1", "yes" } )11 β Normalize Severity
Section titled β11 β Normalize SeverityβCreate:
SEVERITIES = { "critical", "high", "medium", "low", "informational"}Then:
def normalize_severity(value): normalized = ( str(value) .strip() .lower() )
if normalized not in SEVERITIES: return "unknown"
return normalized12 β Why Normalize Severity?
Section titled β12 β Why Normalize Severity?βWithout normalization:
Critical
CRITICAL
criticalmay behave like three different values.
After normalization:
critical13 β Parse CVSS Safely
Section titled β13 β Parse CVSS SafelyβCreate:
def parse_cvss(value): try: score = float(value)
if 0.0 <= score <= 10.0: return score
except ( TypeError, ValueError ): pass
return None14 β Why Validate CVSS?
Section titled β14 β Why Validate CVSS?βA scanner export might contain:
9.8
N/A
Unknown
not-a-scoreYour script should not crash because one row contains bad data.
15 β Parse Dates
Section titled β15 β Parse DatesβCreate:
def parse_date(value): try: return datetime.strptime( value.strip(), "%Y-%m-%d" ).date()
except ( AttributeError, ValueError ): return None16 β Normalize Hostnames
Section titled β16 β Normalize HostnamesβCreate:
def normalize_hostname(value): return ( str(value) .strip() .upper() )Therefore:
web01
WEB01
Web01become:
WEB0117 β Load the Asset Inventory
Section titled β17 β Load the Asset InventoryβCreate:
def load_assets(): assets = {}
with ASSET_FILE.open( "r", encoding="utf-8", newline="" ) as file:
reader = csv.DictReader( file )
for row in reader: hostname = normalize_hostname( row["hostname"] )
assets[hostname] = { "asset_id": row["asset_id"],
"hostname": hostname,
"environment": row["environment"].strip(),
"criticality": row["criticality"] .strip() .lower(),
"owner": row["owner"].strip(),
"internet_exposed": parse_boolean( row["internet_exposed"] ) }
return assets18 β Test Asset Loading
Section titled β18 β Test Asset LoadingβTemporarily:
assets = load_assets()
print( json.dumps( assets, indent=2 ))Run:
python src/vulnerability_analyzer.py19 β Required Vulnerability Fields
Section titled β19 β Required Vulnerability FieldsβCreate:
REQUIRED_FIELDS = { "finding_id", "hostname", "vulnerability_name", "severity", "cvss", "status", "discovered_date", "remediation_due_date"}20 β Validate CSV Schema
Section titled β20 β Validate CSV SchemaβCreate:
def validate_headers(fieldnames): if not fieldnames: return False
return REQUIRED_FIELDS.issubset( set(fieldnames) )21 β Load Vulnerability Data
Section titled β21 β Load Vulnerability DataβCreate:
def load_vulnerabilities(): valid = [] invalid = []
with VULNERABILITY_FILE.open( "r", encoding="utf-8", newline="" ) as file:
reader = csv.DictReader( file )
if not validate_headers( reader.fieldnames ): raise ValueError( "Vulnerability CSV is missing required columns." )
for line_number, row in enumerate( reader, start=2 ): normalized = normalize_finding( row, line_number )
if normalized["valid"]: valid.append( normalized["record"] ) else: invalid.append( normalized["record"] )
return valid, invalid22 β Create the Finding Normalizer
Section titled β22 β Create the Finding NormalizerβAdd:
def normalize_finding( row, line_number): hostname = normalize_hostname( row.get( "hostname", "" ) )
vulnerability_name = ( row.get( "vulnerability_name", "" ) .strip() )
severity = normalize_severity( row.get( "severity", "" ) )
cvss = parse_cvss( row.get( "cvss", "" ) )
discovered_date = parse_date( row.get( "discovered_date", "" ) )
due_date = parse_date( row.get( "remediation_due_date", "" ) )
record = { "line_number": line_number,
"finding_id": row.get( "finding_id", "" ).strip(),
"hostname": hostname,
"vulnerability_name": vulnerability_name,
"severity": severity,
"cvss": cvss,
"status": row.get( "status", "" ).strip().lower(),
"discovered_date": discovered_date,
"remediation_due_date": due_date }
reasons = []
if not hostname: reasons.append( "Missing hostname" )
if not vulnerability_name: reasons.append( "Missing vulnerability name" )
if severity == "unknown": reasons.append( "Unknown severity" )
if cvss is None: reasons.append( "Invalid CVSS score" )
if discovered_date is None: reasons.append( "Invalid discovered date" )
if due_date is None: reasons.append( "Invalid remediation due date" )
if reasons: record["reason"] = "; ".join( reasons )
return { "valid": False, "record": record }
return { "valid": True, "record": record }23 β Test Vulnerability Loading
Section titled β23 β Test Vulnerability LoadingβTemporarily:
valid, invalid = load_vulnerabilities()
print( f"Valid findings: {len(valid)}")
print( f"Invalid findings: {len(invalid)}")Expected:
Valid findings: 11
Invalid findings: 1because:
VULN-012contains:
not-a-score24 β Deduplication Strategy
Section titled β24 β Deduplication StrategyβTwo findings can refer to the same underlying vulnerability.
For this lab, use:
HOSTNAME+VULNERABILITY NAME+STATUSas a simple deduplication key.
25 β Build the Deduplication Key
Section titled β25 β Build the Deduplication KeyβCreate:
def get_deduplication_key( record): return ( record["hostname"], record[ "vulnerability_name" ].strip().lower(), record["status"] )26 β Deduplicate Findings
Section titled β26 β Deduplicate FindingsβCreate:
def deduplicate_findings( records): unique = [] duplicates = []
seen = set()
for record in records: key = get_deduplication_key( record )
if key in seen: duplicates.append( record ) continue
seen.add(key)
unique.append( record )
return unique, duplicates27 β Why Deduplication Matters
Section titled β27 β Why Deduplication MattersβScanner pipelines may produce duplicates because of:
MULTIPLE SCAN JOBS
MULTIPLE SCANNERS
REPEATED IMPORTS
AGENT + NETWORK SCANS
DATA PIPELINE ISSUESWithout deduplication:
10 REAL FINDINGSmight appear as:
25 FINDINGS28 β Deduplication Limitation
Section titled β28 β Deduplication LimitationβThe simple key:
HOSTNAME + FINDING NAME + STATUSis not universally reliable.
Production systems may need:
PLUGIN ID
CVE
PORT
PROTOCOL
SCANNER ID
ASSET ID
FIRST SEEN
FINGERPRINT29 β Enrich Findings with Asset Data
Section titled β29 β Enrich Findings with Asset DataβCreate:
def enrich_with_assets( findings, assets): enriched = []
for finding in findings: item = finding.copy()
asset = assets.get( item["hostname"] )
if asset: item[ "asset_mapped" ] = True
item.update({ "environment": asset[ "environment" ],
"criticality": asset[ "criticality" ],
"owner": asset[ "owner" ],
"internet_exposed": asset[ "internet_exposed" ] })
else: item[ "asset_mapped" ] = False
item.update({ "environment": "unknown",
"criticality": "unknown",
"owner": "",
"internet_exposed": False })
enriched.append( item )
return enriched30 β Why Asset Mapping Matters
Section titled β30 β Why Asset Mapping MattersβBefore enrichment:
Vulnerability:Outdated Web Framework
CVSS:9.8After enrichment:
Vulnerability:Outdated Web Framework
CVSS:9.8
Asset:WEB01
Environment:Production
Criticality:Critical
Internet Exposed:Yes
Owner:Web TeamThat is much more actionable.
31 β Find Unmapped Assets
Section titled β31 β Find Unmapped AssetsβCreate:
def get_unmapped_findings( findings): return [ item for item in findings if not item[ "asset_mapped" ] ]You should identify:
UNKNOWN0132 β Why Unmapped Assets Matter
Section titled β32 β Why Unmapped Assets MatterβAn unmapped vulnerability may mean:
ASSET INVENTORY GAP
HOSTNAME MISMATCH
STALE SCANNER RECORD
UNMANAGED ASSET
DECOMMISSIONING ISSUEDo not silently ignore it.
33 β Determine Overdue Status
Section titled β33 β Determine Overdue StatusβCreate:
def is_overdue( finding, reference_date=REFERENCE_DATE): if finding[ "status" ] != "open": return False
due_date = finding[ "remediation_due_date" ]
if due_date is None: return False
return ( due_date < reference_date )34 β Add Days Overdue
Section titled β34 β Add Days OverdueβCreate:
def calculate_days_overdue( finding, reference_date=REFERENCE_DATE): if not is_overdue( finding, reference_date ): return 0
return ( reference_date - finding[ "remediation_due_date" ] ).days35 β Add Finding Age
Section titled β35 β Add Finding AgeβCreate:
def calculate_finding_age( finding, reference_date=REFERENCE_DATE): discovered = finding[ "discovered_date" ]
if discovered is None: return None
return ( reference_date - discovered ).days36 β Why Age Matters
Section titled β36 β Why Age MattersβA critical vulnerability open for:
1 DAYand a critical vulnerability open for:
120 DAYSmay require different management attention.
37 β Add Time Context
Section titled β37 β Add Time ContextβCreate:
def add_time_context( findings): results = []
for finding in findings: item = finding.copy()
item["overdue"] = ( is_overdue( item ) )
item["days_overdue"] = ( calculate_days_overdue( item ) )
item["finding_age_days"] = ( calculate_finding_age( item ) )
results.append( item )
return results38 β Build the Risk Model
Section titled β38 β Build the Risk ModelβFor training, create a score based on:
CVSS
ASSET CRITICALITY
INTERNET EXPOSURE
OVERDUE STATUS
MISSING OWNER39 β Important Risk Warning
Section titled β39 β Important Risk WarningβThis scoring system is:
A TRAINING MODELIt is not a universal enterprise risk formula.
Real prioritization may include:
KNOWN EXPLOITATION
EPSS
THREAT INTELLIGENCE
BUSINESS IMPACT
DATA CLASSIFICATION
EXPOSURE
COMPENSATING CONTROLS
ASSET FUNCTION
ATTACK PATH CONTEXT40 β Criticality Weight
Section titled β40 β Criticality WeightβCreate:
CRITICALITY_WEIGHT = { "critical": 30, "high": 20, "medium": 10, "low": 5, "unknown": 0}41 β Severity Weight
Section titled β41 β Severity WeightβCreate:
SEVERITY_WEIGHT = { "critical": 30, "high": 20, "medium": 10, "low": 5, "informational": 0, "unknown": 0}42 β Build Priority Score
Section titled β42 β Build Priority ScoreβCreate:
def calculate_priority_score( finding): if finding[ "status" ] != "open": return 0
score = 0
score += SEVERITY_WEIGHT.get( finding["severity"], 0 )
score += CRITICALITY_WEIGHT.get( finding["criticality"], 0 )
if finding["cvss"] is not None: score += int( finding["cvss"] * 2 )
if finding[ "internet_exposed" ]: score += 20
if finding[ "overdue" ]: score += 15
if not finding[ "owner" ]: score += 10
return score43 β Example Priority Calculation
Section titled β43 β Example Priority CalculationβSuppose:
Severity:Criticalβ 30
Asset Criticality:Criticalβ 30
CVSS:9.8β 19
Internet Exposed:Yesβ 20
Overdue:Yesβ 15Total:
114This is:
A PRIORITIZATION SCOREnot a standardized risk value.
44 β Priority Categories
Section titled β44 β Priority CategoriesβCreate:
def determine_priority( score): if score >= 100: return "P1"
if score >= 75: return "P2"
if score >= 50: return "P3"
if score > 0: return "P4"
return "Closed"45 β Priority Meaning
Section titled β45 β Priority MeaningβFor this lab:
P1Immediate remediation review
P2High-priority remediation
P3Planned remediation
P4Standard remediation
ClosedNo active remediation queue46 β Add Priority Context
Section titled β46 β Add Priority ContextβCreate:
def add_priority_context( findings): results = []
for finding in findings: item = finding.copy()
score = ( calculate_priority_score( item ) )
item[ "priority_score" ] = score
item[ "priority" ] = determine_priority( score )
results.append( item )
return results47 β Build the Complete Processing Pipeline
Section titled β47 β Build the Complete Processing PipelineβCreate:
def process_findings(): assets = load_assets()
valid, invalid = ( load_vulnerabilities() )
unique, duplicates = ( deduplicate_findings( valid ) )
enriched = ( enrich_with_assets( unique, assets ) )
enriched = ( add_time_context( enriched ) )
enriched = ( add_priority_context( enriched ) )
return { "raw_valid_count": len(valid),
"invalid_count": len(invalid),
"unique_count": len(unique),
"duplicate_count": len(duplicates),
"findings": enriched,
"invalid": invalid,
"duplicates": duplicates }48 β Test the Pipeline
Section titled β48 β Test the PipelineβTemporarily:
results = process_findings()
print( json.dumps( results, indent=2, default=str ))Run:
python src/vulnerability_analyzer.py49 β Sort by Priority Score
Section titled β49 β Sort by Priority ScoreβCreate:
def sort_findings( findings): return sorted( findings, key=lambda item: item[ "priority_score" ], reverse=True )50 β Build the Remediation Queue
Section titled β50 β Build the Remediation QueueβCreate:
def build_remediation_queue( findings): return [ item for item in sort_findings( findings ) if item[ "status" ] == "open" ]51 β Expected High-Priority Findings
Section titled β51 β Expected High-Priority FindingsβYour highest-priority findings should likely include:
WEB01Outdated Web Framework
VPN01Remote Access Security Update
DB01Missing Security Update
OLD01Legacy Service ExposureExact ordering depends on your scoring logic.
52 β Why WEB01 Should Rank Highly
Section titled β52 β Why WEB01 Should Rank HighlyβContext:
SEVERITYCritical
CVSS9.8
ASSET CRITICALITYCritical
ENVIRONMENTProduction
INTERNET EXPOSEDYes
OVERDUEYesThat combination creates stronger remediation urgency.
53 β Why OLD01 Deserves Attention
Section titled β53 β Why OLD01 Deserves AttentionβContext:
HIGH-CRITICALITY ASSET
CRITICAL VULNERABILITY
OVERDUE
NO OWNEREven though it is not internet exposed, the ownership gap adds operational risk.
54 β Vulnerabilities Without Owners
Section titled β54 β Vulnerabilities Without OwnersβCreate:
def findings_without_owner( findings): return [ item for item in findings if ( item[ "status" ] == "open" and not item[ "owner" ] ) ]55 β Internet-Exposed Findings
Section titled β55 β Internet-Exposed FindingsβCreate:
def internet_exposed_findings( findings): return [ item for item in findings if ( item[ "status" ] == "open" and item[ "internet_exposed" ] ) ]56 β Overdue Findings
Section titled β56 β Overdue FindingsβCreate:
def overdue_findings( findings): return [ item for item in findings if item[ "overdue" ] ]57 β Critical Open Findings
Section titled β57 β Critical Open FindingsβCreate:
def critical_open_findings( findings): return [ item for item in findings if ( item[ "status" ] == "open" and item[ "severity" ] == "critical" ) ]58 β Count Findings by Severity
Section titled β58 β Count Findings by SeverityβCreate:
def count_by_severity( findings): return Counter( item["severity"] for item in findings if item[ "status" ] == "open" )59 β Count Findings by Owner
Section titled β59 β Count Findings by OwnerβCreate:
def count_by_owner( findings): counter = Counter()
for item in findings: if item[ "status" ] != "open": continue
owner = ( item["owner"] or "Unassigned" )
counter[owner] += 1
return counter60 β Count Findings by Priority
Section titled β60 β Count Findings by PriorityβCreate:
def count_by_priority( findings): return Counter( item["priority"] for item in findings if item[ "status" ] == "open" )61 β Build a Vulnerability Summary
Section titled β61 β Build a Vulnerability SummaryβCreate:
def build_summary( results): findings = results[ "findings" ]
open_findings = [ item for item in findings if item[ "status" ] == "open" ]
return { "valid_raw_findings": results[ "raw_valid_count" ],
"unique_findings": results[ "unique_count" ],
"duplicates_removed": results[ "duplicate_count" ],
"invalid_findings": results[ "invalid_count" ],
"open_findings": len( open_findings ),
"critical_open": len( critical_open_findings( findings ) ),
"overdue": len( overdue_findings( findings ) ),
"internet_exposed": len( internet_exposed_findings( findings ) ),
"unowned_open": len( findings_without_owner( findings ) ),
"severity_counts": dict( count_by_severity( findings ) ),
"priority_counts": dict( count_by_priority( findings ) ) }62 β Export the Remediation Queue
Section titled β62 β Export the Remediation QueueβCreate:
def export_remediation_queue( findings): output = ( REPORT_DIR / "remediation-queue.csv" )
fields = [ "priority", "priority_score", "hostname", "environment", "criticality", "owner", "internet_exposed", "vulnerability_name", "severity", "cvss", "discovered_date", "remediation_due_date", "overdue", "days_overdue" ]
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.DictWriter( file, fieldnames=fields, extrasaction="ignore" )
writer.writeheader()
for item in ( build_remediation_queue( findings ) ): writer.writerow( item )63 β Export Invalid Findings
Section titled β63 β Export Invalid FindingsβCreate:
def export_invalid_findings( records): output = ( REPORT_DIR / "invalid-findings.csv" )
fields = [ "line_number", "finding_id", "hostname", "vulnerability_name", "severity", "cvss", "reason" ]
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.DictWriter( file, fieldnames=fields, extrasaction="ignore" )
writer.writeheader()
writer.writerows( records )64 β Export Duplicate Findings
Section titled β64 β Export Duplicate FindingsβCreate:
def export_duplicates( records): output = ( REPORT_DIR / "duplicate-findings.csv" )
fields = [ "finding_id", "hostname", "vulnerability_name", "severity", "cvss", "status" ]
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.DictWriter( file, fieldnames=fields, extrasaction="ignore" )
writer.writeheader()
writer.writerows( records )65 β Export Unmapped Assets
Section titled β65 β Export Unmapped AssetsβCreate:
def export_unmapped_findings( findings): output = ( REPORT_DIR / "unmapped-assets.csv" )
records = get_unmapped_findings( findings )
fields = [ "finding_id", "hostname", "vulnerability_name", "severity", "cvss" ]
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.DictWriter( file, fieldnames=fields, extrasaction="ignore" )
writer.writeheader()
writer.writerows( records )66 β Export Owner Remediation Report
Section titled β66 β Export Owner Remediation ReportβCreate:
def export_owner_report( findings): output = ( REPORT_DIR / "owner-remediation-summary.csv" )
owner_data = defaultdict( lambda: { "open_findings": 0, "critical": 0, "p1": 0, "overdue": 0 } )
for item in findings: if item[ "status" ] != "open": continue
owner = ( item["owner"] or "Unassigned" )
owner_data[ owner ][ "open_findings" ] += 1
if item[ "severity" ] == "critical": owner_data[ owner ][ "critical" ] += 1
if item[ "priority" ] == "P1": owner_data[ owner ][ "p1" ] += 1
if item[ "overdue" ]: owner_data[ owner ][ "overdue" ] += 1
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.writer( file )
writer.writerow([ "owner", "open_findings", "critical_findings", "p1_findings", "overdue_findings" ])
for owner, data in sorted( owner_data.items() ): writer.writerow([ owner, data[ "open_findings" ], data[ "critical" ], data[ "p1" ], data[ "overdue" ] ])67 β Export JSON
Section titled β67 β Export JSONβCreate:
def export_json( results, summary): output = ( REPORT_DIR / "vulnerability-analysis.json" )
data = { "reference_date": str( REFERENCE_DATE ),
"summary": summary,
"findings": results[ "findings" ],
"invalid": results[ "invalid" ],
"duplicates": results[ "duplicates" ] }
with output.open( "w", encoding="utf-8" ) as file:
json.dump( data, file, indent=2, default=str )68 β Generate the Markdown Report
Section titled β68 β Generate the Markdown ReportβCreate:
def generate_markdown_report( results, summary): findings = results[ "findings" ]
queue = ( build_remediation_queue( findings ) )
lines = []
lines.append( "# Vulnerability Prioritization Report" )
lines.append("")
lines.append( f"Reference Date: {REFERENCE_DATE}" )
lines.append("")
lines.append( "## Executive Summary" )
lines.append("")
lines.append( f"- Open findings: " f"{summary['open_findings']}" )
lines.append( f"- Critical open findings: " f"{summary['critical_open']}" )
lines.append( f"- Overdue findings: " f"{summary['overdue']}" )
lines.append( f"- Internet-exposed findings: " f"{summary['internet_exposed']}" )
lines.append( f"- Open findings without owner: " f"{summary['unowned_open']}" )
lines.append( f"- Duplicate findings removed: " f"{summary['duplicates_removed']}" )
lines.append( f"- Invalid findings rejected: " f"{summary['invalid_findings']}" )
lines.append("")
lines.append( "## Highest Priority Remediation" )
lines.append("")
for item in queue[:10]: lines.append( f"- {item['priority']} | " f"{item['hostname']} | " f"{item['vulnerability_name']} | " f"Score {item['priority_score']} | " f"Owner: {item['owner'] or 'Unassigned'}" )
lines.append("")
lines.append( "## Overdue Findings" )
lines.append("")
overdue = overdue_findings( findings )
if overdue: for item in sort_findings( overdue ): lines.append( f"- {item['hostname']} | " f"{item['vulnerability_name']} | " f"{item['days_overdue']} days overdue" ) else: lines.append( "- No overdue findings." )
lines.append("")
lines.append( "## Unmapped Assets" )
lines.append("")
unmapped = get_unmapped_findings( findings )
if unmapped: for item in unmapped: lines.append( f"- {item['hostname']} | " f"{item['vulnerability_name']}" ) else: lines.append( "- No unmapped assets." )
lines.append("")
lines.append( "## Analyst Guidance" )
lines.append("")
lines.append( "Priority scores in this lab are training values. " "Validate remediation urgency using asset criticality, " "exposure, threat intelligence, exploitability, business " "impact, compensating controls, and change-management " "requirements before making production decisions." )
output = ( REPORT_DIR / "vulnerability-prioritization-report.md" )
output.write_text( "\n".join( lines ), encoding="utf-8" )69 β Create main()
Section titled β69 β Create main()βAdd:
def main(): results = process_findings()
summary = build_summary( results )
export_remediation_queue( results[ "findings" ] )
export_invalid_findings( results[ "invalid" ] )
export_duplicates( results[ "duplicates" ] )
export_unmapped_findings( results[ "findings" ] )
export_owner_report( results[ "findings" ] )
export_json( results, summary )
generate_markdown_report( results, summary )
print( "Vulnerability analysis complete." )
print( f"Reports saved to: " f"{REPORT_DIR}" )70 β Add the Entry Point
Section titled β70 β Add the Entry PointβAdd:
if __name__ == "__main__": main()71 β Run the Complete Tool
Section titled β71 β Run the Complete ToolβRun:
python src/vulnerability_analyzer.pyExpected:
Vulnerability analysis complete.Reports saved to: ...72 β Review the Output Files
Section titled β72 β Review the Output FilesβYou should now have:
reports/|+-- remediation-queue.csv|+-- invalid-findings.csv|+-- duplicate-findings.csv|+-- unmapped-assets.csv|+-- owner-remediation-summary.csv|+-- vulnerability-analysis.json|+-- vulnerability-prioritization-report.md73 β Review the Remediation Queue
Section titled β73 β Review the Remediation QueueβThe queue should place the most important open findings near the top.
Example:
P1 WEB01 Outdated Web Framework
P1 VPN01 Remote Access Security Update
P1 DB01 Missing Security Update
P1/P2 OLD01 Legacy Service ExposureExact priority may depend on your calculated score.
74 β Important Lesson About CVSS
Section titled β74 β Important Lesson About CVSSβDo not prioritize only by:
CVSSCompare:
CVSS 9.8on development systemwith:
CVSS 8.1on internet-facing critical production systemRisk context can change urgency.
75 β Risk Context Model
Section titled β75 β Risk Context ModelβTECHNICAL SEVERITY +ASSET CRITICALITY +EXPOSURE +THREAT CONTEXT +AGE +OWNERSHIP =REMEDIATION PRIORITY76 β Add Severity Statistics
Section titled β76 β Add Severity StatisticsβExample:
summary = count_by_severity( results["findings"])
print(summary)Expected structure:
{ "critical": 4, "high": 3, "medium": 1}Exact counts depend on deduplication and open status.
77 β Add Priority Statistics
Section titled β77 β Add Priority StatisticsβExample:
print( count_by_priority( results["findings"] ))This helps produce:
P1 COUNT
P2 COUNT
P3 COUNT
P4 COUNT78 β Build an SLA Model
Section titled β78 β Build an SLA ModelβA real organization may define:
CRITICAL15 days
HIGH30 days
MEDIUM60 days
LOW90 daysFor the lab, treat existing:
remediation_due_dateas the approved SLA-derived deadline.
79 β SLA Compliance Metric
Section titled β79 β SLA Compliance MetricβCreate:
def sla_compliance( findings): open_items = [ item for item in findings if item[ "status" ] == "open" ]
if not open_items: return 100.0
compliant = [ item for item in open_items if not item[ "overdue" ] ]
return round( 100.0 * len(compliant) / len(open_items), 2 )80 β Why SLA Metrics Matter
Section titled β80 β Why SLA Metrics MatterβLeadership may ask:
WHAT PERCENTAGE OF OPEN FINDINGSARE WITHIN REMEDIATION SLA?This is different from:
HOW MANY FINDINGS EXIST?81 β Add SLA Metric to Summary
Section titled β81 β Add SLA Metric to SummaryβInclude:
"open_sla_compliance_percentage": sla_compliance( findings )82 β Age Buckets
Section titled β82 β Age BucketsβCreate:
def age_bucket( days): if days is None: return "unknown"
if days <= 30: return "0-30"
if days <= 60: return "31-60"
if days <= 90: return "61-90"
return "90+"83 β Why Age Buckets Matter
Section titled β83 β Why Age Buckets MatterβA vulnerability management dashboard might show:
0β30 DAYS
31β60 DAYS
61β90 DAYS
90+ DAYSLarge numbers in:
90+may indicate remediation backlog.
84 β Add Age Bucket to Findings
Section titled β84 β Add Age Bucket to FindingsβInside time enrichment:
item[ "age_bucket"] = age_bucket( item[ "finding_age_days" ])85 β Count Findings by Age Bucket
Section titled β85 β Count Findings by Age BucketβCreate:
def count_by_age_bucket( findings): return Counter( item[ "age_bucket" ] for item in findings if item[ "status" ] == "open" )86 β Owner Accountability
Section titled β86 β Owner AccountabilityβA mature remediation queue should answer:
WHO NEEDS TO ACT?not only:
WHAT IS VULNERABLE?87 β Owner Summary Mental Model
Section titled β87 β Owner Summary Mental ModelβFINDING βASSET βOWNER βPRIORITY βDUE DATE βREMEDIATION ACTION88 β Missing Owner Escalation
Section titled β88 β Missing Owner EscalationβIf:
OWNER = EMPTYdo not simply discard the finding.
Create:
OWNERSHIP REVIEWbecause unowned systems can become unmanaged risk.
89 β Data Quality Matters
Section titled β89 β Data Quality MattersβYour pipeline already detects:
INVALID CVSS
UNMAPPED ASSET
DUPLICATE FINDINGA mature program should also detect:
MISSING HOSTNAME
INVALID DATE
UNKNOWN SEVERITY
MISSING FINDING ID
MISSING OWNER
UNKNOWN ENVIRONMENT90 β Add Logging
Section titled β90 β Add LoggingβImport:
import loggingConfigure:
logging.basicConfig( level=logging.INFO, format=( "%(asctime)s " "%(levelname)s " "%(message)s" ))91 β Add Pipeline Logging
Section titled β91 β Add Pipeline LoggingβExample:
logging.info( "Loading asset inventory")
logging.info( "Loading vulnerability data")
logging.info( "Deduplicating findings")
logging.info( "Generating remediation reports")92 β Do Not Log Sensitive Data Needlessly
Section titled β92 β Do Not Log Sensitive Data NeedlesslyβReal vulnerability records may reveal:
INTERNAL HOSTNAMES
IP ADDRESSES
SYSTEM OWNERS
SECURITY WEAKNESSES
BUSINESS SYSTEMSTreat vulnerability reports as sensitive security information.
93 β Add Command-Line Arguments
Section titled β93 β Add Command-Line ArgumentsβImport:
import argparseCreate:
def get_arguments(): parser = argparse.ArgumentParser( description=( "Analyze vulnerability scanner " "exports and build a remediation queue." ) )
parser.add_argument( "--vulnerabilities", default=str( VULNERABILITY_FILE ), help="Path to vulnerability CSV" )
parser.add_argument( "--assets", default=str( ASSET_FILE ), help="Path to asset inventory CSV" )
return parser.parse_args()94 β Future Input Flexibility
Section titled β94 β Future Input FlexibilityβEventually you could run:
python src/vulnerability_analyzer.py \ --vulnerabilities data/weekly-scan.csv \ --assets data/cmdb-export.csv95 β Safe Automation Principle
Section titled β95 β Safe Automation PrincipleβThe tool should:
ANALYZE
PRIORITIZE
REPORTIt should not automatically:
PATCH SYSTEMS
SHUT DOWN SERVERS
DISABLE SERVICES
CHANGE FIREWALL RULESwithout approved remediation workflows.
96 β Vulnerability Management Workflow
Section titled β96 β Vulnerability Management WorkflowβDISCOVER βVALIDATE βNORMALIZE βDEDUPLICATE βENRICH βPRIORITIZE βASSIGN OWNER βREMEDIATE βVERIFY βCLOSE97 β Remediation Is Not Closure
Section titled β97 β Remediation Is Not ClosureβA vulnerability should not be considered closed because someone says:
PATCH APPLIEDYou should ideally:
RESCAN
VERIFY
CONFIRM FINDING NO LONGER EXISTS98 β Add Verification Status Concept
Section titled β98 β Add Verification Status ConceptβA production schema might contain:
status
remediation_status
verification_statusExample:
Openβ Remediation In Progressβ Pending Verificationβ Verified Closed99 β Risk Acceptance Concept
Section titled β99 β Risk Acceptance ConceptβNot every vulnerability can be immediately remediated.
Possible handling includes:
REMEDIATE
MITIGATE
ACCEPT RISK
DECOMMISSION
DEFER WITH APPROVAL100 β Risk Acceptance Requirements
Section titled β100 β Risk Acceptance RequirementsβRisk acceptance should include:
OWNER
BUSINESS JUSTIFICATION
EXPIRY DATE
APPROVER
COMPENSATING CONTROLS
REVIEW DATEnot simply:
WE CANNOT FIX IT101 β False Positive Handling
Section titled β101 β False Positive HandlingβSome scanner findings may be inaccurate.
Workflow:
FINDING βVALIDATE βFALSE POSITIVE? βDOCUMENT EVIDENCE βAPPROVED EXCEPTION102 β Do Not Delete False Positives Blindly
Section titled β102 β Do Not Delete False Positives BlindlyβInstead retain:
WHY IT WAS CLOSED
WHO APPROVED IT
WHEN IT WAS REVIEWEDThis supports auditability.
103 β Add Threat Intelligence Concept
Section titled β103 β Add Threat Intelligence ConceptβA more advanced prioritization model could add:
KNOWN EXPLOITED?
ACTIVE CAMPAIGN?
EXPLOIT AVAILABLE?
EPSS?Then:
VULNERABILITY +THREAT CONTEXT =BETTER PRIORITY104 β Important Threat Context Rule
Section titled β104 β Important Threat Context RuleβEven if a vulnerability is known to be actively exploited, you still need:
IS THE AFFECTED PRODUCT PRESENT?
IS THE VULNERABLE VERSION PRESENT?
IS THE SYSTEM EXPOSED?
DO COMPENSATING CONTROLS EXIST?105 β Add Asset Tiering
Section titled β105 β Add Asset TieringβInstead of only:
Critical
High
Medium
Lowsome enterprises may use:
Tier 0Identity-critical
Tier 1Business-critical
Tier 2Important
Tier 3Standard106 β Prioritization Becomes Better With Context
Section titled β106 β Prioritization Becomes Better With ContextβVULNERABILITY βASSET βBUSINESS SERVICE βDATA SENSITIVITY βEXPOSURE βTHREAT βOWNER βPRIORITY107 β Create Test Suite
Section titled β107 β Create Test SuiteβCreate:
tests/test_vulnerability_analyzer.py108 β Test Severity Normalization
Section titled β108 β Test Severity Normalizationβdef test_normalize_severity(): assert ( normalize_severity( "CRITICAL" ) == "critical" )109 β Test CVSS Validation
Section titled β109 β Test CVSS Validationβdef test_valid_cvss(): assert ( parse_cvss( "9.8" ) == 9.8 )110 β Test Invalid CVSS
Section titled β110 β Test Invalid CVSSβdef test_invalid_cvss(): assert ( parse_cvss( "not-a-score" ) is None )111 β Test Hostname Normalization
Section titled β111 β Test Hostname Normalizationβdef test_hostname_normalization(): assert ( normalize_hostname( " web01 " ) == "WEB01" )112 β Test Boolean Parsing
Section titled β112 β Test Boolean Parsingβdef test_boolean(): assert parse_boolean( "true" )
assert not parse_boolean( "false" )113 β Test Overdue Logic
Section titled β113 β Test Overdue LogicβExample:
def test_overdue(): finding = { "status": "open", "remediation_due_date": date( 2026, 8, 15 ) }
assert is_overdue( finding )114 β Test Closed Finding
Section titled β114 β Test Closed Findingβdef test_closed_not_overdue(): finding = { "status": "closed", "remediation_due_date": date( 2026, 7, 1 ) }
assert not is_overdue( finding )115 β Test Deduplication
Section titled β115 β Test DeduplicationβProvide two:
WEB01+Outdated Web Framework+Openrecords.
Verify:
UNIQUE = 1
DUPLICATE = 1116 β Test Unknown Asset
Section titled β116 β Test Unknown AssetβEnsure:
UNKNOWN01receives:
asset_mapped = False117 β Test Priority Score
Section titled β117 β Test Priority ScoreβCreate a synthetic finding with:
critical severity
critical asset
CVSS 9.8
internet exposed
overdueVerify that it receives:
P1under your training model.
118 β Challenge 01 β Add CVE
Section titled β118 β Challenge 01 β Add CVEβAdd a field:
cveto the scanner export.
Then use it as part of your deduplication logic.
119 β Challenge 02 β Add Port
Section titled β119 β Challenge 02 β Add PortβAdd:
portand:
protocolThis helps distinguish the same finding across different services.
120 β Challenge 03 β Add Business Service
Section titled β120 β Challenge 03 β Add Business ServiceβUpdate:
assets.csvwith:
business_serviceExamples:
Customer Portal
Payments
Identity
Corporate VPN121 β Challenge 04 β Add Data Classification
Section titled β121 β Challenge 04 β Add Data ClassificationβAdd:
data_classificationsuch as:
Public
Internal
Confidential
RestrictedThen adjust priority.
122 β Challenge 05 β Add Exploitability Context
Section titled β122 β Challenge 05 β Add Exploitability ContextβUse synthetic data:
known_exploited
exploit_availableDo not use this as proof of compromise.
Use it only as additional prioritization context.
123 β Challenge 06 β Add Compensating Controls
Section titled β123 β Challenge 06 β Add Compensating ControlsβExample fields:
network_isolated
waf_protected
edr_enabledThen ask:
SHOULD THESE LOWER PRIORITY?
BY HOW MUCH?
WHO APPROVES THAT LOGIC?124 β Challenge 07 β Add SLA Calculations
Section titled β124 β Challenge 07 β Add SLA CalculationsβInstead of supplying due dates manually, calculate them from:
SEVERITY
DISCOVERED DATE
SLA POLICYExample training values:
Critical15 days
High30 days
Medium60 days
Low90 days125 β Challenge 08 β Add Owner-Specific Reports
Section titled β125 β Challenge 08 β Add Owner-Specific ReportsβGenerate:
Web-Team.csv
Database-Team.csv
Network-Team.csv
Unassigned.csv126 β Challenge 09 β Add HTML Dashboard
Section titled β126 β Challenge 09 β Add HTML DashboardβCreate a simple HTML report showing:
TOTAL OPEN
P1
P2
OVERDUE
INTERNET EXPOSED
UNOWNED
TOP OWNERS127 β Challenge 10 β Store Results in SQLite
Section titled β127 β Challenge 10 β Store Results in SQLiteβArchitecture:
SCANNER CSV βPYTHON βNORMALIZED FINDINGS βSQLITE βSQL REPORTINGThis connects directly to:
Lab 05 β SQL Security Analytics128 β Challenge 11 β Compare Two Scan Dates
Section titled β128 β Challenge 11 β Compare Two Scan DatesβCreate:
scan-week-1.csv
scan-week-2.csvIdentify:
NEW FINDINGS
REMEDIATED FINDINGS
PERSISTENT FINDINGS129 β Vulnerability Trend Model
Section titled β129 β Vulnerability Trend ModelβPREVIOUS SCAN βCOMPARE βCURRENT SCAN βNEWREMEDIATEDUNCHANGED130 β Why Trending Matters
Section titled β130 β Why Trending MattersβManagement should not only ask:
HOW MANY VULNERABILITIESDO WE HAVE?Also ask:
ARE WE GETTING BETTER?
ARE FINDINGS AGING?
ARE P1 ITEMS CLOSING?
IS THE BACKLOG GROWING?131 β Challenge 12 β Mean Time to Remediate
Section titled β131 β Challenge 12 β Mean Time to RemediateβIf you add:
closed_dateyou can calculate:
MTTRConceptually:
CLOSED DATE-DISCOVERED DATE=REMEDIATION TIME132 β Vulnerability Management Metrics
Section titled β132 β Vulnerability Management MetricsβUseful metrics may include:
OPEN FINDINGS
CRITICAL OPEN
P1 OPEN
OVERDUE
SLA COMPLIANCE
UNOWNED FINDINGS
INTERNET-EXPOSED FINDINGS
90+ DAY FINDINGS
MEAN TIME TO REMEDIATE
REOPENED FINDINGS133 β Be Careful With Metrics
Section titled β133 β Be Careful With MetricsβMetrics can drive bad behavior if poorly designed.
Example:
GOAL:REDUCE VULNERABILITY COUNTcould encourage teams to close:
LOW-RISK EASY FINDINGSwhile ignoring:
HIGH-RISK DIFFICULT FINDINGSMeasure:
RISK REDUCTIONnot only:
TICKET VOLUME134 β Analyst Investigation Exercise 01
Section titled β134 β Analyst Investigation Exercise 01βYou find:
WEB01
Critical vulnerability
CVSS 9.8
Internet exposed
14 days overdueWhat should happen?
Review:
VALIDATE FINDING
CONFIRM AFFECTED VERSION
CHECK EXPOSURE
CHECK COMPENSATING CONTROLS
CONTACT OWNER
PRIORITIZE CHANGE
REMEDIATE
RESCAN135 β Analyst Investigation Exercise 02
Section titled β135 β Analyst Investigation Exercise 02βYou find:
OLD01
Critical vulnerability
No asset owner
Production
High criticalityThe vulnerability is important.
But there is also a:
GOVERNANCE FAILUREbecause nobody owns the system.
136 β Analyst Investigation Exercise 03
Section titled β136 β Analyst Investigation Exercise 03βYou find:
UNKNOWN01
High severity
CVSS 8.0
Asset not in inventoryPossible response:
VERIFY HOSTNAME
SEARCH CMDB
CHECK SCANNER ASSET ID
CHECK WHETHER SYSTEM WAS DECOMMISSIONED
IDENTIFY OWNER
DO NOT DISCARD THE FINDING137 β Analyst Investigation Exercise 04
Section titled β137 β Analyst Investigation Exercise 04βA finding has:
CVSS 9.8but the asset is:
Development
Not internet exposed
Short-lived lab systemShould it automatically be the organizationβs top risk?
Not necessarily.
Context matters.
138 β Analyst Investigation Exercise 05
Section titled β138 β Analyst Investigation Exercise 05βA finding has:
CVSS 7.5on:
JUMP01
Critical administrative hostIt may deserve greater attention than raw CVSS suggests because of:
ASSET ROLE139 β Vulnerability Prioritization Mental Model
Section titled β139 β Vulnerability Prioritization Mental ModelβDO NOT ASK ONLY:
"HOW SEVERE ISTHE VULNERABILITY?"
ALSO ASK:
"WHERE IS IT?"
"WHAT DOES THE ASSET DO?"
"WHO OWNS IT?"
"IS IT EXPOSED?"
"HOW OLD IS IT?"
"IS IT BEING EXPLOITED?"
"WHAT CONTROLS EXIST?"140 β Build the Final Portfolio Structure
Section titled β140 β Build the Final Portfolio StructureβYour final repository should look like:
vulnerability-prioritization/|+-- data/| +-- assets.csv| +-- vulnerabilities.csv|+-- src/| +-- vulnerability_analyzer.py|+-- tests/| +-- test_vulnerability_analyzer.py|+-- reports/| +-- remediation-queue.csv| +-- invalid-findings.csv| +-- duplicate-findings.csv| +-- unmapped-assets.csv| +-- owner-remediation-summary.csv| +-- vulnerability-analysis.json| +-- vulnerability-prioritization-report.md|+-- README.md|+-- architecture.md141 β README Structure
Section titled β141 β README StructureβInclude:
PROJECT OVERVIEW
SECURITY PROBLEM
ARCHITECTURE
INPUT DATA
NORMALIZATION
DEDUPLICATION
ASSET ENRICHMENT
RISK MODEL
OUTPUT REPORTS
HOW TO RUN
TESTING
SECURITY CONSIDERATIONS
LIMITATIONS
FUTURE IMPROVEMENTS142 β Document Your Risk Formula
Section titled β142 β Document Your Risk FormulaβBe transparent.
Do not simply output:
Risk Score = 114without explaining where it came from.
Document:
Severity weight
Asset criticality weight
CVSS contribution
Internet exposure weight
Overdue weight
Missing-owner weight143 β Why Explainability Matters
Section titled β143 β Why Explainability MattersβSecurity teams need to understand:
WHY DID THIS FINDINGBECOME P1?A transparent model supports:
AUDITABILITY
TRUST
TUNING
GOVERNANCE144 β Common Vulnerability Management Mistakes
Section titled β144 β Common Vulnerability Management MistakesβAvoid:
PRIORITIZING ONLY BY CVSS
NO ASSET CONTEXT
NO OWNERSHIP DATA
NO DEDUPLICATION
NO SLA TRACKING
IGNORING OLD FINDINGS
IGNORING UNKNOWN ASSETS
COUNTING CLOSED ITEMS AS ACTIVE RISK
NO FALSE-POSITIVE PROCESS
NO VERIFICATION AFTER REMEDIATION
AUTOMATICALLY PATCHING WITHOUT CHANGE CONTROL
NO DOCUMENTED RISK MODEL145 β Data Pipeline Mistakes
Section titled β145 β Data Pipeline MistakesβAvoid:
TRUSTING EVERY CSV FIELD
CRASHING ON ONE BAD RECORD
SILENTLY DISCARDING INVALID DATA
CASE-SENSITIVE HOSTNAME MISMATCHES
BAD DATE PARSING
NO DUPLICATE HANDLING
NO REPORT VERSIONING
NO LOGGING146 β Mission Validation Checklist
Section titled β146 β Mission Validation ChecklistβConfirm:
- Python environment prepared
- Asset CSV created
- Vulnerability CSV created
- Asset inventory loads
- Vulnerability data loads
- Required columns validated
- Hostnames normalized
- Severity normalized
- CVSS validated
- Dates parsed
- Invalid findings recorded
- Duplicate findings identified
- Duplicate findings removed
- Findings mapped to assets
- Unknown assets identified
- Asset criticality added
- Asset owner added
- Internet exposure added
- Overdue status calculated
- Days overdue calculated
- Finding age calculated
- Priority score calculated
- Priority category calculated
- Remediation queue generated
- P1 findings reviewed
- Open critical findings reviewed
- Internet-exposed findings reviewed
- Unowned findings reviewed
- Owner summary generated
- CSV reports generated
- JSON report generated
- Markdown report generated
- Risk formula documented
- Bad-data behavior tested
- Deduplication tested
- Priority logic tested
- No automatic remediation performed
- Limitations documented
Mission Review
Section titled βMission ReviewβYou started with:
RAW VULNERABILITYSCANNER DATAcontaining:
DUPLICATES
BAD DATA
SEVERITY
CVSS
DATES
HOSTNAMESYou transformed it into:
SCANNER FINDING βVALIDATE βNORMALIZE βDEDUPLICATE βMAP ASSET βADD BUSINESS CONTEXT βCHECK EXPOSURE βCHECK AGE / SLA βCALCULATE PRIORITY βASSIGN OWNER βREMEDIATION QUEUEWhat You Built
Section titled βWhat You BuiltβYou now have a vulnerability management analytics workflow capable of answering:
WHAT SHOULD WE FIX FIRST?
WHICH FINDINGS ARE OVERDUE?
WHICH CRITICAL ASSETS ARE AFFECTED?
WHICH INTERNET-EXPOSED ASSETS ARE VULNERABLE?
WHICH FINDINGS HAVE NO OWNER?
WHICH SCANNER RECORDS ARE DUPLICATES?
WHICH ASSETS ARE MISSING FROM INVENTORY?Key Security Lesson
Section titled βKey Security LessonβThe central lesson is:
VULNERABILITYSEVERITYIS NOT THE SAME ASBUSINESS RISKA useful remediation decision requires:
TECHNICAL SEVERITY +ASSET CRITICALITY +EXPOSURE +THREAT CONTEXT +AGE +OWNERSHIP +BUSINESS IMPACT =REMEDIATION PRIORITYFinal Mental Model
Section titled βFinal Mental ModelβWhenever vulnerability data arrives, think:
IS THE DATA VALID? βIS IT A DUPLICATE? βWHICH ASSET IS AFFECTED? βHOW CRITICAL IS THE ASSET? βIS IT EXPOSED? βHOW SEVERE IS THE FINDING? βHOW OLD IS IT? βIS IT OVERDUE? βWHO OWNS IT? βWHAT SHOULD BE FIXED FIRST? βHOW WILL WE VERIFY CLOSURE?The goal is not:
GENERATE A LONGERVULNERABILITY REPORTThe goal is:
TURN SCANNER DATAINTO A PRIORITIZED,OWNED,TRACKABLEREMEDIATION PROGRAMWhatβs Next?
Section titled βWhatβs Next?ββ‘οΈ Lab 07 β Security API Integration
The next lab moves from local CSV-based vulnerability analysis into external security-system integration.
You will build:
SECURITY AUTOMATION βAPI CLIENT βAUTHENTICATION βREQUEST βTIMEOUT βRESPONSE VALIDATION βJSON PARSING βERROR HANDLING βRETRY βRATE LIMIT AWARENESS βNORMALIZED SECURITY DATAYou will use a controlled training API or local mock service to learn how security tools exchange data safely without hard-coding secrets or depending on uncontrolled external systems.