Skip to content
CSY103 Week 08 Beginner

Practice error handling and defensive coding before moving to reading resources.

Programming Fundamentals

Track your progress through this week's content

Opening Framing: When Things Go Wrong

Your scripts will encounter errors. Files won't exist. Networks will timeout. Users will provide malformed input. APIs will return unexpected data. In security work, these aren't edge cases—they're Tuesday.

A script that crashes during an incident is worse than no script at all. Defensive coding anticipates failures and handles them gracefully. Your script should report what went wrong, continue if possible, and never leave systems in an inconsistent state.

This week, you'll learn exception handling, input validation, and defensive programming patterns that make your security tools production-ready.

Key insight: Professional code isn't code that never fails—it's code that fails predictably and recovers gracefully. Error handling separates scripts from tools.

1) Exceptions: Understanding Errors

Python uses exceptions to signal errors. When something goes wrong, Python "raises" an exception:

# Common exceptions you'll encounter
int("abc")           # ValueError: invalid literal
my_list[100]         # IndexError: list index out of range
my_dict["missing"]   # KeyError: 'missing'
open("nofile.txt")   # FileNotFoundError
10 / 0               # ZeroDivisionError
import nonexistent   # ModuleNotFoundError

Exception Hierarchy:

  • Exception - base class for most errors
  • ValueError - wrong value type or content
  • TypeError - wrong data type
  • KeyError - dictionary key not found
  • IndexError - list index out of range
  • FileNotFoundError - file doesn't exist
  • PermissionError - access denied
  • ConnectionError - network issue
  • TimeoutError - operation timed out

Key insight: Exceptions aren't just error messages—they're objects with types. Different exception types let you handle different errors differently.

2) Try-Except: Catching Errors

The try-except block catches exceptions and handles them instead of crashing:

# Basic try-except
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
    result = 0

print(f"Result: {result}")  # Continues execution

Catching Specific Exceptions:

# Handle different errors differently
def safe_parse_port(port_string):
    try:
        port = int(port_string)
        if port < 1 or port > 65535:
            raise ValueError("Port out of range")
        return port
    except ValueError as e:
        print(f"Invalid port '{port_string}': {e}")
        return None

# Test
print(safe_parse_port("443"))     # 443
print(safe_parse_port("abc"))     # None (with error message)
print(safe_parse_port("99999"))   # None (out of range)

Multiple Exception Types:

# Handle multiple exception types
def read_config(filepath):
    try:
        with open(filepath, "r") as f:
            import json
            return json.load(f)
    except FileNotFoundError:
        print(f"Config file not found: {filepath}")
        return {}
    except json.JSONDecodeError as e:
        print(f"Invalid JSON in {filepath}: {e}")
        return {}
    except PermissionError:
        print(f"Permission denied: {filepath}")
        return {}

Key insight: Catch specific exceptions, not generic Exception. Specific handling lets you respond appropriately—retry network errors, report file errors, validate input errors.

3) Try-Except-Else-Finally

The full try statement has four parts:

try:
    # Code that might raise an exception
    file = open("data.txt", "r")
    data = file.read()
except FileNotFoundError:
    # Runs if exception occurs
    print("File not found")
    data = ""
else:
    # Runs only if NO exception occurred
    print(f"Read {len(data)} bytes")
finally:
    # ALWAYS runs, even if exception occurred
    print("Cleanup complete")
    # Good place to close connections, release resources

Security Application: Safe File Processing

def process_log_file(filepath):
    """Process a log file with comprehensive error handling."""
    events = []
    errors = []
    
    try:
        with open(filepath, "r") as file:
            for line_num, line in enumerate(file, 1):
                try:
                    # Process each line (might fail on malformed data)
                    event = parse_log_line(line.strip())
                    events.append(event)
                except ValueError as e:
                    # Log error but continue processing
                    errors.append(f"Line {line_num}: {e}")
                    continue
    except FileNotFoundError:
        return None, [f"File not found: {filepath}"]
    except PermissionError:
        return None, [f"Permission denied: {filepath}"]
    else:
        print(f"Successfully processed {len(events)} events")
    finally:
        if errors:
            print(f"Encountered {len(errors)} parsing errors")
    
    return events, errors

