Lab 09 β SOC Alert Triage Automation
Mission Information
Section titled βMission InformationβDifficulty: Advanced
Estimated Time: 180β240 minutes
Primary Language: Python
Security Domain: SOC / Detection Engineering / Incident Response / Security Automation
Environment: Local controlled lab
Automation Type: Defensive SOC Alert Triage
Mission
Section titled βMissionβYou are working as a SOC automation engineer.
Your SOC receives alerts from multiple security technologies:
SIEM
EDR / XDR
IDENTITY PLATFORM
FIREWALL
CLOUD SECURITY
VULNERABILITY MANAGEMENT
THREAT INTELLIGENCEThe challenge is not simply receiving alerts.
The challenge is determining:
WHICH ALERTSREQUIRE ATTENTION FIRST?Your mission is to build a Python automation pipeline that transforms raw alerts into enriched, prioritized analyst work items.
The final workflow will be:
RAW ALERTS βVALIDATE βNORMALIZE βDEDUPLICATE βENRICH βββββΌβββββββββββββββ β β β βUSER ASSET IOC VULNERABILITY β β β β βββββΌβββββββββββββββ βCORRELATE βRISK SCORE βTRIAGE RECOMMENDATION βANALYST QUEUE βHUMAN REVIEWWhy This Lab Matters
Section titled βWhy This Lab MattersβA SOC can receive:
100
1,000
10,000+
ALERTS PER DAYTreating every alert equally creates:
ALERT FATIGUE
SLOW INVESTIGATION
MISSED CRITICAL EVENTS
INCONSISTENT TRIAGE
ANALYST BURNOUTSecurity automation helps analysts answer:
WHO IS INVOLVED?
WHAT ASSET IS INVOLVED?
HOW CRITICAL IS THE ASSET?
IS THE USER PRIVILEGED?
IS THE SOURCE KNOWN?
ARE THERE VULNERABILITIES?
IS THE ASSET INTERNET-FACING?
ARE THERE RELATED ALERTS?
WHAT SHOULD BE INVESTIGATED FIRST?Important Principle
Section titled βImportant PrincipleβThis lab automates:
COLLECTION
NORMALIZATION
ENRICHMENT
CORRELATION
PRIORITIZATION
REPORTINGIt does not automatically:
DISABLE ACCOUNTS
ISOLATE ENDPOINTS
BLOCK IP ADDRESSES
DELETE CLOUD RESOURCES
RESET PASSWORDS
MODIFY FIREWALL RULESHigh-impact response remains:
ANALYST / INCIDENT RESPONDER βVALIDATION βAPPROVAL βCONTROLLED RESPONSELearning Objectives
Section titled βLearning ObjectivesβBy completing this lab, you should be able to:
PROCESS SOC ALERTS
VALIDATE SECURITY EVENTS
NORMALIZE MULTIPLE ALERT TYPES
DEDUPLICATE ALERTS
ENRICH IDENTITY CONTEXT
ENRICH ASSET CONTEXT
ENRICH IOC CONTEXT
ADD VULNERABILITY CONTEXT
ADD CLOUD CONTEXT
CORRELATE RELATED ALERTS
BUILD EXPLAINABLE RISK SCORING
ASSIGN TRIAGE PRIORITIES
GENERATE ANALYST RECOMMENDATIONS
BUILD INVESTIGATION QUEUES
EXPORT CSV
EXPORT JSON
GENERATE MARKDOWN REPORTS
PRESERVE HUMAN APPROVALFinal Architecture
Section titled βFinal Architectureβ SECURITY ALERTS β ALERT INGESTION β VALIDATION β NORMALIZATION β DEDUPLICATION β ENRICHMENT βββββββββββββββββββΌββββββββββββββββββ β β β IDENTITY ASSET IOC β β β PRIVILEGE CRITICALITY REPUTATION β β β ββββββββββββββ¬βββββ΄βββββ¬βββββββββββββ β β VULNERABILITY CLOUD β β ββββββ¬βββββ β CORRELATION β RISK SCORING β TRIAGE RECOMMENDATION β ANALYST QUEUE β HUMAN REVIEW01 β Create the Lab Workspace
Section titled β01 β Create the Lab WorkspaceβCreate:
soc-alert-triage/|+-- data/|+-- reports/|+-- src/|+-- tests/|+-- logs/|+-- README.mdLinux/macOS:
mkdir -p soc-alert-triage/{data,reports,src,tests,logs}cd soc-alert-triagePowerShell:
mkdir soc-alert-triage
cd soc-alert-triage
mkdir datamkdir reportsmkdir srcmkdir testsmkdir logs02 β Verify Python
Section titled β02 β Verify PythonβRun:
python --versionRecommended:
Python 3.10+03 β Understand SOC Triage
Section titled β03 β Understand SOC TriageβSOC triage is the process of determining:
WHAT HAPPENED?
WHO IS INVOLVED?
WHAT SYSTEM IS INVOLVED?
HOW IMPORTANT IS IT?
WHAT CONTEXT EXISTS?
IS THERE RELATED ACTIVITY?
WHAT SHOULD THE ANALYST DO NEXT?04 β Alert vs Incident
Section titled β04 β Alert vs IncidentβAn:
ALERTis a signal that requires evaluation.
An:
INCIDENTis a security event or group of events that has been validated and managed according to the organizationβs incident process.
Therefore:
ALERTβ CONFIRMED INCIDENT05 β Create Synthetic Alerts
Section titled β05 β Create Synthetic AlertsβCreate:
data/alerts.jsonAdd:
[ { "alert_id": "ALT-1001", "timestamp": "2026-08-29T08:15:00Z", "source": "identity-monitor", "alert_type": "multiple_failed_logins", "severity": "medium", "user": "admin01", "asset": "JUMP01", "source_ip": "203.0.113.25", "description": "Multiple failed authentication attempts detected." }, { "alert_id": "ALT-1002", "timestamp": "2026-08-29T08:18:00Z", "source": "identity-monitor", "alert_type": "successful_login_after_failures", "severity": "high", "user": "admin01", "asset": "JUMP01", "source_ip": "203.0.113.25", "description": "Successful authentication observed after repeated failures." }, { "alert_id": "ALT-1003", "timestamp": "2026-08-29T08:22:00Z", "source": "endpoint-monitor", "alert_type": "suspicious_process", "severity": "high", "user": "user01", "asset": "WEB01", "source_ip": "198.51.100.50", "description": "Endpoint monitoring generated a suspicious process alert." }, { "alert_id": "ALT-1004", "timestamp": "2026-08-29T08:30:00Z", "source": "cloud-security", "alert_type": "public_storage", "severity": "medium", "user": "", "asset": "legacy-public-files", "source_ip": "", "description": "Cloud storage resource appears publicly accessible." }, { "alert_id": "ALT-1005", "timestamp": "2026-08-29T08:35:00Z", "source": "vulnerability-monitor", "alert_type": "critical_vulnerability", "severity": "critical", "user": "", "asset": "WEB01", "source_ip": "", "description": "Critical vulnerability identified on WEB01." }, { "alert_id": "ALT-1006", "timestamp": "2026-08-29T08:40:00Z", "source": "network-monitor", "alert_type": "unusual_connection", "severity": "medium", "user": "service-web", "asset": "WEB01", "source_ip": "192.0.2.75", "description": "Unusual network connection requires analyst review." }, { "alert_id": "ALT-1006", "timestamp": "2026-08-29T08:40:00Z", "source": "network-monitor", "alert_type": "unusual_connection", "severity": "medium", "user": "service-web", "asset": "WEB01", "source_ip": "192.0.2.75", "description": "Duplicate training alert." }]06 β Why Include a Duplicate?
Section titled β06 β Why Include a Duplicate?βReal pipelines can receive duplicate alerts because of:
RETRIES
MULTIPLE COLLECTORS
FORWARDER ISSUES
API PAGINATION
MESSAGE REDELIVERYYour automation should identify duplicates before creating analyst work.
07 β Create Identity Context
Section titled β07 β Create Identity ContextβCreate:
data/identities.jsonAdd:
[ { "user": "admin01", "department": "Cloud Operations", "privileged": true, "account_type": "human", "enabled": true, "risk_level": "high" }, { "user": "user01", "department": "Application Engineering", "privileged": false, "account_type": "human", "enabled": true, "risk_level": "normal" }, { "user": "service-web", "department": "Application Platform", "privileged": false, "account_type": "service", "enabled": true, "risk_level": "normal" }]08 β Why Identity Context Matters
Section titled β08 β Why Identity Context MattersβCompare:
FAILED LOGINfor:
TRAINING-USERversus:
PRIVILEGED CLOUD ADMINISTRATORThe raw detection may be similar.
The security context is not.
09 β Create Asset Context
Section titled β09 β Create Asset ContextβCreate:
data/assets.jsonAdd:
[ { "asset": "JUMP01", "type": "jump-host", "environment": "production", "criticality": "critical", "internet_facing": false, "owner": "Cloud Operations" }, { "asset": "WEB01", "type": "web-server", "environment": "production", "criticality": "high", "internet_facing": true, "owner": "Application Engineering" }, { "asset": "legacy-public-files", "type": "cloud-storage", "environment": "production", "criticality": "high", "internet_facing": true, "owner": "Cloud Platform Team" }]10 β Asset Context Questions
Section titled β10 β Asset Context QuestionsβFor every alert ask:
IS IT PRODUCTION?
IS IT CRITICAL?
IS IT INTERNET-FACING?
WHO OWNS IT?
WHAT SERVICE DOES IT SUPPORT?11 β Create IOC Context
Section titled β11 β Create IOC ContextβCreate:
data/ioc_context.jsonAdd:
[ { "indicator": "203.0.113.25", "type": "ip", "source": "synthetic-training-feed", "confidence": 70, "classification": "review", "last_seen": "2026-08-28T18:00:00Z" }, { "indicator": "198.51.100.50", "type": "ip", "source": "synthetic-training-feed", "confidence": 40, "classification": "informational", "last_seen": "2026-08-27T10:00:00Z" }]12 β IOC Context Warning
Section titled β12 β IOC Context WarningβRemember:
IOC MATCHβ CONFIRMED COMPROMISEIOC data contributes context.
It does not automatically determine the outcome of an investigation.
13 β Create Vulnerability Context
Section titled β13 β Create Vulnerability ContextβCreate:
data/vulnerabilities.jsonAdd:
[ { "asset": "WEB01", "finding_id": "VULN-2001", "severity": "critical", "score": 9.8, "status": "open", "internet_exploitable": true }, { "asset": "JUMP01", "finding_id": "VULN-2002", "severity": "medium", "score": 5.4, "status": "open", "internet_exploitable": false }]14 β Why Vulnerability Context Matters
Section titled β14 β Why Vulnerability Context MattersβCompare:
SUSPICIOUS ACTIVITY+FULLY PATCHED LOW-RISK ASSETwith:
SUSPICIOUS ACTIVITY+INTERNET-FACING ASSET+OPEN CRITICAL VULNERABILITYThe second deserves greater attention.
15 β Create Cloud Context
Section titled β15 β Create Cloud ContextβCreate:
data/cloud_context.jsonAdd:
[ { "asset": "legacy-public-files", "provider": "aws", "scope": "production", "public": true, "encrypted": false, "security_findings": [ "storage-public-access", "storage-encryption" ] }, { "asset": "WEB01", "provider": "aws", "scope": "production", "public": true, "encrypted": true, "security_findings": [] }]16 β Create the Triage Engine
Section titled β16 β Create the Triage EngineβCreate:
src/soc_triage.pyStart with:
from pathlib import Pathfrom collections import Counter, defaultdictfrom datetime import datetime, timezoneimport csvimport jsonimport logging17 β Configure Paths
Section titled β17 β Configure PathsβAdd:
BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / "data"
REPORT_DIR = BASE_DIR / "reports"
LOG_DIR = BASE_DIR / "logs"
REPORT_DIR.mkdir( parents=True, exist_ok=True)
LOG_DIR.mkdir( parents=True, exist_ok=True)18 β Configure Logging
Section titled β18 β Configure LoggingβAdd:
logging.basicConfig( level=logging.INFO, format=( "%(asctime)s " "%(levelname)s " "%(message)s" ))19 β Create JSON Loader
Section titled β19 β Create JSON LoaderβAdd:
def load_json( filename): path = ( DATA_DIR / filename )
try: with path.open( "r", encoding="utf-8" ) as file:
return json.load( file )
except FileNotFoundError: logging.error( "Missing file: %s", path )
return []
except json.JSONDecodeError as error: logging.error( "Invalid JSON in %s: %s", path, error )
return []20 β Load Lab Data
Section titled β20 β Load Lab DataβCreate:
def load_lab_data(): return { "alerts": load_json( "alerts.json" ),
"identities": load_json( "identities.json" ),
"assets": load_json( "assets.json" ),
"iocs": load_json( "ioc_context.json" ),
"vulnerabilities": load_json( "vulnerabilities.json" ),
"cloud": load_json( "cloud_context.json" ) }21 β Define Required Alert Fields
Section titled β21 β Define Required Alert FieldsβCreate:
REQUIRED_ALERT_FIELDS = { "alert_id", "timestamp", "source", "alert_type", "severity", "description"}22 β Validate Alert
Section titled β22 β Validate AlertβCreate:
def validate_alert( alert): missing = ( REQUIRED_ALERT_FIELDS - set( alert.keys() ) )
if missing: return ( False, "Missing fields: " + ", ".join( sorted( missing ) ) )
if not str( alert.get( "alert_id", "" ) ).strip():
return ( False, "Empty alert_id" )
return ( True, None )23 β Validate Timestamp
Section titled β23 β Validate TimestampβCreate:
def parse_timestamp( value): try: return datetime.fromisoformat( value.replace( "Z", "+00:00" ) )
except ( ValueError, AttributeError ): return None24 β Add Timestamp Validation
Section titled β24 β Add Timestamp ValidationβInside alert validation:
timestamp = parse_timestamp( alert.get( "timestamp" ))
if timestamp is None: return ( False, "Invalid timestamp" )25 β Define Valid Severities
Section titled β25 β Define Valid SeveritiesβCreate:
VALID_SEVERITIES = { "critical", "high", "medium", "low", "informational"}26 β Validate Severity
Section titled β26 β Validate SeverityβAdd:
severity = str( alert.get( "severity", "" )).strip().lower()
if severity not in VALID_SEVERITIES: return ( False, "Invalid severity" )27 β Normalize Alerts
Section titled β27 β Normalize AlertsβCreate:
def normalize_alert( alert): return { "alert_id": str( alert[ "alert_id" ] ).strip(),
"timestamp": str( alert[ "timestamp" ] ).strip(),
"source": str( alert[ "source" ] ).strip().lower(),
"alert_type": str( alert[ "alert_type" ] ).strip().lower(),
"severity": str( alert[ "severity" ] ).strip().lower(),
"user": str( alert.get( "user", "" ) ).strip().lower(),
"asset": str( alert.get( "asset", "" ) ).strip().upper(),
"source_ip": str( alert.get( "source_ip", "" ) ).strip(),
"description": str( alert[ "description" ] ).strip() }28 β Process Validation
Section titled β28 β Process ValidationβCreate:
def validate_and_normalize( alerts): valid = []
invalid = []
for alert in alerts: is_valid, reason = ( validate_alert( alert ) )
if not is_valid: invalid.append({ "alert": alert,
"reason": reason })
continue
valid.append( normalize_alert( alert ) )
return valid, invalid29 β Deduplicate Alerts
Section titled β29 β Deduplicate AlertsβCreate:
def deduplicate_alerts( alerts): unique = []
duplicates = []
seen = set()
for alert in alerts: alert_id = alert[ "alert_id" ]
if alert_id in seen: duplicates.append( alert )
continue
seen.add( alert_id )
unique.append( alert )
return unique, duplicates30 β Why Deduplication Comes Early
Section titled β30 β Why Deduplication Comes EarlyβWithout deduplication:
ONE SECURITY EVENT βFIVE DUPLICATE ALERTS βFIVE ANALYST CASESThis creates unnecessary workload.
31 β Build Identity Index
Section titled β31 β Build Identity IndexβCreate:
def build_identity_index( identities): return { str( item[ "user" ] ).strip().lower(): item
for item in identities }32 β Build Asset Index
Section titled β32 β Build Asset IndexβCreate:
def build_asset_index( assets): return { str( item[ "asset" ] ).strip().upper(): item
for item in assets }33 β Build IOC Index
Section titled β33 β Build IOC IndexβCreate:
def build_ioc_index( iocs): return { str( item[ "indicator" ] ).strip(): item
for item in iocs }34 β Build Vulnerability Index
Section titled β34 β Build Vulnerability IndexβOne asset may have multiple vulnerabilities.
Create:
def build_vulnerability_index( vulnerabilities): index = defaultdict( list )
for item in vulnerabilities: asset = str( item[ "asset" ] ).strip().upper()
index[ asset ].append( item )
return index35 β Build Cloud Index
Section titled β35 β Build Cloud IndexβCreate:
def build_cloud_index( cloud_records): return { str( item[ "asset" ] ).strip().upper(): item
for item in cloud_records }36 β Build Enrichment Indexes
Section titled β36 β Build Enrichment IndexesβCreate:
def build_indexes( data): return { "identity": build_identity_index( data[ "identities" ] ),
"asset": build_asset_index( data[ "assets" ] ),
"ioc": build_ioc_index( data[ "iocs" ] ),
"vulnerability": build_vulnerability_index( data[ "vulnerabilities" ] ),
"cloud": build_cloud_index( data[ "cloud" ] ) }37 β Enrich Identity Context
Section titled β37 β Enrich Identity ContextβCreate:
def enrich_identity( alert, identity_index): user = alert[ "user" ]
if not user: return { "identity_found": False }
identity = ( identity_index.get( user ) )
if not identity: return { "identity_found": False }
return { "identity_found": True,
"department": identity.get( "department" ),
"privileged": identity.get( "privileged", False ),
"account_type": identity.get( "account_type" ),
"identity_enabled": identity.get( "enabled" ),
"identity_risk": identity.get( "risk_level" ) }38 β Enrich Asset Context
Section titled β38 β Enrich Asset ContextβCreate:
def enrich_asset( alert, asset_index): asset = alert[ "asset" ]
if not asset: return { "asset_found": False }
record = ( asset_index.get( asset ) )
if not record: return { "asset_found": False }
return { "asset_found": True,
"asset_type": record.get( "type" ),
"environment": record.get( "environment" ),
"asset_criticality": record.get( "criticality" ),
"internet_facing": record.get( "internet_facing" ),
"asset_owner": record.get( "owner" ) }39 β Enrich IOC Context
Section titled β39 β Enrich IOC ContextβCreate:
def enrich_ioc( alert, ioc_index): source_ip = alert[ "source_ip" ]
if not source_ip: return { "ioc_found": False }
record = ( ioc_index.get( source_ip ) )
if not record: return { "ioc_found": False }
return { "ioc_found": True,
"ioc_source": record.get( "source" ),
"ioc_confidence": record.get( "confidence" ),
"ioc_classification": record.get( "classification" ),
"ioc_last_seen": record.get( "last_seen" ) }40 β Enrich Vulnerability Context
Section titled β40 β Enrich Vulnerability ContextβCreate:
def enrich_vulnerabilities( alert, vulnerability_index): asset = alert[ "asset" ]
records = ( vulnerability_index.get( asset, [] ) )
open_records = [ item for item in records if item.get( "status" ) == "open" ]
critical = [ item for item in open_records if item.get( "severity" ) == "critical" ]
exploitable = [ item for item in open_records if item.get( "internet_exploitable" ) ]
return { "open_vulnerability_count": len( open_records ),
"critical_vulnerability_count": len( critical ),
"internet_exploitable_vulnerability_count": len( exploitable ) }41 β Enrich Cloud Context
Section titled β41 β Enrich Cloud ContextβCreate:
def enrich_cloud( alert, cloud_index): asset = alert[ "asset" ]
record = ( cloud_index.get( asset ) )
if not record: return { "cloud_asset": False }
return { "cloud_asset": True,
"cloud_provider": record.get( "provider" ),
"cloud_scope": record.get( "scope" ),
"cloud_public": record.get( "public" ),
"cloud_encrypted": record.get( "encrypted" ),
"cloud_security_findings": record.get( "security_findings", [] ) }42 β Combine Enrichment
Section titled β42 β Combine EnrichmentβCreate:
def enrich_alert( alert, indexes): enriched = ( alert.copy() )
enriched.update( enrich_identity( alert, indexes[ "identity" ] ) )
enriched.update( enrich_asset( alert, indexes[ "asset" ] ) )
enriched.update( enrich_ioc( alert, indexes[ "ioc" ] ) )
enriched.update( enrich_vulnerabilities( alert, indexes[ "vulnerability" ] ) )
enriched.update( enrich_cloud( alert, indexes[ "cloud" ] ) )
return enriched43 β Test Enrichment
Section titled β43 β Test EnrichmentβFor:
ALT-1002you should obtain context similar to:
USER:admin01
PRIVILEGED:true
ASSET:JUMP01
CRITICALITY:critical
IOC:203.0.113.25
IOC CONFIDENCE:7044 β Missing Context Is Important
Section titled β44 β Missing Context Is ImportantβIf:
identity_found = falsedo not automatically conclude:
USER IS SAFEIt may mean:
IDENTITY INVENTORY GAP
UNKNOWN ACCOUNT
STALE CMDB
NORMALIZATION PROBLEM45 β Build Risk Scoring
Section titled β45 β Build Risk ScoringβRisk scoring should be:
EXPLAINABLE
REPEATABLE
TUNABLEnot mysterious.
46 β Base Severity Scores
Section titled β46 β Base Severity ScoresβCreate:
BASE_SEVERITY_SCORE = { "critical": 50, "high": 40, "medium": 25, "low": 10, "informational": 0}47 β Context Scores
Section titled β47 β Context ScoresβFor this lab:
PRIVILEGED IDENTITY+15
CRITICAL ASSET+15
HIGH ASSET+10
INTERNET-FACING ASSET+10
IOC CONFIDENCE >= 70+10
CRITICAL OPEN VULNERABILITY+15
INTERNET-EXPLOITABLE VULNERABILITY+10
CLOUD SECURITY FINDING+10These are:
TRAINING WEIGHTSnot universal SOC scoring standards.
48 β Create Risk Score Function
Section titled β48 β Create Risk Score FunctionβAdd:
def calculate_risk_score( alert): score = ( BASE_SEVERITY_SCORE.get( alert[ "severity" ], 0 ) )
reasons = [ ( "Base alert severity: " f"{alert['severity']}" ) ]
if alert.get( "privileged" ): score += 15
reasons.append( "Privileged identity involved" )
criticality = alert.get( "asset_criticality" )
if criticality == "critical": score += 15
reasons.append( "Critical asset involved" )
elif criticality == "high": score += 10
reasons.append( "High-criticality asset involved" )
if alert.get( "internet_facing" ): score += 10
reasons.append( "Internet-facing asset" )
confidence = alert.get( "ioc_confidence" )
if ( isinstance( confidence, (int, float) ) and confidence >= 70 ): score += 10
reasons.append( "Higher-confidence IOC context" )
if alert.get( "critical_vulnerability_count", 0 ) > 0:
score += 15
reasons.append( "Critical open vulnerability present" )
if alert.get( "internet_exploitable_vulnerability_count", 0 ) > 0:
score += 10
reasons.append( "Internet-exploitable vulnerability present" )
if alert.get( "cloud_security_findings", [] ): score += 10
reasons.append( "Cloud security findings present" )
return ( min( score, 100 ), reasons )49 β Why Cap the Score?
Section titled β49 β Why Cap the Score?βUse:
0β100for predictable interpretation.
50 β Risk Bands
Section titled β50 β Risk BandsβCreate:
def risk_band( score): if score >= 80: return "critical"
if score >= 60: return "high"
if score >= 40: return "medium"
return "low"51 β Risk Score vs Alert Severity
Section titled β51 β Risk Score vs Alert SeverityβDo not confuse:
VENDOR ALERT SEVERITYwith:
SOC TRIAGE PRIORITYExample:
MEDIUM ALERT+PRIVILEGED ADMIN+CRITICAL ASSET+HIGH-CONFIDENCE IOC=HIGH TRIAGE PRIORITY52 β Add Risk to Alerts
Section titled β52 β Add Risk to AlertsβCreate:
def score_alert( alert): enriched = ( alert.copy() )
score, reasons = ( calculate_risk_score( alert ) )
enriched[ "risk_score" ] = score
enriched[ "risk_band" ] = risk_band( score )
enriched[ "risk_reasons" ] = reasons
return enriched53 β Build Triage Recommendations
Section titled β53 β Build Triage RecommendationsβAutomation should recommend:
REVIEW PRIORITYrather than declare:
COMPROMISED54 β Recommendation Function
Section titled β54 β Recommendation FunctionβCreate:
def triage_recommendation( alert): score = alert[ "risk_score" ]
if score >= 80: return ( "Immediate analyst review. " "Validate the alert, correlate related " "activity, confirm asset and identity context, " "and follow the approved incident-response " "process if escalation criteria are met." )
if score >= 60: return ( "High-priority analyst review. " "Validate detection evidence and investigate " "related identity, endpoint, network, and " "vulnerability context." )
if score >= 40: return ( "Standard analyst review. " "Validate context and determine whether " "additional investigation is required." )
return ( "Low-priority review. " "Retain for normal SOC triage and correlation." )55 β Add Recommendation
Section titled β55 β Add RecommendationβCreate:
def add_recommendation( alert): result = alert.copy()
result[ "triage_recommendation" ] = ( triage_recommendation( alert ) )
return result56 β Correlation
Section titled β56 β CorrelationβA single alert provides:
ONE SIGNALCorrelation asks:
WHAT ELSE HAPPENEDAROUND THE SAME USER,ASSET,SOURCE,OR TIME?57 β Correlation Keys
Section titled β57 β Correlation KeysβUseful keys include:
USER
ASSET
SOURCE IP
ALERT TYPE
TIME WINDOW58 β Count Alerts by User
Section titled β58 β Count Alerts by UserβCreate:
def alerts_by_user( alerts): counts = Counter()
for alert in alerts: user = alert.get( "user" )
if user: counts[ user ] += 1
return counts59 β Count Alerts by Asset
Section titled β59 β Count Alerts by AssetβCreate:
def alerts_by_asset( alerts): counts = Counter()
for alert in alerts: asset = alert.get( "asset" )
if asset: counts[ asset ] += 1
return counts60 β Count Alerts by Source IP
Section titled β60 β Count Alerts by Source IPβCreate:
def alerts_by_source_ip( alerts): counts = Counter()
for alert in alerts: source_ip = alert.get( "source_ip" )
if source_ip: counts[ source_ip ] += 1
return counts61 β Add Correlation Counts
Section titled β61 β Add Correlation CountsβCreate:
def add_correlation_context( alerts): user_counts = ( alerts_by_user( alerts ) )
asset_counts = ( alerts_by_asset( alerts ) )
ip_counts = ( alerts_by_source_ip( alerts ) )
results = []
for alert in alerts: item = alert.copy()
item[ "related_user_alerts" ] = user_counts.get( alert.get( "user" ), 0 )
item[ "related_asset_alerts" ] = asset_counts.get( alert.get( "asset" ), 0 )
item[ "related_source_ip_alerts" ] = ip_counts.get( alert.get( "source_ip" ), 0 )
results.append( item )
return results62 β Review admin01
Section titled β62 β Review admin01βYou should see:
ALT-1001MULTIPLE FAILED LOGINS
ALT-1002SUCCESS AFTER FAILURESwith the same:
USER
ASSET
SOURCE IPThis is stronger context than reviewing each alert completely independently.
63 β Review WEB01
Section titled β63 β Review WEB01βYou should see multiple signals involving:
WEB01including:
SUSPICIOUS PROCESS
CRITICAL VULNERABILITY
UNUSUAL CONNECTION64 β Correlation Does Not Prove Causation
Section titled β64 β Correlation Does Not Prove CausationβRemember:
SAME ASSET+SIMILAR TIMEdoes not automatically prove:
SAME ATTACKIt tells the analyst:
INVESTIGATE RELATIONSHIP65 β Add Correlation Bonus
Section titled β65 β Add Correlation BonusβFor this training lab, you may optionally add:
3+ ALERTS ON SAME ASSET+10and:
2+ ALERTS FOR SAME USER+566 β Correlation Score Function
Section titled β66 β Correlation Score FunctionβCreate:
def correlation_bonus( alert): score = 0
reasons = []
if alert.get( "related_asset_alerts", 0 ) >= 3:
score += 10
reasons.append( "Multiple alerts involve the same asset" )
if alert.get( "related_user_alerts", 0 ) >= 2:
score += 5
reasons.append( "Multiple alerts involve the same user" )
return score, reasons67 β Recalculate Final Risk
Section titled β67 β Recalculate Final RiskβCreate:
def calculate_final_risk( alert): base_score, reasons = ( calculate_risk_score( alert ) )
bonus, correlation_reasons = ( correlation_bonus( alert ) )
final_score = min( base_score + bonus, 100 )
return ( final_score, reasons + correlation_reasons )68 β Processing Order Matters
Section titled β68 β Processing Order MattersβYour workflow should now be:
LOAD βVALIDATE βNORMALIZE βDEDUPLICATE βENRICH βCORRELATE βSCORE βRECOMMEND βREPORT69 β Build the Complete Processing Pipeline
Section titled β69 β Build the Complete Processing PipelineβCreate:
def process_alerts( data): valid, invalid = ( validate_and_normalize( data[ "alerts" ] ) )
unique, duplicates = ( deduplicate_alerts( valid ) )
indexes = build_indexes( data )
enriched = [ enrich_alert( alert, indexes ) for alert in unique ]
correlated = ( add_correlation_context( enriched ) )
final_alerts = []
for alert in correlated: item = alert.copy()
score, reasons = ( calculate_final_risk( item ) )
item[ "risk_score" ] = score
item[ "risk_band" ] = risk_band( score )
item[ "risk_reasons" ] = reasons
item[ "triage_recommendation" ] = ( triage_recommendation( item ) )
final_alerts.append( item )
return ( final_alerts, invalid, duplicates )70 β Sort the Analyst Queue
Section titled β70 β Sort the Analyst QueueβCreate:
def build_analyst_queue( alerts): return sorted( alerts, key=lambda item: ( item[ "risk_score" ], item[ "timestamp" ] ), reverse=True )71 β Analyst Queue Mental Model
Section titled β71 β Analyst Queue Mental ModelβInstead of:
ALERT 1
ALERT 2
ALERT 3
ALERT 4the SOC receives:
HIGHEST CONTEXTUAL RISK βNEXT HIGHEST βSTANDARD REVIEW βLOWER PRIORITY72 β Generate Alert Summary
Section titled β72 β Generate Alert SummaryβCreate:
def build_summary( alerts, invalid, duplicates): return { "raw_alerts": ( len(alerts) + len(duplicates) + len(invalid) ),
"valid_unique_alerts": len( alerts ),
"invalid_alerts": len( invalid ),
"duplicates": len( duplicates ),
"risk_bands": dict( Counter( item[ "risk_band" ] for item in alerts ) ),
"sources": dict( Counter( item[ "source" ] for item in alerts ) ) }73 β Export Analyst Queue
Section titled β73 β Export Analyst QueueβCreate:
def export_analyst_queue( alerts): output = ( REPORT_DIR / "analyst-triage-queue.csv" )
fields = [ "alert_id", "timestamp", "risk_score", "risk_band", "severity", "source", "alert_type", "user", "asset", "source_ip", "privileged", "asset_criticality", "internet_facing", "ioc_confidence", "critical_vulnerability_count", "related_user_alerts", "related_asset_alerts", "triage_recommendation" ]
with output.open( "w", encoding="utf-8", newline="" ) as file:
writer = csv.DictWriter( file, fieldnames=fields, extrasaction="ignore" )
writer.writeheader()
writer.writerows( build_analyst_queue( alerts ) )74 β Export Full JSON
Section titled β74 β Export Full JSONβCreate:
def export_json( alerts, summary, invalid, duplicates): output = ( REPORT_DIR / "soc-triage-results.json" )
with output.open( "w", encoding="utf-8" ) as file:
json.dump( { "summary": summary,
"alerts": alerts,
"invalid_alerts": invalid,
"duplicates": duplicates }, file, indent=2 )75 β Export Invalid Alerts
Section titled β75 β Export Invalid AlertsβCreate:
def export_invalid_alerts( invalid): output = ( REPORT_DIR / "invalid-alerts.json" )
output.write_text( json.dumps( invalid, indent=2 ), encoding="utf-8" )76 β Export Duplicates
Section titled β76 β Export DuplicatesβCreate:
def export_duplicates( duplicates): output = ( REPORT_DIR / "duplicate-alerts.json" )
output.write_text( json.dumps( duplicates, indent=2 ), encoding="utf-8" )77 β Generate Markdown Investigation Report
Section titled β77 β Generate Markdown Investigation ReportβCreate:
def generate_markdown_report( alerts, summary): queue = build_analyst_queue( alerts )
lines = []
lines.append( "# SOC Alert Triage Report" )
lines.append("")
lines.append( "## Executive Summary" )
lines.append("")
lines.append( f"- Valid unique alerts: " f"{summary['valid_unique_alerts']}" )
lines.append( f"- Invalid alerts: " f"{summary['invalid_alerts']}" )
lines.append( f"- Duplicate alerts: " f"{summary['duplicates']}" )
lines.append("")
lines.append( "## Prioritized Analyst Queue" )
lines.append("")
for alert in queue: lines.append( f"### {alert['risk_band'].upper()} β " f"{alert['alert_id']} β " f"{alert['alert_type']}" )
lines.append("")
lines.append( f"- Risk score: " f"{alert['risk_score']}" )
lines.append( f"- Original severity: " f"{alert['severity']}" )
lines.append( f"- User: " f"{alert['user'] or 'N/A'}" )
lines.append( f"- Asset: " f"{alert['asset'] or 'N/A'}" )
lines.append( f"- Source IP: " f"{alert['source_ip'] or 'N/A'}" )
lines.append( f"- Recommendation: " f"{alert['triage_recommendation']}" )
lines.append("")
lines.append( "**Risk Factors**" )
lines.append("")
for reason in alert[ "risk_reasons" ]: lines.append( f"- {reason}" )
lines.append("")
lines.append( "## Analyst Guidance" )
lines.append("")
lines.append( "Risk scores and recommendations are triage aids. " "They do not establish compromise. Analysts should " "validate source evidence, identity and asset context, " "related activity, known business behavior, and approved " "incident criteria before escalating or taking response actions." )
output = ( REPORT_DIR / "soc-triage-report.md" )
output.write_text( "\n".join( lines ), encoding="utf-8" )78 β Build main()
Section titled β78 β Build main()βCreate:
def main(): logging.info( "SOC triage automation started" )
data = load_lab_data()
alerts, invalid, duplicates = ( process_alerts( data ) )
summary = build_summary( alerts, invalid, duplicates )
export_analyst_queue( alerts )
export_json( alerts, summary, invalid, duplicates )
export_invalid_alerts( invalid )
export_duplicates( duplicates )
generate_markdown_report( alerts, summary )
logging.info( "SOC triage automation completed" )
print( f"Reports saved to: " f"{REPORT_DIR}" )79 β Add Entry Point
Section titled β79 β Add Entry PointβAdd:
if __name__ == "__main__": main()80 β Run the Lab
Section titled β80 β Run the LabβRun:
python src/soc_triage.pyExpected:
SOC triage automation started
SOC triage automation completed
Reports saved to: ...81 β Expected Reports
Section titled β81 β Expected ReportsβYou should now have:
reports/|+-- analyst-triage-queue.csv|+-- soc-triage-results.json|+-- invalid-alerts.json|+-- duplicate-alerts.json|+-- soc-triage-report.md82 β Review Highest-Priority Alerts
Section titled β82 β Review Highest-Priority AlertsβPay special attention to:
ALT-1002because it combines:
HIGH ALERT SEVERITY
PRIVILEGED USER
CRITICAL ASSET
IOC CONTEXT
RELATED AUTHENTICATION ALERTAlso review alerts involving:
WEB01because it combines:
INTERNET-FACING ASSET
HIGH CRITICALITY
CRITICAL OPEN VULNERABILITY
MULTIPLE RELATED ALERTS83 β Do Not Hard-Code Expected Conclusions
Section titled β83 β Do Not Hard-Code Expected ConclusionsβThe lab should not automatically say:
ADMIN01 IS COMPROMISEDor:
WEB01 HAS BEEN BREACHEDInstead:
HIGH-PRIORITY INVESTIGATIONRECOMMENDED84 β Add Investigation Status
Section titled β84 β Add Investigation StatusβFuture analyst workflow could use:
NEW
IN_REVIEW
ESCALATED
CLOSED_BENIGN
CLOSED_EXPECTED
INCIDENT_CREATED85 β Keep Automation and Analyst Decisions Separate
Section titled β85 β Keep Automation and Analyst Decisions SeparateβAutomation-generated:
risk_score
risk_band
recommendationAnalyst-generated:
disposition
investigation_notes
incident_id
closure_reasonDo not mix these concepts.
86 β Alert Disposition
Section titled β86 β Alert DispositionβAn analyst may eventually determine:
TRUE POSITIVE
FALSE POSITIVE
BENIGN TRUE POSITIVE
EXPECTED ACTIVITY
INSUFFICIENT EVIDENCE87 β Preserve Analyst Feedback
Section titled β87 β Preserve Analyst FeedbackβAnalyst decisions can later improve:
DETECTION RULES
THRESHOLDS
ENRICHMENT
RISK SCORING
SUPPRESSION LOGIC88 β Feedback Loop
Section titled β88 β Feedback LoopβALERT βTRIAGE βANALYST DECISION βFEEDBACK βDETECTION IMPROVEMENT βBETTER ALERT89 β Time-Window Correlation
Section titled β89 β Time-Window CorrelationβYour current correlation uses total counts.
A stronger implementation should correlate within:
5 MINUTES
15 MINUTES
30 MINUTES
1 HOURdepending on the use case.
90 β Why Time Windows Matter
Section titled β90 β Why Time Windows MatterβThese alerts:
FAILED LOGIN β JANUARY
SUCCESSFUL LOGIN β AUGUSTshould not normally be treated like:
FAILED LOGIN β 08:15
SUCCESSFUL LOGIN β 08:1891 β Create Time Difference Function
Section titled β91 β Create Time Difference FunctionβCreate:
def minutes_between( first, second): first_time = parse_timestamp( first )
second_time = parse_timestamp( second )
if ( first_time is None or second_time is None ): return None
difference = abs( second_time - first_time )
return ( difference.total_seconds() / 60 )92 β Authentication Sequence Correlation
Section titled β92 β Authentication Sequence CorrelationβSearch for:
multiple_failed_loginsfollowed by:
successful_login_after_failuresfor the same:
USER
SOURCE IPwithin a controlled time window.
93 β Create Authentication Sequence Detector
Section titled β93 β Create Authentication Sequence Detectorβdef find_auth_sequences( alerts, max_minutes=15): sequences = []
failures = [ item for item in alerts if item[ "alert_type" ] == "multiple_failed_logins" ]
successes = [ item for item in alerts if item[ "alert_type" ] == "successful_login_after_failures" ]
for failure in failures: for success in successes:
same_user = ( failure.get( "user" ) == success.get( "user" ) )
same_ip = ( failure.get( "source_ip" ) == success.get( "source_ip" ) )
difference = ( minutes_between( failure[ "timestamp" ], success[ "timestamp" ] ) )
if ( same_user and same_ip and difference is not None and difference <= max_minutes ): sequences.append({ "failure_alert": failure[ "alert_id" ],
"success_alert": success[ "alert_id" ],
"user": success[ "user" ],
"source_ip": success[ "source_ip" ],
"minutes": round( difference, 2 ) })
return sequences94 β Expected Sequence
Section titled β94 β Expected SequenceβYour synthetic data should identify:
ALT-1001 βALT-1002for:
admin01
203.0.113.25within approximately:
3 MINUTES95 β Sequence Detection Is Still Not a Conclusion
Section titled β95 β Sequence Detection Is Still Not a ConclusionβA legitimate user could:
TYPE THE WRONG PASSWORD
RETRY
THEN SUCCESSFULLY LOG INTherefore the sequence means:
REVIEWnot:
ACCOUNT TAKEOVER CONFIRMED96 β Build Investigation Groups
Section titled β96 β Build Investigation GroupsβA SOC often groups related alerts into:
CASES
STORIES
INCIDENT CANDIDATESFor the lab, group by:
USER
ASSET
SOURCE IP97 β Group by Asset
Section titled β97 β Group by AssetβCreate:
def group_by_asset( alerts): groups = defaultdict( list )
for alert in alerts: asset = alert.get( "asset" )
if asset: groups[ asset ].append( alert[ "alert_id" ] )
return dict( groups )98 β Expected WEB01 Group
Section titled β98 β Expected WEB01 GroupβYou should see:
WEB01|+-- ALT-1003|+-- ALT-1005|+-- ALT-100699 β Investigation Story
Section titled β99 β Investigation StoryβThe analyst can now see:
WEB01 βSUSPICIOUS PROCESS ALERT βCRITICAL VULNERABILITY βUNUSUAL NETWORK CONNECTIONThe automation should say:
RELATED ACTIVITY REQUIRES REVIEWnot automatically construct an attack conclusion.
100 β Add Data Freshness
Section titled β100 β Add Data FreshnessβContext can become stale.
Examples:
ASSET CRITICALITY
USER ROLE
IOC REPUTATION
VULNERABILITY STATUSshould have timestamps where possible.
101 β IOC Freshness
Section titled β101 β IOC FreshnessβAn IOC observed:
2 YEARS AGOshould not necessarily carry the same weight as one observed:
TODAY102 β Asset Inventory Freshness
Section titled β102 β Asset Inventory FreshnessβIf the CMDB says:
WEB01=PRODUCTIONbut was last updated:
18 MONTHS AGOthe analyst should know.
103 β Enrichment Confidence
Section titled β103 β Enrichment ConfidenceβFuture enrichment records can include:
source
confidence
last_updated
collection_status104 β Unknown Context
Section titled β104 β Unknown ContextβA mature pipeline should distinguish:
FALSEfrom:
UNKNOWNExample:
internet_facing = falseis different from:
internet_facing = unknown105 β Data Quality Score
Section titled β105 β Data Quality ScoreβFuture alerts could include:
context_completenessExample:
IDENTITY FOUNDASSET FOUNDIOC CHECK COMPLETEDVULNERABILITY DATA AVAILABLE106 β Context Completeness
Section titled β106 β Context CompletenessβConceptually:
AVAILABLE CONTEXT/EXPECTED CONTEXTThis helps analysts identify:
LOW-CONFIDENCE TRIAGE107 β Create Context Completeness
Section titled β107 β Create Context CompletenessβExample:
def context_completeness( alert): checks = [ alert.get( "identity_found" ) if alert.get( "user" ) else True,
alert.get( "asset_found" ) if alert.get( "asset" ) else True,
( "ioc_found" in alert ),
( "open_vulnerability_count" in alert ) ]
completed = sum( bool(item) for item in checks )
return round( ( completed / len(checks) ) * 100, 2 )108 β Why Context Completeness Matters
Section titled β108 β Why Context Completeness MattersβAn alert with:
RISK SCORE = 85but:
CONTEXT COMPLETENESS = 25%should be interpreted carefully.
109 β Detection Confidence
Section titled β109 β Detection ConfidenceβFuture alert records may contain:
detection_confidenceDo not confuse:
DETECTION CONFIDENCEwith:
INCIDENT CONFIDENCE110 β Build Alert Fingerprint
Section titled β110 β Build Alert FingerprintβAlert IDs may change across systems.
A fingerprint can combine:
SOURCE
ALERT TYPE
USER
ASSET
SOURCE IP
TIME BUCKET111 β Why Fingerprints Matter
Section titled β111 β Why Fingerprints MatterβThey can help with:
DEDUPLICATION
CORRELATION
SUPPRESSION
TRENDING112 β Suppression
Section titled β112 β SuppressionβSome alerts may be:
KNOWN
EXPECTED
APPROVEDExample:
AUTHORIZED VULNERABILITY SCANNERA suppression should require:
OWNER
JUSTIFICATION
EXPIRATION
SCOPE113 β Suppression Warning
Section titled β113 β Suppression WarningβDo not create:
IGNORE THIS ALERT FOREVERbecause an exception can become stale.
114 β Suppression Workflow
Section titled β114 β Suppression WorkflowβNOISY ALERT βANALYST REVIEW βEXPECTED? βDOCUMENT βAPPROVE βTIME-LIMITED SUPPRESSION βREVIEW115 β SLA Mapping
Section titled β115 β SLA MappingβOrganizations may define:
CRITICALβ IMMEDIATE REVIEW
HIGHβ RAPID REVIEW
MEDIUMβ STANDARD QUEUE
LOWβ ROUTINE REVIEWUse organizational policy rather than inventing universal response times.
116 β Queue Ownership
Section titled β116 β Queue OwnershipβFuture alerts should include:
assigned_teamExamples:
SOC
CLOUD SECURITY
IDENTITY TEAM
ENDPOINT TEAM
APPLICATION SECURITY117 β Routing Function
Section titled β117 β Routing FunctionβExample:
def route_alert( alert): source = alert[ "source" ]
if source == "cloud-security": return "Cloud Security"
if source == "identity-monitor": return "SOC / Identity"
if source == "endpoint-monitor": return "SOC / Endpoint"
if source == "vulnerability-monitor": return "Vulnerability Management"
return "SOC"118 β Add Routing
Section titled β118 β Add RoutingβAdd:
item[ "assigned_team"] = route_alert( item)to the final processing pipeline.
119 β Routing Does Not Replace Escalation
Section titled β119 β Routing Does Not Replace EscalationβA cloud alert assigned to:
CLOUD SECURITYmay still require:
SOC
INCIDENT RESPONSE
IDENTITY
LEGAL
PRIVACYdepending on the investigation.
120 β Create Case Candidate
Section titled β120 β Create Case CandidateβFor high-context alerts, automation can create a:
CASE CANDIDATErather than automatically declaring an incident.
Example:
{ "case_candidate": true, "reason": "Multiple related high-risk alerts"}121 β Case Candidate Rule
Section titled β121 β Case Candidate RuleβFor the training lab:
def case_candidate( alert): return ( alert[ "risk_score" ] >= 80 and ( alert.get( "related_user_alerts", 0 ) >= 2 or alert.get( "related_asset_alerts", 0 ) >= 2 ) )122 β Why Candidate?
Section titled β122 β Why Candidate?βBecause:
AUTOMATIONcan identify:
PATTERN WORTH ESCALATINGbut:
ANALYSTshould validate whether incident criteria are actually met.
123 β Evidence Preservation
Section titled β123 β Evidence PreservationβFor high-priority alerts, preserve:
RAW ALERT
NORMALIZED ALERT
ENRICHMENT DATA
CORRELATION RESULTS
RISK REASONS
TIMESTAMP124 β Never Modify Raw Evidence
Section titled β124 β Never Modify Raw EvidenceβKeep:
RAW DATAseparate from:
NORMALIZED DATA125 β Evidence Architecture
Section titled β125 β Evidence ArchitectureβRAW ALERT βPRESERVE βCOPY βNORMALIZE βENRICH βANALYZE126 β Export Raw Alert Snapshot
Section titled β126 β Export Raw Alert SnapshotβCreate:
def export_raw_alerts( alerts): output = ( REPORT_DIR / "raw-alert-snapshot.json" )
output.write_text( json.dumps( alerts, indent=2 ), encoding="utf-8" )Call it before transformation.
127 β Hash Raw Evidence
Section titled β127 β Hash Raw EvidenceβImport:
import hashlibCreate:
def sha256_file( path): digest = hashlib.sha256()
with path.open( "rb" ) as file:
for block in iter( lambda: file.read( 8192 ), b"" ): digest.update( block )
return digest.hexdigest()128 β Evidence Hash Purpose
Section titled β128 β Evidence Hash PurposeβThe hash helps answer:
HAS THIS FILE CHANGEDSINCE COLLECTION?It does not by itself establish the full chain of custody.
129 β Create Investigation Package
Section titled β129 β Create Investigation PackageβFuture high-priority case folder:
cases/|+-- CASE-001/ | +-- raw-alert.json | +-- normalized-alert.json | +-- identity-context.json | +-- asset-context.json | +-- vulnerability-context.json | +-- analyst-notes.md130 β Integrate Security APIs
Section titled β130 β Integrate Security APIsβIn a real authorized environment, enrichment could use:
SIEM API
EDR API
CMDB API
IDENTITY API
THREAT INTELLIGENCE API
VULNERABILITY API
CLOUD SECURITY APIUse the lessons from:
Lab 07 β Security API Integration131 β API Failure Handling
Section titled β131 β API Failure HandlingβIf threat-intelligence enrichment fails:
DO NOT DROP THE ALERTInstead:
ALERT βIOC ENRICHMENT FAILED βMARK CONTEXT UNAVAILABLE βCONTINUE TRIAGE132 β Graceful Degradation
Section titled β132 β Graceful DegradationβA resilient pipeline should continue when one enrichment source is unavailable.
IDENTITY API β
ASSET API β
IOC API β
VULNERABILITY API βResult:
PARTIAL ENRICHMENTnot:
TOTAL PIPELINE FAILURE133 β Retry Carefully
Section titled β133 β Retry CarefullyβRetry:
TEMPORARY NETWORK ERROR
429
5XXDo not repeatedly retry:
401
403
INVALID REQUEST134 β Secret Management
Section titled β134 β Secret ManagementβAPI tokens must not appear in:
SOURCE CODE
REPORTS
LOGS
GIT
ERROR OUTPUT135 β SOC Automation Logging
Section titled β135 β SOC Automation LoggingβUseful operational logs include:
ALERTS RECEIVED
ALERTS VALIDATED
INVALID ALERTS
DUPLICATES REMOVED
ENRICHMENT FAILURES
ALERTS SCORED
REPORTS GENERATED
PIPELINE DURATION136 β Metrics
Section titled β136 β MetricsβCreate SOC automation metrics such as:
RAW ALERT COUNT
VALID ALERT COUNT
DUPLICATE RATE
ENRICHMENT SUCCESS RATE
HIGH-PRIORITY COUNT
AVERAGE RISK SCORE
PROCESSING TIME137 β Alert Reduction Metric
Section titled β137 β Alert Reduction MetricβExample:
RAW ALERTS1000
DUPLICATES200
VALID UNIQUE800Deduplication reduced analyst workload by:
20%138 β Be Careful With Automation Metrics
Section titled β138 β Be Careful With Automation MetricsβA lower alert count is not automatically:
BETTER SECURITYYou could reduce alerts by:
DISABLING DETECTIONSwhich would be harmful.
Measure:
QUALITY
COVERAGE
PRECISION
RESPONSE VALUEnot only volume.
139 β Analyst Feedback Dataset
Section titled β139 β Analyst Feedback DatasetβFuture data:
{ "alert_id": "ALT-1002", "analyst_disposition": "true_positive", "escalated": true, "notes": "Synthetic training result"}140 β Detection Tuning
Section titled β140 β Detection TuningβAnalyze analyst feedback to identify:
HIGH FALSE-POSITIVE RULES
LOW-VALUE ALERTS
MISSING CONTEXT
BAD THRESHOLDS
USEFUL CORRELATIONS141 β Never Auto-Tune Blindly
Section titled β141 β Never Auto-Tune BlindlyβDo not automatically disable a detection because:
MANY ALERTS WERE CLOSEDInvestigate:
WHY?142 β Build Test File
Section titled β142 β Build Test FileβCreate:
tests/test_soc_triage.py143 β Test Alert Validation
Section titled β143 β Test Alert Validationβdef test_valid_alert(): alert = { "alert_id": "ALT-1", "timestamp": "2026-08-29T08:00:00Z", "source": "training", "alert_type": "test", "severity": "medium", "description": "Training alert" }
valid, reason = ( validate_alert( alert ) )
assert valid is True assert reason is None144 β Test Missing Alert ID
Section titled β144 β Test Missing Alert IDβdef test_missing_alert_id(): alert = { "timestamp": "2026-08-29T08:00:00Z", "source": "training", "alert_type": "test", "severity": "medium", "description": "Training alert" }
valid, reason = ( validate_alert( alert ) )
assert valid is False145 β Test Invalid Severity
Section titled β145 β Test Invalid SeverityβUse:
severity = emergency-super-criticalExpected:
INVALID146 β Test Invalid Timestamp
Section titled β146 β Test Invalid TimestampβUse:
not-a-dateExpected:
INVALID147 β Test Deduplication
Section titled β147 β Test DeduplicationβInput:
ALT-1001
ALT-1001Expected:
UNIQUE = 1
DUPLICATE = 1148 β Test Identity Enrichment
Section titled β148 β Test Identity EnrichmentβInput:
user = admin01Expected:
privileged = true149 β Test Asset Enrichment
Section titled β149 β Test Asset EnrichmentβInput:
asset = WEB01Expected:
criticality = high
internet_facing = true150 β Test IOC Enrichment
Section titled β150 β Test IOC EnrichmentβInput:
203.0.113.25Expected:
ioc_found = true
confidence = 70151 β Test Unknown IOC
Section titled β151 β Test Unknown IOCβInput:
192.0.2.200Expected:
ioc_found = falseDo not mark it:
SAFEjust because your dataset has no record.
152 β Test Vulnerability Context
Section titled β152 β Test Vulnerability ContextβInput:
WEB01Expected:
critical_vulnerability_count >= 1153 β Test Risk Scoring
Section titled β153 β Test Risk ScoringβBuild a synthetic alert with:
HIGH SEVERITY
PRIVILEGED USER
CRITICAL ASSETConfirm the score increases for each documented reason.
154 β Test Risk Cap
Section titled β154 β Test Risk CapβCreate enough contextual factors to exceed:
100Expected:
risk_score = 100155 β Test Correlation
Section titled β155 β Test CorrelationβInput multiple alerts using:
WEB01Confirm:
related_asset_alertsis correct.
156 β Test Authentication Sequence
Section titled β156 β Test Authentication SequenceβConfirm:
ALT-1001and:
ALT-1002are correlated within:
15 MINUTES157 β Test Graceful Missing Identity
Section titled β157 β Test Graceful Missing IdentityβUse:
user = unknown-userExpected:
identity_found = falsewith no crash.
158 β Test Missing Asset
Section titled β158 β Test Missing AssetβUse:
asset = UNKNOWN01Expected:
asset_found = false159 β Test Empty IOC Data
Section titled β159 β Test Empty IOC DataβReplace:
ioc_context.jsontemporarily with:
[]The triage engine should still process alerts.
160 β Test Missing Vulnerability Data
Section titled β160 β Test Missing Vulnerability DataβThe pipeline should continue with:
open_vulnerability_count = 0while ideally recording that collection status separately in a more mature implementation.
161 β Challenge 01 β Add SQLite
Section titled β161 β Challenge 01 β Add SQLiteβStore normalized alerts in:
soc.dbTables:
alerts
identities
assets
vulnerabilities
triage_results162 β SQL Investigation
Section titled β162 β SQL InvestigationβUse skills from:
Lab 05 β SQL Security Analyticsto query:
SELECT asset, COUNT(*) AS alert_countFROM alertsGROUP BY assetORDER BY alert_count DESC;163 β Challenge 02 β Add API Enrichment
Section titled β163 β Challenge 02 β Add API EnrichmentβReplace one static enrichment file with a:
LOCAL TRAINING APIfrom Lab 07.
Architecture:
ALERT βPYTHON βTRAINING API βJSON βENRICHMENT164 β Challenge 03 β Add Cloud Findings
Section titled β164 β Challenge 03 β Add Cloud FindingsβFeed the output of:
Lab 08directly into the SOC triage pipeline.
CLOUD AUDITOR βCLOUD FINDINGS βSOC TRIAGE165 β Challenge 04 β Add Vulnerability Prioritization
Section titled β165 β Challenge 04 β Add Vulnerability PrioritizationβUse output from:
Lab 06to enrich affected assets.
166 β Challenge 05 β Add IOC Pipeline
Section titled β166 β Challenge 05 β Add IOC PipelineβUse normalized IOC output from:
Lab 02rather than the simplified IOC context file.
167 β Challenge 06 β Add Log Analyzer Findings
Section titled β167 β Challenge 06 β Add Log Analyzer FindingsβUse:
Lab 01to create authentication alerts that feed this lab.
Architecture:
AUTH LOG βLOG ANALYZER βALERT βSOC TRIAGE168 β Challenge 07 β Build Dashboard Dataset
Section titled β168 β Challenge 07 β Build Dashboard DatasetβExport:
dashboard.jsonwith:
TOTAL ALERTS
CRITICAL QUEUE
HIGH QUEUE
TOP USERS
TOP ASSETS
TOP ALERT TYPES
TOP SOURCES169 β Challenge 08 β Add JavaScript Dashboard
Section titled β169 β Challenge 08 β Add JavaScript DashboardβUse your:
JavaScript Fundamentalsskills to create a simple local dashboard that reads sanitized lab data.
Do not expose:
TOKENS
SECRETS
SENSITIVE RAW EVIDENCEin browser-side code.
170 β Challenge 09 β Add MITRE ATT&CK Mapping
Section titled β170 β Challenge 09 β Add MITRE ATT&CK MappingβAdd optional fields such as:
technique_id
tacticto synthetic alerts.
Use mappings for:
CLASSIFICATION
INVESTIGATION CONTEXT
REPORTINGnot as proof that an adversary definitely performed a technique.
171 β Challenge 10 β Add Detection Rule ID
Section titled β171 β Challenge 10 β Add Detection Rule IDβEvery alert should eventually identify:
rule_idExample:
AUTH-001
ENDPOINT-004
CLOUD-012172 β Challenge 11 β Add Rule Version
Section titled β172 β Challenge 11 β Add Rule VersionβTrack:
rule_versionThis helps answer:
WHICH VERSIONGENERATED THE ALERT?173 β Challenge 12 β Add Detection Source Evidence
Section titled β173 β Challenge 12 β Add Detection Source EvidenceβStore:
event_id
log_source
sensor
query_referencewithout copying unnecessary sensitive raw data into every report.
174 β Challenge 13 β Add Case Candidates
Section titled β174 β Challenge 13 β Add Case CandidatesβAutomatically create:
case-candidates.jsonfor alerts satisfying your controlled candidate criteria.
Do not automatically mark them:
CONFIRMED INCIDENTS175 β Challenge 14 β Add SLA Queue
Section titled β175 β Challenge 14 β Add SLA QueueβCreate:
critical
high
medium
lowqueues.
Use organizational response targets rather than arbitrary universal SLAs.
176 β Challenge 15 β Add Analyst Notes Template
Section titled β176 β Challenge 15 β Add Analyst Notes TemplateβGenerate:
analyst-investigation-template.mdcontaining:
ALERT ID
SUMMARY
IDENTITY CONTEXT
ASSET CONTEXT
IOC CONTEXT
VULNERABILITY CONTEXT
RELATED ALERTS
TIMELINE
ANALYST OBSERVATIONS
DISPOSITION
ESCALATION
FOLLOW-UP177 β Challenge 16 β Add Pipeline Health Report
Section titled β177 β Challenge 16 β Add Pipeline Health ReportβGenerate:
pipeline-health.jsoncontaining:
ALERT SOURCE STATUS
IDENTITY SOURCE STATUS
ASSET SOURCE STATUS
IOC SOURCE STATUS
VULNERABILITY SOURCE STATUS
CLOUD SOURCE STATUS178 β Challenge 17 β Add Dry Run
Section titled β178 β Challenge 17 β Add Dry RunβAny future response workflow should default to:
DRY RUNExample:
PROPOSED ACTION:
Request review of account admin01
NO CHANGE PERFORMED179 β Challenge 18 β Add Approval Gate
Section titled β179 β Challenge 18 β Add Approval GateβFuture response architecture:
HIGH-RISK ALERT βTRIAGE βPROPOSE ACTION βANALYST APPROVAL βAUTHORIZED RESPONSE βVERIFY180 β SOAR Relationship
Section titled β180 β SOAR RelationshipβWhat you are building resembles a simplified:
SOARworkflow.
SOAR commonly stands for:
SECURITY
ORCHESTRATION
AUTOMATION
RESPONSE181 β Orchestration vs Automation
Section titled β181 β Orchestration vs AutomationβAutomation:
DO ONE TASKAUTOMATICALLYOrchestration:
CONNECT MULTIPLESECURITY SYSTEMSINTO A WORKFLOWExample:
SIEM βIDENTITY βCMDB βTHREAT INTEL βVULNERABILITY PLATFORM βCASE MANAGEMENT182 β Why Human-in-the-Loop Matters
Section titled β182 β Why Human-in-the-Loop MattersβA SOC workflow may have permission to:
DISABLE USER
ISOLATE HOST
BLOCK INDICATORA false positive could therefore cause:
BUSINESS OUTAGE
USER LOCKOUT
SERVICE DISRUPTIONUse human approval for high-impact decisions unless a carefully governed automation use case has been explicitly approved.
183 β Risk Scoring Governance
Section titled β183 β Risk Scoring GovernanceβYour scoring model must be documented.
Analysts should understand:
WHY DID THIS ALERTGET SCORE 90?184 β Bad Risk Model
Section titled β184 β Bad Risk ModelβAvoid:
ALERT SCORE = 93
WHY?
UNKNOWN.185 β Good Risk Model
Section titled β185 β Good Risk ModelβPrefer:
BASE HIGH SEVERITY+40
PRIVILEGED USER+15
CRITICAL ASSET+15
HIGH-CONFIDENCE IOC+10
RELATED USER ALERTS+5
TOTAL85186 β Explainability
Section titled β186 β ExplainabilityβEvery score should include:
REASONSbecause explainability supports:
ANALYST TRUST
TUNING
AUDITING
DEBUGGING187 β Avoid Double Counting
Section titled β187 β Avoid Double CountingβBe careful when:
ALERT SEVERITYalready includes:
ASSET CRITICALITYand your risk model adds:
ASSET CRITICALITY AGAINYou may accidentally inflate risk.
188 β Risk Model Versioning
Section titled β188 β Risk Model VersioningβAdd:
risk_model_versionExample:
1.0This helps explain historical differences.
189 β Detection Quality Metrics
Section titled β189 β Detection Quality MetricsβFuture metrics:
TRUE POSITIVE RATE
FALSE POSITIVE RATE
BENIGN TRUE POSITIVE RATE
ESCALATION RATE
MEAN TIME TO TRIAGE
ENRICHMENT COVERAGE190 β Mean Time to Triage
Section titled β190 β Mean Time to TriageβConceptually:
TRIAGE TIME-ALERT CREATED TIMETrack this carefully using consistent timestamps.
191 β Queue Aging
Section titled β191 β Queue AgingβMonitor alerts that remain:
UNREVIEWEDfor too long.
ALERT βQUEUE βAGE βESCALATION POLICY192 β Auditability
Section titled β192 β AuditabilityβThe system should record:
WHEN ALERT ARRIVED
WHEN ENRICHMENT OCCURRED
WHAT DATA SOURCES WERE USED
WHAT SCORE WAS ASSIGNED
WHAT MODEL VERSION WAS USED
WHO REVIEWED IT
WHAT DECISION WAS MADE193 β Data Retention
Section titled β193 β Data RetentionβSOC data can contain:
USERNAMES
HOSTNAMES
IP ADDRESSES
SECURITY EVENTS
INVESTIGATION NOTESFollow approved:
RETENTION
ACCESS CONTROL
PRIVACY
EVIDENCErequirements.
194 β Security of the Automation Platform
Section titled β194 β Security of the Automation PlatformβThe triage platform itself becomes:
SECURITY-CRITICAL INFRASTRUCTUREProtect:
API TOKENS
DATABASE
LOGS
REPORTS
CONFIGURATION
SCORING RULES
SOURCE CODE195 β Least Privilege
Section titled β195 β Least PrivilegeβA triage engine that only:
READS ALERTSshould not automatically receive permission to:
ISOLATE HOSTS
DISABLE ACCOUNTS
DELETE RESOURCES196 β Separate Read and Response Credentials
Section titled β196 β Separate Read and Response CredentialsβPrefer:
TRIAGE SERVICE βREAD-ONLY TOKENand a separately governed:
RESPONSE WORKFLOW βAPPROVED WRITE PERMISSION197 β Fail-Safe Behavior
Section titled β197 β Fail-Safe BehaviorβIf enrichment fails:
DO NOTAUTOMATICALLY CLOSE ALERTIf scoring fails:
DO NOTAUTOMATICALLY MARK LOW RISKIf context is unavailable:
MARK UNKNOWNAND REQUEST REVIEW198 β Fail-Safe Mental Model
Section titled β198 β Fail-Safe Mental ModelβUNCERTAINTY βVISIBILITY βHUMAN REVIEWnot:
UNCERTAINTY βIGNORE199 β Common SOC Automation Mistakes
Section titled β199 β Common SOC Automation MistakesβAvoid:
TRUSTING ALERT SEVERITY BLINDLY
NO NORMALIZATION
NO DEDUPLICATION
NO ASSET CONTEXT
NO IDENTITY CONTEXT
IOC MATCH = COMPROMISE
NO VULNERABILITY CONTEXT
NO CORRELATION
NO TIME WINDOWS
NO EXPLAINABLE SCORING
NO DATA FRESHNESS
MISSING DATA = SAFE
AUTOMATICALLY CLOSING ALERTS
AUTOMATICALLY DISABLING USERS
AUTOMATICALLY ISOLATING HOSTS
LOGGING API TOKENS
NO ANALYST FEEDBACK
NO AUDIT TRAIL200 β SOC Analyst Investigation Questions
Section titled β200 β SOC Analyst Investigation QuestionsβFor every high-priority alert, ask:
- What detection generated the alert?
- What evidence supports it?
- Is the alert data complete?
- Which user is involved?
- Is the identity privileged?
- Is the account enabled?
- Is the behavior normal for this identity?
- Which asset is involved?
- Is the asset production?
- Is it business-critical?
- Is it internet-facing?
- Are critical vulnerabilities present?
- Is the source indicator known?
- How current is the IOC information?
- Are there related alerts?
- Are the alerts close together in time?
- Is there an approved exception?
- Could this be legitimate activity?
- Does it meet incident escalation criteria?
- What evidence should be preserved?
201 β Final Project Structure
Section titled β201 β Final Project StructureβYour completed project should look like:
soc-alert-triage/|+-- data/| +-- alerts.json| +-- identities.json| +-- assets.json| +-- ioc_context.json| +-- vulnerabilities.json| +-- cloud_context.json|+-- src/| +-- soc_triage.py|+-- tests/| +-- test_soc_triage.py|+-- reports/| +-- raw-alert-snapshot.json| +-- analyst-triage-queue.csv| +-- soc-triage-results.json| +-- invalid-alerts.json| +-- duplicate-alerts.json| +-- soc-triage-report.md|+-- logs/|+-- README.md|+-- architecture.md202 β README Structure
Section titled β202 β README StructureβDocument:
PROJECT OVERVIEW
SOC USE CASE
ARCHITECTURE
AUTHORIZED USE
DATA SOURCES
ALERT SCHEMA
VALIDATION
NORMALIZATION
DEDUPLICATION
IDENTITY ENRICHMENT
ASSET ENRICHMENT
IOC ENRICHMENT
VULNERABILITY ENRICHMENT
CLOUD CONTEXT
CORRELATION
RISK MODEL
TRIAGE RECOMMENDATIONS
ANALYST QUEUE
REPORTS
TESTING
SECURITY CONSIDERATIONS
LIMITATIONS203 β Document Lab Limitations
Section titled β203 β Document Lab LimitationsβThe lab uses:
SYNTHETIC ALERTS
STATIC JSON ENRICHMENT
SIMPLE CORRELATION
TRAINING RISK WEIGHTS
NO PRODUCTION SIEM
NO PRODUCTION EDR
NO REAL THREAT-INTELLIGENCE FEED
NO AUTOMATIC RESPONSE204 β Portfolio Deliverables
Section titled β204 β Portfolio DeliverablesβYour portfolio should demonstrate:
ALERT INGESTION
DATA VALIDATION
NORMALIZATION
DEDUPLICATION
MULTI-SOURCE ENRICHMENT
CORRELATION
EXPLAINABLE RISK SCORING
ANALYST QUEUE GENERATION
REPORTING
TESTING
SAFE AUTOMATION DESIGN205 β Mission Validation Checklist
Section titled β205 β Mission Validation Checklistβ- Lab workspace created
- Synthetic alerts created
- Identity dataset created
- Asset dataset created
- IOC context created
- Vulnerability context created
- Cloud context created
- JSON loader created
- Required fields validated
- Timestamp validated
- Severity validated
- Alerts normalized
- Duplicate alerts detected
- Identity index created
- Asset index created
- IOC index created
- Vulnerability index created
- Cloud index created
- Identity enrichment implemented
- Asset enrichment implemented
- IOC enrichment implemented
- Vulnerability enrichment implemented
- Cloud enrichment implemented
- Base risk scoring implemented
- Risk reasons recorded
- Risk bands implemented
- User correlation implemented
- Asset correlation implemented
- Source-IP correlation implemented
- Correlation bonus implemented
- Triage recommendations implemented
- Analyst queue generated
- Authentication sequence reviewed
- Raw alerts preserved
- Invalid alerts exported
- Duplicate alerts exported
- JSON report generated
- CSV queue generated
- Markdown report generated
- Missing context handled safely
- No automatic disruptive response created
- Test cases completed
- README created
- Limitations documented
Mission Review
Section titled βMission ReviewβYou started with:
RAW ALERTScontaining only basic information such as:
ALERT TYPE
SEVERITY
USER
ASSET
SOURCE IPYou transformed them into:
NORMALIZED ALERT βIDENTITY CONTEXT βASSET CONTEXT βIOC CONTEXT βVULNERABILITY CONTEXT βCLOUD CONTEXT βCORRELATION βEXPLAINABLE RISK SCORE βTRIAGE RECOMMENDATION βPRIORITIZED ANALYST QUEUEWhat You Built
Section titled βWhat You BuiltβYou now have the foundation of a defensive SOC triage automation platform.
It can:
INGEST
VALIDATE
NORMALIZE
DEDUPLICATE
ENRICH
CORRELATE
PRIORITIZE
ROUTE
REPORTsecurity alerts.
Key Security Lesson
Section titled βKey Security LessonβThe most important lesson is:
ALERT+CONTEXTβ AUTOMATIC CONCLUSIONInstead:
ALERT+IDENTITY+ASSET+IOC+VULNERABILITY+CLOUD+RELATED ACTIVITY+ANALYST JUDGMENT=BETTER SECURITY DECISIONFinal Mental Model
Section titled βFinal Mental ModelβWhen an alert arrives, think:
IS THE ALERT VALID? βIS IT A DUPLICATE? βWHO IS INVOLVED? βIS THE USER PRIVILEGED? βWHAT ASSET IS INVOLVED? βHOW CRITICAL IS IT? βIS IT INTERNET-FACING? βIS THERE IOC CONTEXT? βARE VULNERABILITIES PRESENT? βARE CLOUD FINDINGS PRESENT? βARE THERE RELATED ALERTS? βHOW CLOSE ARE THEY IN TIME? βWHAT FACTORS DRIVE PRIORITY? βWHAT SHOULD THE ANALYST REVIEW? βDOES IT MEET ESCALATION CRITERIA?The goal is not:
REMOVE THE ANALYSTThe goal is:
REMOVE REPETITIVE WORKSO THE ANALYSTCAN MAKE BETTER DECISIONSWhatβs Next?
Section titled βWhatβs Next?ββ‘οΈ Lab 10 β Enterprise Security Automation Capstone
The final Programming Lab brings the entire learning path together.
You will combine:
PYTHON
BASH
POWERSHELL
SQL
JSON
APIs
LINUX SECURITY DATA
WINDOWS SECURITY DATA
CLOUD SECURITY DATA
VULNERABILITY DATA
SOC ALERTS
IOC ENRICHMENTinto one enterprise security automation platform.
The capstone architecture will be:
ENTERPRISE SECURITY DATA β ββββββββββββββββΌβββββββββββββββ β β β LINUX WINDOWS CLOUD β β β ββββββββββββββββΌβββββββββββββββ β VULNERABILITIES β IOCS β SOC ALERTS β INGESTION β NORMALIZATION β DATABASE β CORRELATION β RISK SCORING β ANALYST REPORT β HUMAN REVIEWThis will be the final lab where you build a portfolio-ready enterprise security automation project from the skills developed throughout the complete Programming learning path.