Created
April 20, 2026 16:30
-
-
Save birkin/72f01455668aca535209dc16af51840b to your computer and use it in GitHub Desktop.
sanitizes mod-security output for LLM analysis.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S uv run | |
| """ | |
| Sanitizes extracted mod_security log output for safer sharing. | |
| This script preserves the general structure and diagnostic meaning of the log | |
| while redacting likely sensitive values such as IP addresses, hostnames, URIs, | |
| unique request identifiers, timestamps, and absolute file paths. | |
| Usage: | |
| uv run ./sanitize_mod_sec_logs.py | |
| uv run ./sanitize_mod_sec_logs.py ./extracted_modsec_log.txt | |
| uv run ./sanitize_mod_sec_logs.py ./extracted_modsec_log.txt -o ./sanitized_extracted_modsec_log.txt | |
| """ | |
| import argparse | |
| import re | |
| from pathlib import Path | |
| UNIQUE_ID_PATTERN = re.compile(r'\b(?=[A-Za-z0-9_-]{20,}\b)(?=.*\d)(?=.*[a-z])[A-Za-z0-9-]+\b') | |
| def sanitize_line(line: str) -> str: | |
| """ | |
| Sanitizes one mod_security log line by obfuscating sensitive values. | |
| Called by: sanitize_text() | |
| """ | |
| sanitized_line = line | |
| sanitized_line = re.sub(r'\[file "/[^"]+"\]', '[file "<file-path>"]', sanitized_line) | |
| ## replaces bracketed key-value fields that commonly carry sensitive details | |
| bracket_patterns: list[tuple[str, str]] = [ | |
| (r'\[client [^\]]+\]', '[client <client-ip>]'), | |
| (r'\[hostname "[^"]+"\]', '[hostname "<hostname>"]'), | |
| (r'\[uri "[^"]+"\]', '[uri "<uri>"]'), | |
| (r'\[unique_id "[^"]+"\]', '[unique_id "<unique-id>"]'), | |
| ] | |
| for pattern, replacement in bracket_patterns: | |
| sanitized_line = re.sub(pattern, replacement, sanitized_line) | |
| ## replaces standalone timestamped request-header lines and raw paths | |
| sanitized_line = re.sub( | |
| r'^\[(?P<timestamp>[^\]]+)\]\s+\S+\s+\d{1,3}(?:\.\d{1,3}){3}\s+\d+\s+\d{1,3}(?:\.\d{1,3}){3}\s+\d+$', | |
| '[<timestamp>] <unique-id> <client-ip> <client-port> <server-ip> <server-port>', | |
| sanitized_line, | |
| ) | |
| sanitized_line = re.sub(r'/etc/[\w./-]+', '<file-path>', sanitized_line) | |
| sanitized_line = UNIQUE_ID_PATTERN.sub('<unique-id>', sanitized_line) | |
| ## replaces remaining identifiers, addresses, and timing values | |
| replacements: list[tuple[str, str]] = [ | |
| ( | |
| r'\b\d{1,3}(?:\.\d{1,3}){3}\b', | |
| '<ip-address>', | |
| ), | |
| ( | |
| r'^Stopwatch:\s+.+$', | |
| 'Stopwatch: <timing-redacted>', | |
| ), | |
| ( | |
| r'^Stopwatch2:\s+.+$', | |
| 'Stopwatch2: <timing-redacted>', | |
| ), | |
| ] | |
| for pattern, replacement in replacements: | |
| sanitized_line = re.sub(pattern, replacement, sanitized_line) | |
| return sanitized_line | |
| def sanitize_text(text: str) -> str: | |
| """ | |
| Sanitizes the full mod_security log text while preserving general structure. | |
| Called by: write_sanitized_file() | |
| """ | |
| sanitized_lines: list[str] = [] | |
| for line in text.splitlines(): | |
| sanitized_lines.append(sanitize_line(line)) | |
| sanitized_text = '\n'.join(sanitized_lines) | |
| if text.endswith('\n'): | |
| sanitized_text = f'{sanitized_text}\n' | |
| return sanitized_text | |
| def write_sanitized_file(input_path: Path, output_path: Path) -> None: | |
| """ | |
| Reads the input file, sanitizes it, and writes the sanitized output file. | |
| Called by: main() | |
| """ | |
| original_text = input_path.read_text(encoding='utf-8') | |
| sanitized_text = sanitize_text(original_text) | |
| output_path.write_text(sanitized_text, encoding='utf-8') | |
| def build_argument_parser() -> argparse.ArgumentParser: | |
| """ | |
| Builds the CLI argument parser for the sanitizer script. | |
| Called by: main() | |
| """ | |
| parser = argparse.ArgumentParser( | |
| description='Sanitize extracted mod_security logs for safe sharing with an LLM.', | |
| ) | |
| parser.add_argument( | |
| 'input_file', | |
| nargs='?', | |
| default='extracted_modsec_log.txt', | |
| help='Path to the input mod_security log file.', | |
| ) | |
| parser.add_argument( | |
| '-o', | |
| '--output', | |
| default='sanitized_extracted_modsec_log.txt', | |
| help='Path to write the sanitized output file.', | |
| ) | |
| return parser | |
| def main() -> None: | |
| """ | |
| Parses arguments and runs the sanitizer. | |
| Called by: __main__ | |
| """ | |
| parser = build_argument_parser() | |
| args = parser.parse_args() | |
| input_path = Path(args.input_file) | |
| output_path = Path(args.output) | |
| write_sanitized_file(input_path, output_path) | |
| print(f'Sanitized log written to {output_path}') | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment