#!/usr/bin/perl
# Analyze error_log v1.1.2  (c) 17.10.2003 by Andreas Ley  (u) 21.4.2026
# Count error.log entries, send mail to top users, check for bad CGIs

$min_cnt = 25;
$max_send = 100;
$length = 77;

$min_cgis = 20;

$name = "Web-Server";
$from = "apache\@scc.kit.edu";

$sendmail = '/usr/lib/sendmail';

use Getopt::Std;
use Sys::Hostname;

sub usage
{
	my $image = $0;
	$image =~ s!.*/!!;
	print STDERR "Usage: $image [-n] [-a alias] [-m address] [-r referrer] [-f flag] [filename [...]]\n";
	print STDERR "-n  Dry run\n";
	print STDERR "-a  Show server alias in mail subject\n";
	print STDERR "-m  Mail address for the main server admin\n";
	print STDERR "-r  Show only errors with this referrer\n";
	print STDERR "-f  Specify tested path for .noerrormail flag\n";
	exit(1);
}

getopts('Dhxna:m:r:f:');

&usage if ($opt_h);

if (defined($opt_v) || defined($opt_D)) {
	$|=1;
	select((select(STDERR),$|=1)[0]);
}

$host = hostname;
$host .= " ($opt_a)" if (length($opt_a));

$my = "my";
$my = "mycl" if (-d "/etc/cluster");

