The Sabotage Series #1 Follow-Up: Fixing the Log Cleaner That Cleaned Too Much

The Sabotage Series #1 Follow-Up: Fixing the Log Cleaner That Cleaned Too Much

The Reveal

In the first Sabotage Series challenge, we looked at a short PowerShell script that was supposed to clean up old IIS logs before they filled a production server.

The script looked harmless. It had a clear root path, a retention period, and a simple delete loop. But it also carried a classic automation failure: the blast radius was much larger than the intent.

The Flaw

The original script recursively scanned C:inetpublogs and deleted anything older than 30 days:

Get-ChildItem -Path $LogRoot -Recurse |
    Where-Object { $_.LastWriteTime -lt $CutoffDate } |
    ForEach-Object {
        Remove-Item -Path $_.FullName -Force
        Write-Output "Deleted: $($_.FullName)"
    }

The hidden problem is that it never limits the target to files, and it never limits the match to IIS log files. With -Recurse, that means any old item under the log root can become a deletion candidate.

That is how a cleanup job becomes an incident. The script was written with a narrow mental model, but the filesystem was allowed to be much broader.

A Safer PowerShell Version

A safer version should reduce scope before deleting anything. At minimum, filter for files, match the expected IIS log pattern, and support a dry run before live deletion.

$LogRoot = "C:inetpublogsLogFiles"
$RetentionDays = 30
$CutoffDate = (Get-Date).AddDays(-$RetentionDays)
$DryRun = $true

Write-Output "Starting IIS log cleanup for .log files older than $RetentionDays days..."

$ExpiredLogs = Get-ChildItem -Path $LogRoot -Recurse -File -Filter "*.log" |
    Where-Object { $_.LastWriteTime -lt $CutoffDate }

foreach ($LogFile in $ExpiredLogs) {
    if ($DryRun) {
        Write-Output "DRY RUN: Would delete $($LogFile.FullName)"
    }
    else {
        Remove-Item -LiteralPath $LogFile.FullName -Force -WhatIf:$false
        Write-Output "Deleted: $($LogFile.FullName)"
    }
}

Write-Output "IIS log cleanup completed. Files evaluated: $($ExpiredLogs.Count)"

Before scheduling this in production, run it with $DryRun = $true, review the output, and only then flip it to live mode.

An Equivalent Python Fix

Here is the same idea in Python: explicit root, explicit file extension, explicit age check, and dry-run behavior by default.

from pathlib import Path
from datetime import datetime, timedelta

log_root = Path(r"C:inetpublogsLogFiles")
retention_days = 30
dry_run = True
cutoff = datetime.now() - timedelta(days=retention_days)

print(f"Starting IIS log cleanup for .log files older than {retention_days} days...")

expired_logs = []

for path in log_root.rglob("*.log"):
    if not path.is_file():
        continue

    last_modified = datetime.fromtimestamp(path.stat().st_mtime)

    if last_modified < cutoff:
        expired_logs.append(path)

for log_file in expired_logs:
    if dry_run:
        print(f"DRY RUN: Would delete {log_file}")
    else:
        log_file.unlink()
        print(f"Deleted: {log_file}")

print(f"IIS log cleanup completed. Files evaluated: {len(expired_logs)}")

The SRE Lesson

The bug was not just a missing filter. The bug was trusting intent more than boundaries.

Reliable automation should make dangerous actions boring by making them constrained, observable, and reversible where possible.

  • Constrain scope before applying destructive actions.
  • Prefer allowlists over broad recursive searches.
  • Include a dry-run mode for production cleanup jobs.
  • Log both what happened and what would have happened.
  • Design automation for the filesystem you might have tomorrow, not just the one you have today.

The next Sabotage Series challenge will move from disk cleanup to service health checks, where a polling script reports green while the application is already failing.

Leave a Reply

Your email address will not be published. Required fields are marked *