Key insight: finally is for cleanup that must happen regardless of success or failure—closing files, releasing locks, logging completion.

4) Input Validation: Trust Nothing

In security, all input is suspect until validated. Defensive coding validates before processing:

def validate_ip_address(ip_string):
    """Validate IPv4 address format."""
    if not isinstance(ip_string, str):
        raise TypeError("IP address must be a string")
    
    parts = ip_string.split(".")
    if len(parts) != 4:
        raise ValueError(f"Invalid IP format: {ip_string}")
    
    for part in parts:
        try:
            num = int(part)
        except ValueError:
            raise ValueError(f"Non-numeric octet in IP: {part}")
        
        if num < 0 or num > 255:
            raise ValueError(f"Octet out of range: {num}")
    
    return True

# Usage with error handling
def safe_validate_ip(ip_string):
    try:
        validate_ip_address(ip_string)
        return True
    except (TypeError, ValueError) as e:
        print(f"Invalid IP: {e}")
        return False

Validation Patterns:

# Validate before use - common patterns

def validate_port(port):
    """Ensure port is valid integer in range."""
    if not isinstance(port, int):
        raise TypeError(f"Port must be integer, got {type(port)}")
    if port < 1 or port > 65535:
        raise ValueError(f"Port must be 1-65535, got {port}")
    return port

def validate_hash(hash_string, expected_length=32):
    """Validate hash is hex string of expected length."""
    if not isinstance(hash_string, str):
        raise TypeError("Hash must be string")
    if len(hash_string) != expected_length:
        raise ValueError(f"Hash must be {expected_length} chars")
    if not all(c in "0123456789abcdefABCDEF" for c in hash_string):
        raise ValueError("Hash must be hexadecimal")
    return hash_string.lower()

def validate_severity(severity):
    """Ensure severity is one of allowed values."""
    allowed = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    severity = severity.upper()
    if severity not in allowed:
        raise ValueError(f"Severity must be one of {allowed}")
    return severity

Key insight: Validate early, fail fast. It's better to reject bad input immediately than to discover the problem deep in your code where recovery is harder.

5) Defensive Coding Patterns

Beyond exception handling, defensive coding anticipates and prevents errors:

Pattern 1: Default Values

# Use .get() with defaults instead of direct access
config = {"timeout": 30}

# Dangerous - crashes if key missing
# value = config["missing_key"]

# Safe - returns default if missing
timeout = config.get("timeout", 60)
retries = config.get("retries", 3)  # Returns 3, not KeyError

Pattern 2: Guard Clauses

def process_event(event):
    """Process security event with guard clauses."""
    # Guard: Check for None
    if event is None:
        return None
    
    # Guard: Check required fields
    if "timestamp" not in event:
        print("Missing timestamp")
        return None
    
    if "source_ip" not in event:
        print("Missing source_ip")
        return None
    
    # Main logic (only reached if guards pass)
    return {
        "time": event["timestamp"],
        "ip": event["source_ip"],
        "processed": True
    }

Pattern 3: Safe Operations

# Safe list access
def safe_get_index(lst, index, default=None):
    try:
        return lst[index]
    except (IndexError, TypeError):
        return default

# Safe division
def safe_divide(a, b, default=0):
    try:
        return a / b
    except (ZeroDivisionError, TypeError):
        return default

# Safe type conversion
def safe_int(value, default=0):
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

Pattern 4: Logging Errors

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def process_ioc(ioc):
    try:
        # Processing logic
        result = validate_and_enrich(ioc)
        logger.info(f"Processed IOC: {ioc}")
        return result
    except ValueError as e:
        logger.warning(f"Invalid IOC {ioc}: {e}")
        return None
    except Exception as e:
        logger.error(f"Unexpected error processing {ioc}: {e}")
        raise  # Re-raise unexpected errors

