From c4a179ffc2612a71a130e71d9a0725e8adfa1b3b Mon Sep 17 00:00:00 2001 From: Bill Seremetis Date: Tue, 31 Mar 2026 16:53:32 +0300 Subject: [PATCH] access.log forensics --- commands/host/ai-prompts | 60 +++++++++++++++++++++++++ scripts/prompts/access-log-forensics.md | 33 ++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 scripts/prompts/access-log-forensics.md diff --git a/commands/host/ai-prompts b/commands/host/ai-prompts index 873f6b5..61adbe3 100755 --- a/commands/host/ai-prompts +++ b/commands/host/ai-prompts @@ -20,6 +20,7 @@ echo_yellow() { echo -e "${YELLOW}$1${NC}" >&2; } # Menu options OPTIONS=( "1. BackstopJS Init" + "2. Access Log Forensics" ) # AI Agent options @@ -103,6 +104,62 @@ backstop_init() { esac } +# Access Log Forensics function +access_log_forensics() { + # Pick a log file - prefer fzf over *.log files in current dir, fallback to manual input + local log_file + local log_files + log_files=$(find . -maxdepth 1 -name "*_access.log" 2>/dev/null) + + if [[ -n "$log_files" ]]; then + log_file=$(echo "$log_files" | sed 's|^\./||' | fzf --reverse --height=50% --header="Select access log file") + fi + + if [[ -z "$log_file" ]]; then + echo_yellow "No .log files found in current directory. Enter path manually:" + read -r log_file + fi + + if [[ -z "$log_file" || ! -f "$log_file" ]]; then + echo_red "File not found: $log_file" + exit 1 + fi + + # Select AI agent + local agent_selection + agent_selection=$(select_agent) + if [[ -z "$agent_selection" ]]; then + echo_red "No agent selected... Exiting" + exit 0 + fi + + local agent_label + agent_label=$(echo "$agent_selection" | sed 's/^[0-9]*\. //') + + echo_green "Running Access Log Forensics with ${agent_label} on ${log_file}..." + echo "" + + local prompt_content + prompt_content=$(sed -e "s|__ACCESS_LOG_FILE__|$log_file|g" \ + "${DDEV_APPROOT}/.ddev/scripts/prompts/access-log-forensics.md") + + case "$agent_label" in + "Gemini") + gemini "$prompt_content" + ;; + "Copilot") + copilot "$prompt_content" + ;; + "Claude") + claude "$prompt_content" + ;; + *) + echo_red "Unknown agent: $agent_label" + exit 1 + ;; + esac +} + # Main function main() { # Banner @@ -136,6 +193,9 @@ main() { "BackstopJS Init") backstop_init ;; + "Access Log Forensics") + access_log_forensics + ;; *) echo_red "Unknown action: $action_label" exit 1 diff --git a/scripts/prompts/access-log-forensics.md b/scripts/prompts/access-log-forensics.md new file mode 100644 index 0000000..107fc34 --- /dev/null +++ b/scripts/prompts/access-log-forensics.md @@ -0,0 +1,33 @@ +I have an Apache access log at __ACCESS_LOG_FILE__ +Run the following triage in order, stopping to flag anomalies before proceeding: +**STEP 1 — Baseline (run all at once)** +- Date range of the log +- Total request count +- HTTP status code distribution (flag if 4xx+5xx > 10% of total) +- Requests per hour (flag any hour > 3x the median) + +**STEP 2 — Top offenders** +- Top 30 source IPs with request counts +- Group IPs by /24 subnet and flag any subnet with > 500 requests +- Top 20 user agents (flag empty UAs, known scanners, rotating browser strings + doing uniform behaviour) +- Top 20 requested URLs (flag if one URL accounts for > 20% of traffic) + +**STEP 3 — Attack signatures (only on IPs/subnets flagged above)** +- What URLs are they hitting? Uniform = likely DDoS/flood. Varied = crawler/scraper. +- What referrers are they sending? Flag non-existent domains. +- What is their request rate per hour? Coordinated spikes = botnet. +- Run whois on the top flagged /24 to identify the owning organisation and country. + +**STEP 4 — Verdict** +Produce a short report: +- Is an attack ongoing right now? (check last 30 mins of log) +- Was there a past attack? When did it peak, when did it stop? +- What is the blast radius? (did it cause 500/502s for legitimate users?) +- What is the source? (ASN, country, likely actor type) +- Immediate action: list of CIDR ranges to block +- Abuse contact if available from whois + +Keep responses concise. Lead with the verdict, support with numbers. + +