Opening Framing: Scale Changes Everything
In Week 3, you wrote scripts that analyze one login attempt, one IP address, one packet. But real security work involves millions. A SOC analyst doesn't review one log entry—they review millions. A vulnerability scanner doesn't check one port—it checks thousands.
Loops are what make this scale possible. Instead of writing the same code a thousand times, you write it once and let the loop repeat it. This is the fundamental difference between manual and automated security: automation means loops.
This week, you'll learn to process entire log files, scan port ranges, and iterate through lists of indicators—all with code that's barely longer than what you wrote for a single item.
Key insight: Without loops, scripts are calculators. With loops, they become security tools. Every scanner, every parser, every automated detector uses loops to achieve scale.
1) The For Loop: Iterating Over Collections
The for loop processes each item in a collection one at a time:
# Iterate over a list of IPs
suspicious_ips = ["192.168.1.50", "10.0.0.25", "172.16.0.100"]
for ip in suspicious_ips:
print(f"Checking IP: {ip}")
# Output:
# Checking IP: 192.168.1.50
# Checking IP: 10.0.0.25
# Checking IP: 172.16.0.100
How It Works:
for- keyword that starts the loopip- variable that holds the current item (you choose the name)in- keyword connecting variable to collectionsuspicious_ips- the collection to iterate over- The indented block runs once for each item
# Iterate over characters in a string
password = "abc123"
for char in password:
print(f"Character: {char}")
# Useful for checking password composition
Key insight: The loop variable (ip, char) automatically
takes on each value in sequence. You don't manage this—Python handles it.
2) Range: Generating Number Sequences
The range() function generates sequences of numbers—perfect
for port scanning, counting, and indexed access:
# range(stop) - 0 to stop-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4
# range(start, stop) - start to stop-1
for port in range(20, 26):
print(f"Scanning port {port}") # 20, 21, 22, 23, 24, 25
# range(start, stop, step) - with increment
for port in range(20, 30, 2):
print(f"Port {port}") # 20, 22, 24, 26, 28
Security Application: Port Range Scanning
# Simulate scanning well-known ports
print("Scanning well-known ports (1-1024)...")
open_ports = []
for port in range(1, 1025):
# In real scanner, this would check if port is open
# Here we simulate some ports being "open"
if port in [22, 80, 443, 8080]:
open_ports.append(port)
print(f"[OPEN] Port {port}")
print(f"\nScan complete. {len(open_ports)} open ports found.")
Key insight: range() is memory-efficient—it doesn't create all
numbers at once, just generates them as needed. You can range(1, 1000000)
without running out of memory.
3) The While Loop: Condition-Based Repetition
While loops repeat as long as a condition is True—useful when you don't know in advance how many iterations you need:
# Basic while loop
attempts = 0
max_attempts = 3
while attempts < max_attempts:
print(f"Login attempt {attempts + 1}")
attempts += 1
print("Max attempts reached - account locked")
Security Application: Retry with Backoff
# Simulating connection retry logic
import time
max_retries = 5
retry_count = 0
connected = False
while not connected and retry_count < max_retries:
retry_count += 1
print(f"Connection attempt {retry_count}...")
# Simulate: connection succeeds on attempt 3
if retry_count == 3:
connected = True
print("Connected successfully!")
else:
wait_time = retry_count * 2 # Exponential backoff
print(f"Failed. Waiting {wait_time} seconds...")
# time.sleep(wait_time) # Uncomment for real delay
if not connected:
print("All retries exhausted. Connection failed.")
WARNING: Infinite Loops
# DANGER: This runs forever!
# while True:
# print("This never stops")
# Always ensure your condition will eventually become False
# or use 'break' to exit
Key insight: Use for when you know how many times to loop
(or are iterating a collection). Use while when you loop
until a condition changes.
4) Loop Control: break and continue
Sometimes you need to exit a loop early or skip certain iterations:
# break - exit loop immediately
blocklist = ["192.168.1.100", "10.0.0.50", "BLOCKED", "172.16.0.1"]
for ip in blocklist:
if ip == "BLOCKED":
print("Encountered invalid entry - stopping")
break
print(f"Processing: {ip}")
# Output:
# Processing: 192.168.1.100
# Processing: 10.0.0.50
# Encountered invalid entry - stopping
# continue - skip to next iteration
log_entries = ["INFO: Started", "ERROR: Failed", "INFO: Running", "ERROR: Crash"]
print("Showing only ERROR entries:")
for entry in log_entries:
if not entry.startswith("ERROR"):
continue # Skip non-error entries
print(entry)
# Output:
# Showing only ERROR entries:
# ERROR: Failed
# ERROR: Crash
Security Application: Finding First Match
# Search for malicious indicator - stop at first match
indicators = ["clean.exe", "safe.dll", "malware.exe", "normal.doc"]
known_malware = ["malware.exe", "trojan.exe", "virus.dll"]
found_threat = None
for file in indicators:
if file in known_malware:
found_threat = file
break # No need to continue searching
if found_threat:
print(f"ALERT: Malware detected - {found_threat}")
else:
print("Scan complete - no threats found")
Key insight: break is essential for efficiency. Why check
10,000 more files once you've found malware? Exit early, respond fast.
5) Nested Loops and Loop Patterns
Loops can contain other loops—useful for multi-dimensional data like scanning multiple hosts across multiple ports:
# Nested loop: scan multiple hosts, multiple ports
hosts = ["192.168.1.1", "192.168.1.2", "192.168.1.3"]
ports = [22, 80, 443]
for host in hosts:
print(f"\nScanning {host}...")
for port in ports:
print(f" Checking {host}:{port}")
# This produces 9 checks (3 hosts × 3 ports)
Accumulator Pattern: Building Results
# Count occurrences in log
log_lines = [
"Failed login from 192.168.1.50",
"Successful login from 192.168.1.10",
"Failed login from 192.168.1.50",
"Failed login from 10.0.0.25",
"Failed login from 192.168.1.50"
]
failed_count = 0
for line in log_lines:
if "Failed" in line:
failed_count += 1
print(f"Total failed logins: {failed_count}")
Filter Pattern: Collecting Matches
# Extract all IPs with failed logins
failed_ips = []
for line in log_lines:
if "Failed" in line:
# Extract IP (simplified - assumes consistent format)
ip = line.split("from ")[1]
if ip not in failed_ips:
failed_ips.append(ip)
print(f"IPs with failed logins: {failed_ips}")
Key insight: Most security scripts follow these patterns—iterate through data, accumulate counts, filter matches. Recognize the pattern, apply it to any problem.
Real-World Context: Loops in Security Operations
Loops power every security tool you'll encounter:
Nmap Port Scanner: At its core, Nmap loops through IP
ranges and port ranges. A command like nmap -p 1-1000 192.168.1.0/24
creates nested loops: for each of 256 hosts, check each of 1000 ports.
That's 256,000 checks from one command.
Log Analysis: Tools like Splunk and ELK process billions
of log entries using loops. Every search query loops through indexed data.
The SPL query index=firewall action=blocked | stats count by src_ip
loops through entries, filtering and counting.
Brute Force Attacks: From the attacker's perspective, credential stuffing is a loop: for each username/password pair in a list, attempt login. A 10,000-entry credential list is just a for loop with 10,000 iterations.
MITRE ATT&CK Reference: Technique T1110 (Brute Force) describes attacks that are essentially loops. Understanding loops helps you understand how attacks scale and how to detect them through iteration patterns (many attempts from one source).
Key insight: Loops are morally neutral—they amplify both attack and defense. Attackers loop to brute force; defenders loop to detect. The tool is the same.
Guided Lab: Log File Analyzer
Let's build a script that processes multiple log entries and produces a security summary—a simplified version of what SIEM tools do.