Key insight: Defensive coding is about expecting the unexpected. Every external input, every file operation, every network call can fail. Plan for it.

Real-World Context: Error Handling in Security Tools

Error handling is critical in security operations:

Incident Response: During an incident, your scripts process data from compromised systems—data that may be corrupted, incomplete, or maliciously crafted. Scripts that crash on malformed data waste precious time. Robust error handling lets you process what you can and report what failed.

Continuous Monitoring: Security monitoring runs 24/7. A script that crashes at 3 AM because of a network timeout leaves a gap in coverage. Proper error handling retries transient failures and alerts on persistent ones.

Input Validation as Defense: Many attacks exploit poor input validation—SQL injection, command injection, path traversal. The same validation patterns you use in scripts apply to understanding and preventing these attacks.

MITRE ATT&CK Reference: Technique T1059 (Command and Scripting Interpreter) includes attacks where malicious input causes scripts to behave unexpectedly. Defensive coding prevents your security tools from becoming attack vectors themselves.

Key insight: In security, reliability is a security property. Tools that fail unpredictably create gaps attackers can exploit.

Guided Lab: Robust IOC Processor

Let's build an IOC processor that handles all the ways real-world data can go wrong.

Step 1: Create Test Data with Errors

Create messy_iocs.txt:

192.168.1.50
10.0.0.25
not_an_ip
256.1.1.1
5d41402abc4b2a76b9719d911017c592
invalid_hash_xyz
a1b2c3d4e5f6
192.168.1.100

malicious-domain.com
-invalid-domain
evil.com
another.good.domain.net

Step 2: Create the Robust Processor

Create robust_ioc_processor.py:

"""
Robust IOC Processor
Demonstrates defensive coding and error handling
"""

import re
import json
from datetime import datetime


class ValidationError(Exception):
    """Custom exception for validation failures."""
    pass


def validate_ipv4(ip_string):
    """Validate IPv4 address with detailed error messages."""
    if not isinstance(ip_string, str):
        raise ValidationError(f"Expected string, got {type(ip_string).__name__}")
    
    ip_string = ip_string.strip()
    if not ip_string:
        raise ValidationError("Empty string")
    
    parts = ip_string.split(".")
    if len(parts) != 4:
        raise ValidationError(f"Expected 4 octets, got {len(parts)}")
    
    for i, part in enumerate(parts):
        try:
            num = int(part)
        except ValueError:
            raise ValidationError(f"Octet {i+1} '{part}' is not a number")
        
        if num < 0 or num > 255:
            raise ValidationError(f"Octet {i+1} value {num} out of range (0-255)")
    
    return ip_string


def validate_md5(hash_string):
    """Validate MD5 hash format."""
    if not isinstance(hash_string, str):
        raise ValidationError(f"Expected string, got {type(hash_string).__name__}")
    
    hash_string = hash_string.strip().lower()
    if not hash_string:
        raise ValidationError("Empty string")
    
    if len(hash_string) != 32:
        raise ValidationError(f"Expected 32 characters, got {len(hash_string)}")
    
    if not re.match(r'^[a-f0-9]{32}$', hash_string):
        raise ValidationError("Contains non-hexadecimal characters")
    
    return hash_string


def validate_domain(domain_string):
    """Validate domain name format."""
    if not isinstance(domain_string, str):
        raise ValidationError(f"Expected string, got {type(domain_string).__name__}")
    
    domain_string = domain_string.strip().lower()
    if not domain_string:
        raise ValidationError("Empty string")
    
    if domain_string.startswith("-") or domain_string.endswith("-"):
        raise ValidationError("Domain cannot start or end with hyphen")
    
    if not re.match(r'^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$', domain_string):
        raise ValidationError("Invalid domain format")
    
    if "." not in domain_string:
        raise ValidationError("Domain must contain at least one dot")
    
    return domain_string