while (<>) {
	# Ignore McAfee Vulnerability Manager (Foundstone Scripting Language)
	# Siehe 2011-08-23-Protokoll_SCC-Sec.pdf
	next if (/\[client (?:129\.13\.76\.134|141\.52\.250\.230)\]/);
	# Ignore errors without (locatable) local origin
	next if (
		# Ignore errors not resolvable by webmasters
		/Symbolic link not allowed or link target not accessible: \/home\/[^\/]+\/[^\/]+$/ ||
		# Some script kiddies use attack scripts that don't send SNI
		/provided via HTTP have no compatible SSL setup/ ||
		# PHP FMP gone away
		/Connection refused: AH02454: FCGI: attempt to connect to Unix domain socket|AH01079: failed to make connection to backend: httpd-UDS/ ||
		# OCSP stapling lock
		/Identifier removed: AH0194[89]:/ ||
		# SSL session cache lock
		/Invalid argument: AH0202[67]:/ ||
		# https://www.apachelounge.com/viewtopic.php?t=9032
		#  "Just looked at the code again. I will change the log level from Warning to Debug on this.
		#  There is nothing to worry about here. It just means that during connection shutdown, there were ongoing requests that did not immediately exit."
		/h2_stream\([0-9-]*,CLEANUP\): started=1?, scheduled=1?, ready=0?, out_buffer=0?/ ||
		# Ignore failed content negotiation or excluded clients
		/(no acceptable variant|client denied by server configuration):/ ||
		# Ignore failed authentication
		/Password Mismatch|user .* not found:/ ||
		# Ignore misses without referrer
		# Apache 2.0: script not found or unable to stat:
		# Apache 2.2: script '.*' not found or unable to stat
		# Apache 2.4 with PHP-FPM: AH01071: Got error 'Primary script unknown'
		(/Directory index forbidden by Options directive:|File does not exist:|script not found or unable to stat:|script '.*' not found or unable to stat|Got error 'Primary script unknown'/ && !/, referer:/) ||
		# Ignore robots, browser icons and Nimda
		/File does not exist: .*\/(robots\.txt|favicon\.ico|scripts|MSADC|winnt|_vti_[^\/]+)(, referer: .*)?$/ ||
		# Ignore worms searching for vulnerable scripts, faking a top level referrer
		/(File does not exist|script not found or unable to stat): .*\/(cgi|local-cgi|[fF]orm[mM]ail|mail\.cgi|email\.cgi|contact\.cgi|dp_tellafriend).*, referer: http:\/\/[^\/]+\/$/ ||
		# Ignore worms searching for ShellShock vulnerable scripts
		/(File does not exist|script not found or unable to stat): .*, referer: \(\) \{ :; \};/ ||
		# Ignore misbehaving clients (mostly worms)
		/request failed: error reading the headers|invalid URI path|Invalid URI in request|Invalid method in request|client sent HTTP\/1\.1 request without hostname|Hostname .* provided via SNI and hostname .* provided via HTTP are different|Hostname .* provided via SNI, but no hostname provided in HTTP request|attempt to invoke directory as script:|Cannot serve directory .*: No matching DirectoryIndex|Options ExecCGI is off in this directory:/ ||
		# Ignore search engine dynamic content
		/, referer: http:\/\/([^\/]*\.)?google\./ ||
		# Ignore wrong referrer
		(defined($opt_r) && /, referer:/ && !/, referer: http:\/\/$opt_r\//));
	if (m!/home/ws/([^/]+)/public_html/!) {
		$user = $1;
		print STDERR "[$user] $_" if ($opt_D);
		$log{$user} .= $_ unless ($cnt{$user}++ >= $max_send);
	}
	else {
		print STDERR "[MAIN] $_" if ($opt_D);
		$log .= $_ unless ($cnt++ >= $max_send);
	}
	# Check for CGI searching worms
	if (!/, referer:/ && /script not found or unable to stat:\s*(.*)/) {
		$cgi{$1}++;
	}
}

sub lbreak
{
	my ($indent,$text) = @_;
	my ($out);

	my $len = $length - length($indent);
	while (length($text) > $len) {
		if (substr($text,0,$len+1) =~ /\s+(\S*)$/) {
			$out .= $indent.$`."\n";
			$text = $1.substr($text,$len+1);
		}
		else {
			$out .= $indent.substr($text,0,$len)."\n";
			$text = substr($text,$len);
		}
		$indent = " " x length($indent);
	}
	return($out.$indent.$text);
}

sub mail
{
	my ($to,$cnt,$log,$site,$subject) = @_;

	print STDERR '+ ',join(' ',$sendmail,'-i','-t','-f',$from),"\n" if ($opt_D);
	if ($opt_n) {
		#open(MAIL,">&STDOUT") or die "Can't dup STDOUT: $!\n";
		open(MAIL,'|-','sed','s/^/MAIL: /') or die "Can't run sed 's/^/MAIL: /': $!\n";
	}
	else {
		open(MAIL,'|-',$sendmail,'-i','-t','-f',$from) or die "Can't run $sendmail -i -t -f $from: $!\n";
	}
	print MAIL "Precedence: bulk\n";
	print MAIL "From: $name <$from>\n";
	print MAIL "To: $to\n";
	#print MAIL "Cc: $name <$from>\n";
	print MAIL "Subject: $host error.log$subject\n";
	print MAIL "MIME-Version: 1.0\n";
	print MAIL "Content-Type: text/plain\n";
	print MAIL "Content-Transfer-Encoding: quoted-printable\n";
	print MAIL "\n";
	print MAIL &lbreak("","Hallo!")."\n\n";
	print MAIL &lbreak("","(Dies ist eine automatische Mail von ihrem freundlichen Web-Server :)")."\n\n";
	print MAIL &lbreak("","Sie scheinen etwas Probleme mit Ihrer $site zu haben - jedenfalls tauchen im gestrigen error.log des Web-Servers $cnt Eintr=E4ge auf, die sich auf Ihre $site beziehen. Vielleicht m=F6chten Sie dem Problem ja nachgehen.")."\n\n";
	print MAIL &lbreak("","Leider kommt es immer mal wieder vor, dass (abgewehrte) Angriffsversuche in den Logs aufscheinen – typisch daf=FCr ist ein geh=E4uftes Auftreten von Versuchen, auf gesch=FCtzte Dateien (wie z.B. /etc/passwd) au=DFerhalb des Dokumentenbaums zuzugreifen (=FCber sog. Path Traversal, mehrere \"../\" hintereinander) oder der Aufruf nicht existierender CGI-Scripts oder Login-Seiten von typischen Web-Anwendungen. Leider gibt es da eine Vielzahl von M=F6glichkeiten, zudem l=E4sst sich ein Angriff auf eine nicht vorhandene Web-Anwendung schwer von einem legalen, aber fehlgeschlagenen Zugriff auf ein nicht korrekt installierte solche unterscheiden, so dass ein automatisiertes Filtern nicht vollst=E4ndig m=F6glich ist.")."\n\n";
	print MAIL &lbreak("","Wenn Sie sich auf dem Web-Server einloggen, k=F6nnen Sie mit dem Kommando \"${my}errorlog\" die Eintr=E4ge des heutigen Tages ansehen und so ggf. den Fehler analysieren und beheben. Wenn Sie den Fehler nicht selbst beheben k=F6nnen oder die Ursache nicht finden, helfen wir auch gerne weiter.")."\n\n";
	print MAIL &lbreak("","Sollten Sie =FCbrigens diese Fehler-Hinweis-Mails nicht mehr bekommen wollen, k=F6nnen Sie diese durch Anlegen einer Datei \".noerrormail\" im Dokumentenverzeichnis des Webservers abbestellen (z.B. Einloggen per SSH, dann \"touch $opt_f\"). Beachten Sie aber bitte, dass es sich bei den Fehlern in der Regel um Probleme handelt, die f=FCr die Leser ihrer Seiten sichtbar werden; einer Behebung der Probleme sollte also Vorrang gew=E4hrt werden.")."\n\n" if (defined($opt_f));
	if ($log =~ /File does not exist:/) {
		print MAIL &lbreak("","Die h=E4ufigste Fehlerursache sind nicht existierende Dateien. Meist hat man sich nur beim entsprechenden Link verschrieben, oder vergessen, die entsprechende Datei auch anzulegen.")."\n\n";
	}
	if ($log =~ /Symbolic link not allowed:/) {
		print MAIL &lbreak("","Ein typisches Problem sind symbolische Links, die nicht Ihnen selbst geh=F6ren. Normalerweise spielt dies =FCberhaupt keine Rolle - der Web-Server ist da aber etwas pingeliger. Die einfachste Abhilfe ist es, alle nicht Ihnen geh=F6renden symbolischen Links zu l=F6schen und neu anzulegen.")."\n\n";
	}
	if ($cnt > $max_send) {
		print MAIL &lbreak("","Hier die ersten $max_send Zeilen aus dem error.log:")."\n\n";
	}
	else {
		print MAIL &lbreak("","Hier die entsprechenden Zeilen aus dem error.log:")."\n\n";
	}
	print MAIL $log;
	close(MAIL);
}

for $user (keys %log) {
	&mail($user,$cnt{$user},$log{$user},"Homepage"," ($user)") if ($cnt{$user} >= $min_cnt);
}
&mail(defined($opt_m)?$opt_m:$from,$cnt,$log,"Website") if ($cnt >= $min_cnt);

$num_cgis = scalar(keys %cgi);
if ($num_cgis >= $min_cgis) {
	print STDERR '+ ',join(' ',$sendmail,'-i','-t','-f',$from),"\n" if ($opt_D);
	if ($opt_n) {
		#open(MAIL,">&STDOUT") or die "Can't dup STDOUT: $!\n";
		open(MAIL,'|-','sed','s/^/MAIL: /') or die "Can't run sed 's/^/MAIL: /': $!\n";
	}
	else {
		open(MAIL,'|-',$sendmail,'-i','-t','-f',$from) or die "Can't run $sendmail -i -t -f $from: $!\n";
	}
	print MAIL "From: $name <$from>\n";
	print MAIL "To: $name <$from>\n";
	print MAIL "Subject: $host: $num_cgis non-existent CGIs called\n";
	print MAIL "Precedence: bulk\n\n";
	for $cgi (sort {$cgi{$b} <=> $cgi{$a}} keys %cgi) {
		print MAIL $cgi{$cgi},"\t",$cgi,"\n";
		last unless($max_send--);
	}
	close(MAIL);
}
