Lab 02 β IOC Processing and Enrichment Pipeline
Mission Information
Section titled βMission InformationβDifficulty: Beginner β Intermediate
Estimated Time: 90β120 minutes
Primary Language: Python
Security Domain: SOC / Threat Intelligence / Incident Response
Environment: Local controlled lab
Automation Type: Defensive Threat Intelligence Processing
Mission
Section titled βMissionβYour SOC team has received indicators from several security sources.
The information contains:
IP ADDRESSES
DOMAIN NAMES
SHA-256 HASHES
DUPLICATES
INVALID VALUES
INCONSISTENT FORMATTINGBefore analysts can use these indicators, the data must be cleaned and structured.
Your mission is to build:
RAW IOC DATA βVALIDATION βNORMALIZATION βDEDUPLICATION βCLASSIFICATION βENRICHMENT βPRIORITIZATION βANALYST REPORTThe finished application will become a reusable defensive IOC-processing pipeline.
Why This Lab Matters
Section titled βWhy This Lab MattersβThreat intelligence rarely arrives perfectly formatted.
An organization may receive indicators from:
SIEM
EDR
EMAIL SECURITY
THREAT INTELLIGENCE FEEDS
INCIDENT REPORTS
INTERNAL INVESTIGATIONS
SECURITY VENDORSOne source might provide:
Example.COManother:
example.comand another:
example.com.Without normalization, automation may treat these as different indicators.
Security automation therefore needs to understand:
VALIDITY
TYPE
NORMALIZATION
DUPLICATION
CONTEXT
CONFIDENCELearning Objectives
Section titled βLearning ObjectivesβBy completing this lab, you should be able to:
PROCESS IOC DATA
VALIDATE IP ADDRESSES
VALIDATE DOMAIN NAMES
VALIDATE SHA-256 HASHES
NORMALIZE INDICATORS
REMOVE DUPLICATES
CLASSIFY IOC TYPES
ADD DEFENSIVE CONTEXT
WORK WITH JSON
WORK WITH CSV
DESIGN SAFE API ENRICHMENT
HANDLE API FAILURES
GENERATE ANALYST REPORTSFinal Architecture
Section titled βFinal Architectureβ RAW IOC FILE β βββββββββββββ β INGESTION β βββββββ¬ββββββ β βββββββββββββ β VALIDATOR β βββββββ¬ββββββ β βββββββββββββ β NORMALIZE β βββββββ¬ββββββ β βββββββββββββ βDEDUPLICATEβ βββββββ¬ββββββ β βββββββββββββ β CLASSIFY β βββββββ¬ββββββ β βββββββββββββ β ENRICHMENTβ βββββββ¬ββββββ β βββββββββββββ β PRIORITIZEβ βββββββ¬ββββββ β ββββββββββββββββΌβββββββββββββββ β β β CSV JSON MARKDOWNLab Scenario
Section titled βLab ScenarioβYour SOC receives a file containing:
203.0.113.25192.0.2.5010.10.10.25Example.COMexample.comtraining.example.org44d88612fea8a8f36de82e1278abb02fnot-an-ipbad domain valueSome indicators are:
VALID
INVALID
DUPLICATED
PRIVATE
DOCUMENTATION / TRAINING VALUES
DIFFERENT IOC TYPESYour first responsibility is not to declare anything malicious.
Your first responsibility is:
UNDERSTAND THE DATAImportant Security Principle
Section titled βImportant Security PrincipleβAn IOC is:
AN OBSERVABLEor:
A PIECE OF SECURITY CONTEXTAn IOC match does not automatically mean:
COMPROMISE CONFIRMEDFor example:
IP ADDRESS MATCH βREQUIRES CONTEXT βTIME βDIRECTION βASSET βUSER βSOURCE CONFIDENCE βRELATED ACTIVITYAuthorization and Safety
Section titled βAuthorization and SafetyβUse:
SYNTHETIC INDICATORS
DOCUMENTATION IP RANGES
TRAINING DOMAINS
LOCALLY GENERATED HASHES
AUTHORIZED THREAT-INTELLIGENCE APIsDo not submit confidential organizational data, customer information, or sensitive internal indicators to third-party services unless organizational policy explicitly permits it.
01 β Create the Lab Workspace
Section titled β01 β Create the Lab WorkspaceβCreate:
ioc-enrichment-pipeline/|+-- data/|+-- reports/|+-- src/|+-- cache/|+-- tests/|+-- README.mdLinux/macOS:
mkdir -p ioc-enrichment-pipeline/{data,reports,src,cache,tests}cd ioc-enrichment-pipelinePowerShell:
mkdir ioc-enrichment-pipeline
cd ioc-enrichment-pipeline
mkdir datamkdir reportsmkdir srcmkdir cachemkdir tests02 β Verify Python
Section titled β02 β Verify PythonβRun:
python --versionor:
python3 --versionRecommended:
Python 3.10+03 β Create the IOC Dataset
Section titled β03 β Create the IOC DatasetβCreate:
data/iocs.txtAdd:
203.0.113.25192.0.2.50198.51.100.7510.10.10.25172.16.20.15Example.COMexample.comtraining.example.orgsecurity.example.net44d88612fea8a8f36de82e1278abb02f275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f275A021BBFB6489E54D471899F7DB9D1663FC695EC2FE2A2C4538AABF651FD0Fnot-an-ipbad domain value999.999.999.99904 β Understand the Dataset
Section titled β04 β Understand the DatasetβThe file intentionally contains:
IPv4 ADDRESSES
PRIVATE ADDRESSES
DOMAIN NAMES
MD5-LENGTH HASH
SHA-256 HASH
DUPLICATE SHA-256
INVALID DATAYour application must determine which is which.
05 β IOC Classification Strategy
Section titled β05 β IOC Classification StrategyβUse this order:
RAW VALUE βSHA-256? β NOIP ADDRESS? β NODOMAIN? β NOINVALIDWhy check hashes first?
Because a hash can look like a long hostname-like string if validation is poorly designed.
06 β Create the Python Application
Section titled β06 β Create the Python ApplicationβCreate:
src/ioc_processor.pyAdd:
from pathlib import Pathimport csvimport hashlibimport ipaddressimport jsonimport re07 β Define Project Paths
Section titled β07 β Define Project PathsβAdd:
BASE_DIR = Path(__file__).resolve().parent.parent
INPUT_FILE = BASE_DIR / "data" / "iocs.txt"REPORT_DIR = BASE_DIR / "reports"CACHE_DIR = BASE_DIR / "cache"
REPORT_DIR.mkdir( parents=True, exist_ok=True)
CACHE_DIR.mkdir( parents=True, exist_ok=True)08 β Create IOC Types
Section titled β08 β Create IOC TypesβFor this lab:
IP
DOMAIN
SHA256
INVALIDLater you could extend the tool to support:
URL
EMAIL
IPv6
SHA1
MD5
FILE NAMEBut keep the first version simple.
09 β Validate IP Addresses
Section titled β09 β Validate IP AddressesβCreate:
def parse_ip(value): try: return ipaddress.ip_address( value ) except ValueError: return None10 β Test IP Validation
Section titled β10 β Test IP ValidationβExamples:
203.0.113.25β Valid IP
10.10.10.25β Valid IP
999.999.999.999β Invalid IP11 β Understand IP Context
Section titled β11 β Understand IP ContextβPythonβs ipaddress module can provide properties such as:
is_private
is_global
is_loopback
is_multicast
versionThis allows us to enrich indicators without making any external API request.
12 β Build Local IP Enrichment
Section titled β12 β Build Local IP EnrichmentβCreate:
def enrich_ip_local(ip_obj): return { "ip_version": ip_obj.version,
"is_private": ip_obj.is_private,
"is_global": ip_obj.is_global,
"is_loopback": ip_obj.is_loopback,
"is_multicast": ip_obj.is_multicast }13 β Documentation Address Warning
Section titled β13 β Documentation Address WarningβThe ranges:
192.0.2.0/24
198.51.100.0/24
203.0.113.0/24are reserved for documentation and examples.
That makes them useful for safe training material.
Do not interpret these lab addresses as real threat infrastructure.
14 β Validate SHA-256 Hashes
Section titled β14 β Validate SHA-256 HashesβA SHA-256 hash contains:
64 HEXADECIMAL CHARACTERSCreate:
SHA256_PATTERN = re.compile( r"^[a-fA-F0-9]{64}$")Then:
def is_sha256(value): return bool( SHA256_PATTERN.fullmatch( value ) )15 β Normalize SHA-256
Section titled β15 β Normalize SHA-256βHashes should be normalized to lowercase:
def normalize_sha256(value): return value.lower()Therefore:
275A021B...and:
275a021b...become the same value.
16 β What About MD5?
Section titled β16 β What About MD5?βThe dataset contains:
44d88612fea8a8f36de82e1278abb02fThis is 32 hexadecimal characters.
Your current lab supports only:
SHA-256Therefore the value should not be accepted as SHA-256.
This teaches an important principle:
DO NOT GUESSTHE INDICATOR TYPE17 β Domain Validation
Section titled β17 β Domain ValidationβDomain validation requires more care than simply checking for:
"."Create:
DOMAIN_PATTERN = re.compile( r"^(?=.{1,253}$)" r"(?:" r"[a-zA-Z0-9]" r"(?:[a-zA-Z0-9-]{0,61}" r"[a-zA-Z0-9])?" r"\." r")+" r"[a-zA-Z]{2,63}$")18 β Create Domain Validator
Section titled β18 β Create Domain Validatorβdef is_domain(value): return bool( DOMAIN_PATTERN.fullmatch( value ) )19 β Normalize Domains
Section titled β19 β Normalize DomainsβCreate:
def normalize_domain(value): return ( value .strip() .rstrip(".") .lower() )Therefore:
Example.COMbecomes:
example.com20 β Normalize Raw Input
Section titled β20 β Normalize Raw InputβCreate:
def normalize_raw_value(value): return value.strip()21 β Build the IOC Classifier
Section titled β21 β Build the IOC ClassifierβCreate:
def classify_ioc(raw_value): value = normalize_raw_value( raw_value )
if not value: return { "type": "invalid", "value": value, "reason": "Empty value" }
if is_sha256(value): return { "type": "sha256", "value": normalize_sha256(value) }
ip_obj = parse_ip(value)
if ip_obj: return { "type": "ip", "value": str(ip_obj) }
domain = normalize_domain( value )
if is_domain(domain): return { "type": "domain", "value": domain }
return { "type": "invalid", "value": value, "reason": "Unsupported or malformed IOC" }22 β Test the Classifier
Section titled β22 β Test the ClassifierβInput:
Example.COMExpected:
{ "type": "domain", "value": "example.com"}Input:
10.10.10.25Expected:
{ "type": "ip", "value": "10.10.10.25"}23 β Load the IOC File
Section titled β23 β Load the IOC FileβCreate:
def load_iocs(): records = []
with INPUT_FILE.open( "r", encoding="utf-8" ) as file:
for line_number, line in enumerate( file, start=1 ): raw = line.strip()
if not raw: continue
result = classify_ioc( raw )
result["line"] = line_number result["raw"] = raw
records.append( result )
return records24 β Test IOC Loading
Section titled β24 β Test IOC LoadingβTemporarily add:
records = load_iocs()
for record in records: print(record)Run:
python src/ioc_processor.pyReview each classification.
25 β Separate Valid and Invalid IOCs
Section titled β25 β Separate Valid and Invalid IOCsβCreate:
def split_records(records): valid = [] invalid = []
for record in records: if record["type"] == "invalid": invalid.append(record) else: valid.append(record)
return valid, invalid26 β Deduplication
Section titled β26 β DeduplicationβYour dataset contains:
Example.COM
example.comAfter normalization both become:
example.comThe SHA-256 value also appears twice using different letter casing.
These should be deduplicated.
27 β Build Deduplication Logic
Section titled β27 β Build Deduplication LogicβCreate:
def deduplicate(records): unique = [] duplicates = []
seen = set()
for record in records: key = ( record["type"], record["value"] )
if key in seen: duplicates.append( record ) continue
seen.add(key) unique.append(record)
return unique, duplicates28 β Why Include IOC Type?
Section titled β28 β Why Include IOC Type?βUse:
( record["type"], record["value"])rather than only:
record["value"]This makes the deduplication model explicit and easier to extend.
29 β Count IOC Types
Section titled β29 β Count IOC TypesβImport:
from collections import CounterCreate:
def count_types(records): return Counter( record["type"] for record in records )30 β Local Enrichment
Section titled β30 β Local EnrichmentβBefore using external intelligence services, enrich what you can locally.
For IP addresses:
IP VERSION
PRIVATE?
GLOBAL?
LOOPBACK?
MULTICAST?For domains:
NORMALIZED DOMAIN
TRAINING / EXAMPLE DOMAIN?For hashes:
ALGORITHM
LENGTH31 β Enrich IP Records
Section titled β31 β Enrich IP RecordsβCreate:
def enrich_ip_record(record): ip_obj = ipaddress.ip_address( record["value"] )
record.update( enrich_ip_local( ip_obj ) )
return record32 β Enrich Domain Records
Section titled β32 β Enrich Domain RecordsβBecause this lab uses example domains, create:
def enrich_domain_record(record): domain = record["value"]
training_suffixes = ( "example.com", "example.net", "example.org" )
record["training_domain"] = ( domain in training_suffixes or domain.endswith( tuple( "." + suffix for suffix in training_suffixes ) ) )
return record33 β Enrich Hash Records
Section titled β33 β Enrich Hash RecordsβCreate:
def enrich_hash_record(record): record["algorithm"] = "sha256" record["length"] = len( record["value"] )
return record34 β Build the Local Enrichment Pipeline
Section titled β34 β Build the Local Enrichment PipelineβCreate:
def enrich_locally(records): enriched = []
for record in records: item = record.copy()
if item["type"] == "ip": item = enrich_ip_record( item )
elif item["type"] == "domain": item = enrich_domain_record( item )
elif item["type"] == "sha256": item = enrich_hash_record( item )
enriched.append(item)
return enriched35 β Understand Enrichment
Section titled β35 β Understand EnrichmentβBefore:
{ "type": "ip", "value": "10.10.10.25"}After:
{ "type": "ip", "value": "10.10.10.25", "ip_version": 4, "is_private": true, "is_global": false}The indicator now has more:
CONTEXT36 β External Threat Intelligence
Section titled β36 β External Threat IntelligenceβA production workflow might query an approved threat-intelligence service.
Conceptually:
IOC βAUTHORIZED TI API βREPUTATION / CONTEXT βSOURCE βCONFIDENCE βOBSERVATION DATE37 β Important External-Enrichment Rule
Section titled β37 β Important External-Enrichment RuleβBefore sending indicators to a third-party API ask:
IS THE INDICATOR SENSITIVE?
DOES POLICY ALLOW SHARING?
IS THE PROVIDER APPROVED?
WHAT DATA DOES THE SERVICE RETAIN?
DOES THE REQUEST EXPOSEINTERNAL INVESTIGATION DETAILS?38 β API Architecture
Section titled β38 β API ArchitectureβA safe design:
IOC PROCESSOR βAPI CLIENT βTIMEOUT βAUTHORIZED TI API βRESPONSE VALIDATION βNORMALIZED ENRICHMENT39 β Do Not Hard-Code API Keys
Section titled β39 β Do Not Hard-Code API KeysβAvoid:
API_KEY = "real-secret-key"Instead use:
ENVIRONMENT VARIABLEor an approved:
SECRET MANAGER40 β Read an API Token Safely
Section titled β40 β Read an API Token SafelyβExample:
import os
api_token = os.getenv( "THREAT_INTEL_API_TOKEN")
if not api_token: print( "External enrichment disabled." )This allows the lab to work without requiring a real external service.
41 β Build a Mock Enrichment Service
Section titled β41 β Build a Mock Enrichment ServiceβFor the lab, use:
LOCAL MOCK DATAinstead of depending on a real threat-intelligence platform.
Create:
data/mock_intelligence.jsonAdd:
{ "203.0.113.25": { "source": "training-feed", "confidence": 70, "classification": "review" }, "example.com": { "source": "training-feed", "confidence": 10, "classification": "training" }, "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f": { "source": "training-feed", "confidence": 60, "classification": "review" }}These values are:
SYNTHETICand exist only for training.
42 β Load Mock Intelligence
Section titled β42 β Load Mock IntelligenceβCreate:
MOCK_INTEL_FILE = ( BASE_DIR / "data" / "mock_intelligence.json")Then:
def load_mock_intelligence(): if not MOCK_INTEL_FILE.exists(): return {}
with MOCK_INTEL_FILE.open( "r", encoding="utf-8" ) as file:
return json.load(file)43 β Add Mock Enrichment
Section titled β43 β Add Mock EnrichmentβCreate:
def apply_mock_intelligence( records, intelligence): results = []
for record in records: item = record.copy()
context = intelligence.get( item["value"] )
if context: item["intel_match"] = True item["intel"] = context
else: item["intel_match"] = False item["intel"] = None
results.append(item)
return results44 β Why Use Mock Intelligence?
Section titled β44 β Why Use Mock Intelligence?βIt allows students to learn:
LOOKUP
MATCHING
ENRICHMENT
CONFIDENCE
REPORTINGwithout:
API COST
REAL INDICATOR DISCLOSURE
ACCOUNT REQUIREMENTS
RATE LIMIT DEPENDENCY45 β Threat Intelligence Confidence
Section titled β45 β Threat Intelligence ConfidenceβYour synthetic feed contains:
confidenceBut confidence should not be interpreted as:
70=70% CHANCE THE SYSTEM IS COMPROMISEDConfidence normally relates to the providerβs assessment of the intelligence.
Always understand the sourceβs definition.
46 β Intelligence Source
Section titled β46 β Intelligence SourceβAlways retain:
SOURCEFor example:
{ "source": "training-feed", "confidence": 70}Context without provenance is weaker.
47 β Observation Time
Section titled β47 β Observation TimeβA real enrichment pipeline should also consider:
FIRST SEEN
LAST SEEN
OBSERVATION TIME
FEED UPDATE TIMEbecause threat intelligence can become stale.
48 β Add Analysis Status
Section titled β48 β Add Analysis StatusβFor this lab use:
INFORMATIONAL
REVIEWDo not automatically label an indicator:
MALICIOUSsimply because it appears in the mock intelligence file.
49 β Create a Review Function
Section titled β49 β Create a Review Functionβdef determine_review_status(record): if not record.get( "intel_match" ): return "informational"
intel = record.get( "intel" ) or {}
confidence = intel.get( "confidence", 0 )
if confidence >= 50: return "review"
return "informational"50 β Add Review Status
Section titled β50 β Add Review Statusβdef add_review_status(records): results = []
for record in records: item = record.copy()
item["review_status"] = ( determine_review_status( item ) )
results.append(item)
return results51 β Why Human Review?
Section titled β51 β Why Human Review?βConsider:
IOC MATCH +HIGH CONFIDENCEEven then you still need:
ASSET CONTEXT
EVENT CONTEXT
TIME CONTEXT
USER CONTEXT
NETWORK DIRECTION52 β Complete Processing Pipeline
Section titled β52 β Complete Processing PipelineβCreate:
def process_iocs(): records = load_iocs()
valid, invalid = split_records( records )
unique, duplicates = deduplicate( valid )
enriched = enrich_locally( unique )
intelligence = ( load_mock_intelligence() )
enriched = ( apply_mock_intelligence( enriched, intelligence ) )
enriched = add_review_status( enriched )
return { "raw_count": len(records),
"valid_count": len(valid),
"invalid_count": len(invalid),
"unique_count": len(unique),
"duplicate_count": len(duplicates),
"type_counts": dict( count_types(unique) ),
"records": enriched,
"invalid": invalid,
"duplicates": duplicates }53 β Test the Complete Pipeline
Section titled β53 β Test the Complete PipelineβTemporarily:
results = process_iocs()
print( json.dumps( results, indent=2 ))Run:
python src/ioc_processor.py54 β What Should You Observe?
Section titled β54 β What Should You Observe?βYour application should identify:
VALID IPs
VALID DOMAINS
VALID SHA-256
INVALID VALUES
DUPLICATE DOMAINS
DUPLICATE SHA-256
PRIVATE IP CONTEXT
TRAINING DOMAIN CONTEXT
MOCK INTELLIGENCE MATCHES55 β Export Valid IOCs
Section titled β55 β Export Valid IOCsβCreate:
def export_valid_iocs(records): output = ( REPORT_DIR / "valid-iocs.csv" )
with output.open( "w", newline="", encoding="utf-8" ) as file:
writer = csv.writer(file)
writer.writerow([ "type", "value", "review_status", "intel_match" ])
for record in records: writer.writerow([ record["type"], record["value"], record["review_status"], record["intel_match"] ])56 β Export Invalid IOCs
Section titled β56 β Export Invalid IOCsβCreate:
def export_invalid_iocs(records): output = ( REPORT_DIR / "invalid-iocs.csv" )
with output.open( "w", newline="", encoding="utf-8" ) as file:
writer = csv.writer(file)
writer.writerow([ "line", "raw", "reason" ])
for record in records: writer.writerow([ record["line"], record["raw"], record.get( "reason", "" ) ])57 β Export Duplicates
Section titled β57 β Export DuplicatesβCreate:
def export_duplicates(records): output = ( REPORT_DIR / "duplicates.csv" )
with output.open( "w", newline="", encoding="utf-8" ) as file:
writer = csv.writer(file)
writer.writerow([ "line", "type", "raw", "normalized_value" ])
for record in records: writer.writerow([ record["line"], record["type"], record["raw"], record["value"] ])58 β Export Enriched JSON
Section titled β58 β Export Enriched JSONβCreate:
def export_enriched_json( results): output = ( REPORT_DIR / "enriched-iocs.json" )
with output.open( "w", encoding="utf-8" ) as file:
json.dump( results, file, indent=2 )59 β Generate Analyst Report
Section titled β59 β Generate Analyst ReportβCreate:
def generate_report(results): lines = []
lines.append( "# IOC Processing Report" )
lines.append("")
lines.append( "## Executive Summary" )
lines.append("")
lines.append( f"- Raw indicators: " f"{results['raw_count']}" )
lines.append( f"- Valid indicators: " f"{results['valid_count']}" )
lines.append( f"- Invalid indicators: " f"{results['invalid_count']}" )
lines.append( f"- Unique indicators: " f"{results['unique_count']}" )
lines.append( f"- Duplicates: " f"{results['duplicate_count']}" )
lines.append("")
lines.append( "## IOC Types" )
lines.append("")
for ioc_type, count in ( results["type_counts"].items() ): lines.append( f"- {ioc_type}: {count}" )
lines.append("")
lines.append( "## Indicators Requiring Review" )
lines.append("")
review_records = [ record for record in results["records"] if record["review_status"] == "review" ]
if review_records: for record in review_records: lines.append( f"- {record['type']}: " f"{record['value']}" ) else: lines.append( "- No indicators currently " "require review." )
lines.append("")
lines.append( "## Analyst Guidance" )
lines.append("")
lines.append( "IOC enrichment provides context, " "not automatic proof of compromise. " "Correlate relevant indicators with " "endpoint, network, identity, asset, " "and timeline evidence before making " "an incident determination." )
output = ( REPORT_DIR / "ioc-investigation-report.md" )
output.write_text( "\n".join(lines), encoding="utf-8" )60 β Create main()
Section titled β60 β Create main()βdef main(): results = process_iocs()
export_valid_iocs( results["records"] )
export_invalid_iocs( results["invalid"] )
export_duplicates( results["duplicates"] )
export_enriched_json( results )
generate_report( results )
print( "IOC processing complete." )
print( f"Reports saved to: " f"{REPORT_DIR}" )61 β Add Entry Point
Section titled β61 β Add Entry Pointβif __name__ == "__main__": main()62 β Run the Pipeline
Section titled β62 β Run the PipelineβRun:
python src/ioc_processor.pyExpected:
IOC processing complete.Reports saved to: ...63 β Review Your Reports
Section titled β63 β Review Your ReportsβYour directory should contain:
reports/|+-- valid-iocs.csv|+-- invalid-iocs.csv|+-- duplicates.csv|+-- enriched-iocs.json|+-- ioc-investigation-report.md64 β Review Valid Indicators
Section titled β64 β Review Valid IndicatorsβExample:
type,value,review_status,intel_matchip,203.0.113.25,review,Trueip,192.0.2.50,informational,Falsedomain,example.com,informational,Truesha256,275a...,review,True65 β Review Invalid Indicators
Section titled β65 β Review Invalid IndicatorsβYou should see entries such as:
not-an-ip
bad domain value
999.999.999.999You may also see the unsupported MD5-length value classified as invalid.
66 β Review Duplicate Indicators
Section titled β66 β Review Duplicate IndicatorsβYou should identify normalization-driven duplicates such as:
Example.COMexample.comand the uppercase/lowercase SHA-256 pair.
67 β Build an IOC Processing Matrix
Section titled β67 β Build an IOC Processing Matrixβ| Stage | Purpose |
|---|---|
| Ingest | Read raw indicators |
| Validate | Reject malformed values |
| Normalize | Create consistent representation |
| Deduplicate | Remove repeated indicators |
| Classify | Determine IOC type |
| Enrich | Add useful context |
| Prioritize | Identify review candidates |
| Report | Provide analyst-ready output |
68 β Add Logging
Section titled β68 β Add LoggingβImport:
import loggingConfigure:
logging.basicConfig( level=logging.INFO, format=( "%(asctime)s " "%(levelname)s " "%(message)s" ))Add:
logging.info( "IOC processing started")and:
logging.info( "IOC processing completed")69 β Do Not Log Secrets
Section titled β69 β Do Not Log SecretsβIf you later use external APIs, do not log:
API KEYS
AUTHORIZATION HEADERS
TOKENS
SESSION DATA70 β Error Handling
Section titled β70 β Error HandlingβHandle missing input:
try: results = process_iocs()
except FileNotFoundError: print( "IOC input file not found." ) raise SystemExit(1)71 β Test Missing Input
Section titled β71 β Test Missing InputβTemporarily rename:
iocs.txtRun:
python src/ioc_processor.pyVerify the application:
FAILS CLEANLYRestore the file afterward.
72 β Test Empty Input
Section titled β72 β Test Empty InputβCreate an empty IOC file.
The application should return:
0 indicatorswithout crashing.
73 β Test Case Normalization
Section titled β73 β Test Case NormalizationβAdd:
EXAMPLE.COMExample.Comexample.comExpected normalized value:
example.comExpected unique count:
1for those three domain entries.
74 β Test SHA-256 Normalization
Section titled β74 β Test SHA-256 NormalizationβAdd the same hash as:
LOWERCASE
UPPERCASEVerify that the second occurrence becomes:
DUPLICATE75 β Test Invalid Hashes
Section titled β75 β Test Invalid HashesβTry:
12345and:
ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggNeither should be accepted as SHA-256.
76 β Test IPv6
Section titled β76 β Test IPv6βBecause:
ipaddress.ip_address()supports IPv6, try a documentation address such as:
2001:db8::10Your classifier should identify it as:
IP77 β Add IOC Source Metadata
Section titled β77 β Add IOC Source MetadataβReal indicators should ideally include:
SOURCE
COLLECTION TIME
CASE ID
CONFIDENCE
TAGSInstead of only:
indicator78 β Better IOC Schema
Section titled β78 β Better IOC SchemaβA more mature internal record might look like:
{ "indicator": "example.com", "type": "domain", "source": "training-feed", "first_seen": "2026-08-29T09:00:00Z", "confidence": 50, "tags": [ "training" ]}79 β Provenance
Section titled β79 β ProvenanceβAlways ask:
WHERE DID THIS IOC COME FROM?IOC provenance matters because indicators may originate from:
INTERNAL DETECTION
COMMERCIAL INTELLIGENCE
OPEN-SOURCE INTELLIGENCE
PARTNER FEED
INCIDENT INVESTIGATION80 β Freshness
Section titled β80 β FreshnessβIndicators can become stale.
A domain may:
CHANGE OWNERSHIPAn IP may:
BE REASSIGNEDInfrastructure may:
CHANGE PURPOSETherefore:
IOC+TIME+SOURCEis much more useful than an IOC alone.
81 β Expiration
Section titled β81 β ExpirationβA mature system may track:
FIRST SEEN
LAST SEEN
EXPIRATION
REVIEW DATEDo not treat intelligence as permanently valid.
82 β Confidence vs Severity
Section titled β82 β Confidence vs SeverityβDo not confuse:
CONFIDENCEwith:
SEVERITYConfidence asks:
HOW MUCH DO WE TRUSTTHIS INTELLIGENCE?Severity asks:
WHAT COULD THESECURITY IMPACT BE?83 β Add Asset Context
Section titled β83 β Add Asset ContextβSuppose a matching IP is observed communicating with:
DEVELOPMENT TEST SYSTEMversus:
DOMAIN CONTROLLERThe investigation priority may be very different.
84 β Contextual Enrichment Model
Section titled β84 β Contextual Enrichment ModelβIOC MATCH +ASSET CRITICALITY +USER PRIVILEGE +EVENT TYPE +TIME +SOURCE CONFIDENCE =INVESTIGATION PRIORITY85 β IOC Match Workflow
Section titled β85 β IOC Match WorkflowβA mature SOC workflow could use:
IOC MATCH βVALIDATE INTELLIGENCE βCHECK FRESHNESS βIDENTIFY AFFECTED ASSET βIDENTIFY USER βCHECK RELATED EVENTS βBUILD TIMELINE βANALYST REVIEW86 β Avoid Automatic Blocking
Section titled β86 β Avoid Automatic BlockingβDo not design the first version as:
IOC MATCH βBLOCK EVERYTHINGFalse positives or stale intelligence could interrupt legitimate services.
Prefer:
IOC MATCH βENRICH βCORRELATE βREVIEW βAPPROVED RESPONSE87 β Caching
Section titled β87 β CachingβExternal enrichment can be:
SLOW
RATE LIMITED
EXPENSIVEA cache can reduce repeated lookups.
Conceptually:
IOC βCACHE? βββ YES β USE CACHED CONTEXT β βββ NO β API β CACHE RESULT88 β Cache Expiration
Section titled β88 β Cache ExpirationβDo not cache intelligence forever.
Store:
LOOKUP TIMEand define:
CACHE TTLbased on the data source and operational requirements.
89 β API Failure Handling
Section titled β89 β API Failure HandlingβYour external enrichment design should expect:
TIMEOUT
HTTP ERROR
RATE LIMIT
INVALID JSON
AUTHENTICATION FAILURE
SERVICE OUTAGE90 β Graceful Degradation
Section titled β90 β Graceful DegradationβIf enrichment fails:
IOC PROCESSINGSHOULD NOT NECESSARILY FAILCOMPLETELYInstead:
VALIDATE βNORMALIZE βLOCAL ENRICHMENT βEXTERNAL API FAILED βMARK ENRICHMENT UNAVAILABLE βCONTINUE REPORT91 β Retry Strategy
Section titled β91 β Retry StrategyβFor temporary API failures:
REQUEST βFAIL βWAIT βRETRYUse:
LIMITED RETRIES
BACKOFF
LOGGINGNever create:
INFINITE RETRIES92 β Rate Limiting
Section titled β92 β Rate LimitingβIf an API allows:
100 REQUESTS / MINUTEyour automation must respect that limit.
Otherwise:
AUTOMATION βTOO MANY REQUESTS βRATE LIMIT βFAILED PIPELINE93 β Batch Processing
Section titled β93 β Batch ProcessingβFor large datasets:
10,000 IOCsprocess in manageable batches.
Example:
BATCH 01 βENRICH βSAVE
BATCH 02 βENRICH βSAVE94 β Resume Capability
Section titled β94 β Resume CapabilityβA mature pipeline should avoid restarting:
10,000 LOOKUPSbecause lookup:
9,999failed.
Persist processing state where appropriate.
95 β Generate a File Hash
Section titled β95 β Generate a File HashβTo demonstrate hashing safely, create:
data/sample.txtAdd:
GoHackersCloud IOC Processing LabCalculate SHA-256:
def calculate_sha256(path): sha256 = hashlib.sha256()
with path.open("rb") as file: for block in iter( lambda: file.read(8192), b"" ): sha256.update(block)
return sha256.hexdigest()96 β Run the Hash Function
Section titled β96 β Run the Hash FunctionβExample:
sample_file = ( BASE_DIR / "data" / "sample.txt")
print( calculate_sha256( sample_file ))This demonstrates how file indicators can be generated from local lab artifacts.
97 β Hashing Mental Model
Section titled β97 β Hashing Mental ModelβFILE βSHA-256 βFIXED-LENGTH VALUE βCOMPAREHashes can support:
FILE IDENTIFICATION
INTEGRITY CHECKING
IOC MATCHING98 β Hash Limitations
Section titled β98 β Hash LimitationsβThe same hash strongly identifies the same file content.
But:
ONE BYTE CHANGEDcreates a different cryptographic hash.
Therefore hash-based indicators can be:
HIGHLY SPECIFICbut:
FRAGILE TO FILE MODIFICATION99 β Create a Test Suite
Section titled β99 β Create a Test SuiteβCreate:
tests/test_ioc_processor.py100 β Test IP Parsing
Section titled β100 β Test IP Parsingβdef test_valid_ip(): assert ( parse_ip( "10.10.10.25" ) is not None )101 β Test Invalid IP
Section titled β101 β Test Invalid IPβdef test_invalid_ip(): assert ( parse_ip( "999.999.999.999" ) is None )102 β Test SHA-256
Section titled β102 β Test SHA-256βdef test_sha256(): value = ( "275a021bbfb6489e54d471899f7db9d" "1663fc695ec2fe2a2c4538aabf651fd0f" )
assert is_sha256(value)103 β Test Domain Normalization
Section titled β103 β Test Domain Normalizationβdef test_domain_normalization(): assert ( normalize_domain( "Example.COM" ) == "example.com" )104 β Test Deduplication
Section titled β104 β Test DeduplicationβProvide:
Example.COM
example.comVerify:
UNIQUE = 1
DUPLICATE = 1105 β Test Unsupported Input
Section titled β105 β Test Unsupported InputβProvide:
this is not an indicatorExpected:
INVALID106 β Analyst Investigation Exercise
Section titled β106 β Analyst Investigation ExerciseβAssume your enriched output identifies:
IOC203.0.113.25
TYPEIP
INTELLIGENCE MATCHYES
CONFIDENCE70
STATUSREVIEWWhat should you do next?
Do not immediately declare:
INCIDENTInvestigate:
Where was the IP observed?
Was traffic inbound or outbound?
Which asset communicated with it?
Which user was involved?
When did the communication occur?
Was it blocked?
Was the indicator current at that time?
Are related alerts present?
Is the intelligence source trustworthy?107 β Analyst Investigation Exercise 02
Section titled β107 β Analyst Investigation Exercise 02βSuppose:
example.commatches your training intelligence.
But enrichment says:
training_domain = trueThis demonstrates why:
MULTIPLE CONTEXT SOURCESmatter.
A simple:
MATCH = TRUEis not enough.
108 β Analyst Investigation Exercise 03
Section titled β108 β Analyst Investigation Exercise 03βSuppose a private IP appears:
10.10.10.25An external reputation API may not be meaningful for it.
Instead investigate using:
INTERNAL ASSET INVENTORY
DHCP DATA
IDENTITY DATA
EDR DATA
NETWORK TELEMETRY109 β Internal vs External Enrichment
Section titled β109 β Internal vs External EnrichmentβPUBLIC IP βEXTERNAL + INTERNAL CONTEXT
PRIVATE IP βINTERNAL CONTEXTThis is an important SOC automation design principle.
110 β Build an Investigation Package
Section titled β110 β Build an Investigation PackageβFor every review candidate, ideally collect:
IOC
TYPE
SOURCE
CONFIDENCE
FIRST SEEN
LAST SEEN
AFFECTED ASSET
AFFECTED USER
RELATED ALERTS
REVIEW STATUS
ANALYST NOTES111 β Portfolio Enhancement
Section titled β111 β Portfolio EnhancementβYour Git repository should look like:
ioc-enrichment-pipeline/|+-- data/| +-- iocs.txt| +-- mock_intelligence.json| +-- sample.txt|+-- src/| +-- ioc_processor.py|+-- tests/| +-- test_ioc_processor.py|+-- reports/|+-- cache/|+-- README.md|+-- architecture.md112 β README Structure
Section titled β112 β README StructureβInclude:
PROJECT OVERVIEW
SECURITY USE CASE
ARCHITECTURE
SUPPORTED IOC TYPES
INSTALLATION
USAGE
INPUT FORMAT
OUTPUT FORMAT
VALIDATION LOGIC
ENRICHMENT MODEL
TESTING
SECURITY CONSIDERATIONS
LIMITATIONS
FUTURE IMPROVEMENTS113 β Document Security Considerations
Section titled β113 β Document Security ConsiderationsβInclude:
No real threat indicators required
No production credentials included
No automatic blocking
No automatic containment
External API integration disabled by default
Analyst validation required114 β Document Limitations
Section titled β114 β Document LimitationsβYour first version does not provide:
FULL URL PARSING
EMAIL INDICATORS
WHOIS
PASSIVE DNS
ASN ENRICHMENT
REAL-TIME TI FEEDS
HISTORICAL INTELLIGENCE
ASSET CORRELATION
IDENTITY CORRELATIONThese are future improvements.
115 β Advanced Enhancement: URLs
Section titled β115 β Advanced Enhancement: URLsβA later version could support:
https://example.com/pathBut URL normalization requires careful handling of:
SCHEME
HOST
PORT
PATH
QUERY STRINGDo not simply lowercase an entire URL because paths and parameters may be case-sensitive.
116 β Advanced Enhancement: IOC Relationships
Section titled β116 β Advanced Enhancement: IOC RelationshipsβIndicators often have relationships:
DOMAIN βRESOLVES TO βIPor:
FILE HASH βCONTACTS βDOMAINA mature platform can model these relationships.
117 β IOC Graph Mental Model
Section titled β117 β IOC Graph Mental Modelβ DOMAIN / \ β β IP HASH \ / β β EVENT β ASSET β USERThis moves analysis from:
LIST OF INDICATORStoward:
SECURITY RELATIONSHIPS118 β Advanced Enhancement: STIX/TAXII
Section titled β118 β Advanced Enhancement: STIX/TAXIIβThreat-intelligence platforms may exchange structured information using technologies such as:
STIX
TAXIIYou do not need to implement them in this lab.
Understand the concept:
STANDARDIZEDTHREAT INTELLIGENCEEXCHANGE119 β Advanced Enhancement: SIEM Integration
Section titled β119 β Advanced Enhancement: SIEM IntegrationβFuture architecture:
SIEM ALERT βIOC EXTRACTOR βIOC PROCESSOR βTHREAT INTELLIGENCE βENRICHED EVENT βSOC ANALYST120 β Advanced Enhancement: SOAR
Section titled β120 β Advanced Enhancement: SOARβA security orchestration platform could execute:
ALERT βEXTRACT IOC βENRICH βCORRELATE βCREATE CASE βANALYST REVIEWThe same programming principles from this lab still apply.
121 β Common IOC Processing Mistakes
Section titled β121 β Common IOC Processing MistakesβAvoid:
NO VALIDATION
NO NORMALIZATION
NO DEDUPLICATION
TRUSTING EVERY FEED EQUALLY
IGNORING INTELLIGENCE AGE
CONFUSING CONFIDENCE WITH SEVERITY
AUTOMATICALLY BLOCKING EVERY MATCH
SENDING SENSITIVE IOCs TO UNAPPROVED SERVICES
HARDCODING API KEYS
NO ERROR HANDLING
NO SOURCE ATTRIBUTION
NO HUMAN REVIEW122 β Final Lab Validation
Section titled β122 β Final Lab ValidationβConfirm:
- Python environment works
- IOC file loads correctly
- IP addresses are validated
- IPv6 can be recognized
- Domains are validated
- Domains are normalized
- SHA-256 hashes are validated
- Hashes are normalized
- Unsupported indicators are rejected
- Duplicate domains are removed
- Duplicate hashes are removed
- Private IP context is identified
- Local enrichment works
- Mock intelligence loads
- Intelligence matches are recorded
- Confidence is retained
- Review status is calculated
- Valid IOC CSV is generated
- Invalid IOC CSV is generated
- Duplicate report is generated
- JSON report is generated
- Markdown analyst report is generated
- No secrets are hard-coded
- No automatic blocking occurs
- Limitations are documented
Mission Review
Section titled βMission ReviewβYou started with:
RAW IOC LISTcontaining:
VALID DATA
INVALID DATA
DUPLICATES
MULTIPLE IOC TYPESYou transformed it into:
RAW IOC βVALIDATE βCLASSIFY βNORMALIZE βDEDUPLICATE βENRICH βPRIORITIZE βREPORTWhat You Built
Section titled βWhat You BuiltβYou now have a reusable defensive pipeline capable of processing:
IP ADDRESSES
DOMAIN NAMES
SHA-256 HASHESwhile handling:
INVALID DATA
DUPLICATES
LOCAL CONTEXT
INTELLIGENCE MATCHES
CONFIDENCE
ANALYST REVIEWKey Security Lesson
Section titled βKey Security LessonβThe central lesson from this lab is:
IOC MATCHβ CONFIRMED COMPROMISEInstead:
IOC +SOURCE +FRESHNESS +CONFIDENCE +ASSET CONTEXT +IDENTITY CONTEXT +RELATED EVENTS +ANALYST JUDGMENT =USEFUL SECURITY INTELLIGENCEFinal Mental Model
Section titled βFinal Mental ModelβWhenever you receive threat indicators, think:
WHAT IS IT? βIS IT VALID? βHOW SHOULD IT BE NORMALIZED? βHAVE I SEEN IT BEFORE? βWHERE DID IT COME FROM? βHOW CURRENT IS IT? βWHAT CONTEXT CAN I ADD? βWHERE WAS IT OBSERVED? βWHAT ASSET / USER IS INVOLVED? βDOES IT REQUIRE REVIEW?The goal is not:
COLLECT MORE IOCsThe goal is:
TURN RAW INDICATORSINTO USEFULINVESTIGATION CONTEXTWhatβs Next?
Section titled βWhatβs Next?ββ‘οΈ Lab 03 β Linux Security Automation with Bash
The next lab moves from Python-based threat-intelligence processing into operating-system security automation.
You will build:
LINUX HOST βBASH SECURITY SCRIPT βSYSTEM INVENTORY βUSER REVIEW βPERMISSION REVIEW βPROCESS REVIEW βSERVICE REVIEW βLISTENING PORT REVIEW βAUTHENTICATION LOG REVIEW βSECURITY REPORTYou will combine:
BASH
grep
awk
sort
uniq
find
ss
systemctl
journalctlto build a reusable Linux security assessment workflow for systems you own or are explicitly authorized to administer.