def detect_ioc_type(value):
    """Attempt to detect IOC type from value."""
    value = value.strip()
    
    # Try IPv4
    if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', value):
        return "ipv4"
    
    # Try MD5 (32 hex chars)
    if re.match(r'^[a-fA-F0-9]{32}$', value):
        return "md5"
    
    # Try SHA256 (64 hex chars)
    if re.match(r'^[a-fA-F0-9]{64}$', value):
        return "sha256"
    
    # Try domain (contains dot, no spaces)
    if "." in value and " " not in value and not value[0].isdigit():
        return "domain"
    
    return "unknown"


def process_ioc(value, line_number=None):
    """Process a single IOC with full error handling."""
    result = {
        "original": value,
        "line_number": line_number,
        "valid": False,
        "type": None,
        "normalized": None,
        "error": None
    }
    
    # Guard: empty or whitespace-only
    if not value or not value.strip():
        result["error"] = "Empty value"
        return result
    
    value = value.strip()
    ioc_type = detect_ioc_type(value)
    result["type"] = ioc_type
    
    # Validate based on detected type
    try:
        if ioc_type == "ipv4":
            result["normalized"] = validate_ipv4(value)
            result["valid"] = True
        elif ioc_type == "md5":
            result["normalized"] = validate_md5(value)
            result["valid"] = True
        elif ioc_type == "sha256":
            result["normalized"] = value.lower()
            result["valid"] = True
        elif ioc_type == "domain":
            result["normalized"] = validate_domain(value)
            result["valid"] = True
        else:
            result["error"] = "Could not determine IOC type"
    except ValidationError as e:
        result["error"] = str(e)
    except Exception as e:
        result["error"] = f"Unexpected error: {e}"
    
    return result


def process_ioc_file(filepath):
    """Process entire IOC file with comprehensive error handling."""
    results = {
        "filepath": filepath,
        "processed_at": datetime.now().isoformat(),
        "total_lines": 0,
        "valid_iocs": [],
        "invalid_iocs": [],
        "errors": [],
        "summary": {}
    }
    
    try:
        with open(filepath, "r") as file:
            for line_num, line in enumerate(file, 1):
                results["total_lines"] += 1
                
                # Skip empty lines and comments
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                
                # Process the IOC
                ioc_result = process_ioc(line, line_num)
                
                if ioc_result["valid"]:
                    results["valid_iocs"].append(ioc_result)
                else:
                    results["invalid_iocs"].append(ioc_result)
    
    except FileNotFoundError:
        results["errors"].append(f"File not found: {filepath}")
    except PermissionError:
        results["errors"].append(f"Permission denied: {filepath}")
    except Exception as e:
        results["errors"].append(f"Unexpected error: {e}")
    
    # Generate summary
    results["summary"] = {
        "total_lines": results["total_lines"],
        "valid_count": len(results["valid_iocs"]),
        "invalid_count": len(results["invalid_iocs"]),
        "by_type": {}
    }
    
    for ioc in results["valid_iocs"]:
        ioc_type = ioc["type"]
        results["summary"]["by_type"][ioc_type] = \
            results["summary"]["by_type"].get(ioc_type, 0) + 1
    
    return results


def print_report(results):
    """Print human-readable report."""
    print("=" * 60)
    print("IOC PROCESSING REPORT")
    print("=" * 60)
    print(f"File: {results['filepath']}")
    print(f"Processed: {results['processed_at']}")
    print()
    
    summary = results["summary"]
    print(f"Total lines: {summary['total_lines']}")
    print(f"Valid IOCs: {summary['valid_count']}")
    print(f"Invalid entries: {summary['invalid_count']}")
    print()
    
    if summary["by_type"]:
        print("Valid IOCs by type:")
        for ioc_type, count in summary["by_type"].items():
            print(f"  {ioc_type}: {count}")
        print()
    
    if results["invalid_iocs"]:
        print("Invalid entries:")
        for ioc in results["invalid_iocs"][:10]:  # Show first 10
            print(f"  Line {ioc['line_number']}: '{ioc['original']}' - {ioc['error']}")
        if len(results["invalid_iocs"]) > 10:
            print(f"  ... and {len(results['invalid_iocs']) - 10} more")
    
    print()
    print("=" * 60)


