Objective. Students use Python's built-in file I/O and the re module to parse structured and semi-structured log files and extract indicators of compromise.
This lesson, its worked examples and its practice questions has not yet been verified by an industry-certified professional or by a certification or approval body. It is a study draft and may contain errors. Do not use it as instructions for work on a job site or on live equipment. Always follow your instructor, your supervising professional, and current official references and regulations.
VoTechWorks program approval and accreditation are still in progress. Credentials are issued only by the certifying bodies named in each program, after their own exams.
Log files are one of the most valuable sources of evidence in cybersecurity. Web servers, firewalls, authentication systems, and endpoints all write structured or semi-structured text logs that record what happened and when. A security analyst who can parse those logs programmatically gains a decisive advantage: instead of scrolling through thousands of lines by hand, you write a script that extracts exactly what you need in seconds.
This lesson teaches you to use Python's built-in file I/O together with the `re` module (regular expressions) and the `argparse` module (command-line argument parsing) to turn raw Apache access logs into actionable indicators of compromise.
A typical Apache Combined Log Format line looks like this:
```
192.168.1.42 - frank [10/Oct/2024:13:55:36 -0700] "GET /login HTTP/1.1" 404 512
```
The fields, left to right, are: client IP, identity (usually `-`), authenticated user (or `-`), timestamp in brackets, the request line in quotes, HTTP status code, and response size in bytes. When a client repeatedly receives HTTP 404 (Not Found) responses, it may indicate automated scanning, directory brute-forcing, or an attacker mapping your web application — all indicators of compromise (IoCs) worth capturing.
Always open files with a `with` statement. This context manager guarantees the file handle is closed even if an exception is raised, which prevents resource leaks on long-running systems.
```python
with open("access.log", "r", encoding="utf-8") as f:
for line in f:
process(line)
```
Reading line by line (iterating over the file object) is memory-efficient: the entire file never loads into RAM at once, which matters when logs run to gigabytes.
Before attempting to open a file, validate that it exists. Use `pathlib.Path.exists()` or catch `FileNotFoundError` and exit with an informative message using `sys.exit(1)`. Exiting with a non-zero code signals failure to any calling shell script or pipeline — a professional habit that makes your tools composable.
The `re` module provides compiled pattern matching. For log parsing you will use two functions most often:
A pattern to capture the source IP and HTTP status code from an Apache log line:
```python
import re
PATTERN = re.compile(
r'^(\d{1,3}(?:\.\d{1,3}){3}).*?"\S+ \S+ \S+" (\d{3})'
)
```
Group 1 captures the IP address; group 2 captures the three-digit status code. Access groups with `match.group(1)` and `match.group(2)`.
To count occurrences of each IP and record the first and last time each one appeared, use a dictionary keyed by IP address. Each value can be a small dictionary or a list storing `[count, first_seen, last_seen]`.
```python
records = {} # key: ip_str, value: [count, first_seen, last_seen]
if ip not in records:
records[ip] = [1, timestamp, timestamp]
else:
records[ip][0] += 1
records[ip][2] = timestamp # update last_seen
```
Parse the timestamp string from the log line using another capture group or Python's `datetime.strptime` if you need to compare times numerically.
Python's built-in `csv` module handles quoting and delimiter rules automatically. Always write a header row first:
```python
import csv
with open("report.csv", "w", newline="", encoding="utf-8") as out:
writer = csv.writer(out)
writer.writerow(["IP", "count", "first_seen", "last_seen"])
for ip, (count, first, last) in sorted(records.items(),
key=lambda x: x[1][0],
reverse=True):
writer.writerow([ip, count, first, last])
```
The `newline=""` argument is required by the `csv` module on all platforms to prevent extra blank lines.
Hard-coding the log file path inside a script makes it a one-time tool. Use `argparse` to make it reusable:
```python
import argparse
parser = argparse.ArgumentParser(description="Apache 404 analyzer")
parser.add_argument("logfile", help="Path to Apache access log")
parser.add_argument("--threshold", type=int, default=10,
help="Minimum 404 count to include in report")
args = parser.parse_args()
```
Access values as `args.logfile` and `args.threshold`. The `--help` flag is generated automatically.
Log files frequently contain personal data (IP addresses are considered personal identifiers under many privacy frameworks). Handle log files only on systems and with data you are authorized to access. In this lab, use only the provided sample files on your assigned Ubuntu VM — never copy real production logs to personal devices. Keep your VM patched; run scripts in your own user account, not as root, to limit blast radius if a script has a bug. When your lab session ends, do not leave sensitive output files in shared directories.
The workflow for this lab is: parse arguments → validate the file exists → open and iterate the log → apply the regex → filter for 404 lines → accumulate records → apply the threshold filter → write the CSV → exit cleanly. Each step is short and testable independently. Build and test one function at a time before combining them.
Answer each question, then check it. Nothing is recorded; this is your own practice.
A free tech program — complimentary, as our commitment to get America working. Help us build VoTechWorks. Tell us what works, what doesn’t, and what you want to learn.