# Main execution
if __name__ == "__main__":
    # Process the file
    results = process_ioc_file("messy_iocs.txt")
    
    # Print report
    print_report(results)
    
    # Export to JSON
    with open("ioc_results.json", "w") as f:
        json.dump(results, f, indent=2)
    print("Results exported to ioc_results.json")

Step 3: Run and Analyze

Run python3 robust_ioc_processor.py and examine how it handles errors.

Step 4: Reflection (mandatory)

  1. How does the script handle different types of errors differently?
  2. Why create a custom ValidationError exception?
  3. What would happen without the guard clause for empty values?
  4. How does the script continue processing after encountering errors?

Week 8 Outcome Check

By the end of this week, you should be able to:

Next week: Working with External Data—where we connect to APIs, fetch web content, and process data from external sources.

🎯 Hands-On Labs (Free & Essential)

Practice error handling and defensive coding before moving to reading resources.

🎮 TryHackMe: Python Basics (Exceptions)

What you'll do: Handle errors and validate input in guided Python exercises.
Why it matters: Defensive coding keeps tools reliable under pressure.
Time estimate: 1-1.5 hours

Start TryHackMe Python Basics →

📝 Lab Exercise: Safe Input Validator

Task: Write a function that validates IP, port, and username input with try-except.
Deliverable: Script that prints clear error messages and never crashes.
Why it matters: Attackers exploit fragile parsing and validation.
Time estimate: 45-60 minutes

🏁 PicoCTF Practice: General Skills (Error Handling)

What you'll do: Solve beginner challenges that require handling malformed input.
Why it matters: Defensive scripts are resilient to bad data.
Time estimate: 1-2 hours

Start PicoCTF General Skills →

💡 Lab Tip: Fail closed: reject bad input early and log why.

🛡️ Secure Coding: Fail-Safe Error Handling

Exceptions are part of normal operation in security tools. The goal is to keep the system safe and observable when things go wrong.

Error handling checklist:
- Catch specific exceptions, not bare except
- Avoid leaking secrets in error messages
- Log errors with context (but no sensitive data)
- Use safe defaults and explicit failure modes

📚 Building on CSY101 Week-13: Model how attackers can trigger error paths.

Resources

Complete the required resources to build your foundation.

Lab: Bulletproof Log Parser

Goal: Enhance a log parser to handle every possible error condition gracefully.

Linux/Windows Path (same for both)

  1. Create corrupted_log.txt with:
    • Valid log entries
    • Empty lines
    • Malformed timestamps
    • Missing fields
    • Non-UTF8 characters (optional challenge)
  2. Create bulletproof_parser.py that:
    • Handles missing file gracefully
    • Handles permission errors
    • Validates each line before processing
    • Continues on parse errors (doesn't crash)
    • Logs all errors with line numbers
    • Produces summary of successful vs. failed parses
  3. Test with both valid and invalid files

Deliverable (submit):

Checkpoint Questions

  1. What is the difference between a syntax error and an exception?
  2. Why catch specific exceptions instead of bare except:?
  3. When does the finally block execute?
  4. What is a guard clause and why use one?
  5. Why is input validation especially important in security tools?
  6. How does the .get() method help with defensive coding?

Weekly Reflection

Reflection Prompt (200-300 words):

This week you learned defensive coding—the practice of anticipating and handling errors gracefully. Robust error handling separates production-quality tools from fragile scripts.

Reflect on these questions:

A strong reflection will connect defensive coding to broader security principles and real-world operational needs.

Verified Resources & Videos

Error handling transforms scripts into reliable tools. With defensive coding, your security automation runs reliably during the incidents when you need it most. Next week: connecting to external data sources.

← Previous: Week 07 Next: Week 09 →

Week 08 Quiz

Test your understanding of the weekly concepts.

Format: 10 multiple-choice questions. Passing score: 70%. Time: Untimed.

Take Quiz