diff options
| author | 2015-12-04 12:12:28 +0400 | |
|---|---|---|
| committer | 2015-12-04 12:12:28 +0400 | |
| commit | 0c1b6cc2808b4fd45779cce4835a6a80eae48265 (patch) | |
| tree | 39cc7ca32ea5f39c1977d1fdd2147b160e278e9b | |
Initial commit
| -rwxr-xr-x | cgi-bin/query.pl | 130 | ||||
| -rw-r--r-- | cgi-bin/secret/.htaccess | 6 | ||||
| -rwxr-xr-x | cgi-bin/secret/investigate.pl | 203 | ||||
| -rwxr-xr-x | cgi-bin/secret/logs.pl | 34 | ||||
| -rwxr-xr-x | cgi-bin/showUsers.pl | 75 | ||||
| -rw-r--r-- | checks.txt | 28 | ||||
| -rw-r--r-- | index.html | 37 | ||||
| -rw-r--r-- | investigate.html | 18 | ||||
| -rw-r--r-- | query.html | 50 | ||||
| -rw-r--r-- | syntax.txt | 175 |
10 files changed, 756 insertions, 0 deletions
diff --git a/cgi-bin/query.pl b/cgi-bin/query.pl new file mode 100755 index 0000000..4a9ed26 --- /dev/null +++ b/cgi-bin/query.pl @@ -0,0 +1,130 @@ +#!/usr/bin/perl + +#use warnings; +use Data::Dumper; +use strict; +use DBI; + +use CGI_Lite; + +my $dbh = DBI->connect("DBI:mysql:database=asm_main;host=localhost;port=3306", 'USER', 'PASSWORD'); + +my $debug = 0; + +sub esc +{ + my ($arg) = @_; + $arg = $dbh->quote($arg); + $arg =~ s/\*/%/g; + $arg =~ s/_/\\_/g; + $arg =~ s/\?/_/g; + return $arg; +} + +my $cgi = new CGI_Lite; +my %data = $cgi->parse_form_data; + +$debug = int($data{debug}) if (defined($data{debug})); + +if ($debug) { + print "Content-type: text/plain", "\n\n"; + print Dumper(\%data); +} else { + print "Content-type: text/html", "\n\n"; + print "<html><head><title>Query results</title></head><body>\n"; +} + +my ($channel, $nick, $user, $host); +my ($level, $id, $reason); + +my $qry = "SELECT time, channel, nick, user, host, gecos, level, id, reason FROM alertlog WHERE "; + +if (defined($data{channel})) { + $qry = $qry . "channel like " . esc($data{channel}); +} else { die "channel not defined!\n"; } + +if (defined($data{nick}) && ($data{nick} ne "*") && ($data{nick} ne "")) { + $qry .= " and nick like " . esc($data{nick}); +} + +if (defined($data{user}) && ($data{user} ne "*") && ($data{user} ne "")) { + $qry .= " and user like " . esc($data{user}); +} + +if (defined($data{host}) && ($data{host} ne "*") && ($data{host} ne "")) { + $qry .= " and host like " . esc($data{host}); +} + +if (defined($data{gecos}) && ($data{gecos} ne "*") && ($data{gecos} ne "")) { + $qry .= " and gecos like " . esc($data{gecos}); +} + +if (defined($data{since})) { + $qry .= sprintf("and time > '%04d-%02d-%02d %02d:%02d:%02d'", + int($data{syear}), int($data{smonth}), int($data{sday}), + int($data{shour}), int($data{smin}), int($data{ssec})); +} + +if (defined($data{before})) { + $qry .= sprintf("and time < '%04d-%02d-%02d %02d:%02d:%02d'", + int($data{byear}), int($data{bmonth}), int($data{bday}), + int($data{bhour}), int($data{bmin}), int($data{bsec})); +} + +#if (defined($data{id})) { +# $qry .= " and id = " . $dbh->quote($data{id}); +#} + +if (defined($data{level}) && ($data{level} ne "any")) { + $qry .= " and level = " . $dbh->quote($data{level}); +} + +if (defined($data{reason})) { + $qry .= " and reason like " . esc($data{reason}); +} + +if (defined($data{sort}) && defined($data{order}) && ($data{order} =~ /^[ad]$/ ) && + ( $data{sort} =~ /^(time|nick|user|host|level|id|reason)$/ ) ) { + $qry .= " order by " . $data{sort}; + $qry .= " desc" if $data{order} eq "d"; +} + +if ($debug) { + print "Querying: "; + print Dumper($qry); +} + +my $sth = $dbh->prepare($qry); +$sth->execute; +my $names = $sth->{'NAME'}; +my $numFields = $sth->{'NUM_OF_FIELDS'}; + + print "<table border=\"1\"><tr>" unless $debug; + +for (my $i = 0; $i < $numFields; $i++) { + if ($debug) { + printf("%s%s", $i ? "," : "", $$names[$i]); + } else { + print "<th>" . $$names[$i] . "</th>"; + } +} + +print "</tr>" unless $debug; +print "\n"; + +while (my $ref = $sth->fetchrow_arrayref) { + print "<tr>" unless $debug; + + for (my $i = 0; $i < $numFields; $i++) { + if ($debug) { + printf("%s%s", $i ? "," : "", $$ref[$i]); + } else { + print "<td>" . $$ref[$i] . "</td>"; + } + } + print "</tr>" unless $debug; + print "\n"; +} +unless ($debug) { + print "</table></body></html>"; +} diff --git a/cgi-bin/secret/.htaccess b/cgi-bin/secret/.htaccess new file mode 100644 index 0000000..14ea91d --- /dev/null +++ b/cgi-bin/secret/.htaccess @@ -0,0 +1,6 @@ +AuthType Basic +AuthName "Restricted Files" +AuthUserFile /home/icxcnika/AntiSpamMeta/HTTP_ACCESS_USER +AuthGroupFile /home/icxcnika/AntiSpamMeta/HTTP_ACCESS_GROUP +Require group actionlogs +#Require user icxcnika diff --git a/cgi-bin/secret/investigate.pl b/cgi-bin/secret/investigate.pl new file mode 100755 index 0000000..0716480 --- /dev/null +++ b/cgi-bin/secret/investigate.pl @@ -0,0 +1,203 @@ +#!/usr/bin/perl + +#use warnings; +use Data::Dumper; +use strict; +use DBI; + +use CGI_Lite; + +my $dbh = DBI->connect("DBI:mysql:database=asm_main;host=localhost;port=3306", 'USER', 'PASSWORD'); + +my $debug = 0; + +sub esc +{ + my ($arg) = @_; + $arg = $dbh->quote($arg); + $arg =~ s/\*/%/g; + $arg =~ s/_/\\_/g; + $arg =~ s/\?/_/g; + return $arg; +} + +sub dottedQuadToInt +{ + my ($dottedquad) = @_; + my $ip_number = 0; + my @octets = split(/\./, $dottedquad); + foreach my $octet (@octets) { + $ip_number <<= 8; + $ip_number |= $octet; + } + return $ip_number; +} + +my $cgi = new CGI_Lite; +my %data = $cgi->parse_form_data; + +$debug = int($data{debug}) if (defined($data{debug})); + +if ( !defined($data{query}) ) { +print "Content-type: text/html", "\n\n"; +print <<HTML; +<html> + <head> + <title>AntiSpamMeta database query page</title> + </head> + <body> + <h3>Maintaining AntiSpamMeta takes work! Please +<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"> +<input type="hidden" name="cmd" value="_s-xclick"> +<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----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-----END PKCS7----- +"> +<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"> +<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1"> +</form></h3> + <p>Matching is done based on field1 OR field2 OR field3 etc. Wildcards are supported, +except for the realIP field, which must be blank or an IPv4 dotted quad.</p> + <form action="/cgi-bin/secret/investigate.pl" method="get"> + <input type="hidden" name="query" value="1" /> +HTML +print ' Nickname: <input type="text" name="nick" ' . (defined($data{nick}) ? 'value="'.$data{nick}.'" ' : '') . "/><br />\n"; +print ' User: <input type="text" name="user" ' . (defined($data{user}) ? 'value="'.$data{user}.'" ' : '') . "/><br />\n"; +print ' Hostname: <input type="text" name="host" ' . (defined($data{host}) ? 'value="'.$data{host}.'" ' : '') . "/><br />\n"; +print ' Gecos: <input type="text" name="gecos" ' . (defined($data{gecos}) ? 'value="'.$data{gecos}.'" ' : '') . "/><br />\n"; +print ' Account: <input type="text" name="account" ' . (defined($data{account}) ? 'value="'.$data{account}.'" ' : '') . "/><br />\n"; +print ' Real IP: <input type="text" name="realip" ' . (defined($data{realip}) ? 'value="'.$data{realip}.'" ' : '') . "/>\n"; +print <<HTML; + <br /><br /><input type="submit" value="Query!" /> + </form> + </body> +</html> +HTML +exit 0; +} + +if ($debug) { + print "Content-type: text/plain", "\n\n"; + print Dumper(\%data); +} else { + print "Content-type: text/html", "\n\n"; + print <<HTML; +<html> +<head> +<title>Query results</title> +<style> +tr {font-size:90%;} +.uhg {font-size:60%;} +.desc {font-size:90%;} +.time {font-size:80%;} +.action {} +.nick {} +</style> +</head> +<body> +HTML +} + + +##Queryable items: +## nick, user, host, realip, gecos, account +my $qry = 'SELECT * FROM actionlog WHERE '; + +if (defined($data{nick}) && ($data{nick} ne "*") && ($data{nick} ne "")) { + $qry .= " nick like " . esc($data{nick}) . ' or '; +} + +if (defined($data{user}) && ($data{user} ne "*") && ($data{user} ne "")) { + $qry .= ' user like ' . esc($data{user}) . ' or '; +} + +if (defined($data{host}) && ($data{host} ne "*") && ($data{host} ne "")) { + $qry .= ' host like ' . esc($data{host}) . ' or '; +} + +if (defined($data{gecos}) && ($data{gecos} ne "*") && ($data{gecos} ne "")) { + $qry .= ' gecos like ' . esc($data{gecos}) . ' or '; +} + +if (defined($data{account}) && ($data{account} ne "*") && ($data{account} ne "")) { + $qry .= ' account like ' . esc($data{account}) . ' or '; +} + +if (defined($data{realip}) && ($data{realip} =~ /^\d+\.\d+\.\d+\.\d+$/)) { + $qry .= ' ip = ' . dottedQuadToInt($data{realip}) . ' or '; +} + +$qry .= '(1 = 0)'; # rather than trying to get rid of a trailing 'or ' + +if ($debug) { + print "Querying: "; + print Dumper($qry); +} + +my $sth = $dbh->prepare($qry); +$sth->execute; +my $names = $sth->{'NAME'}; +my $numFields = $sth->{'NUM_OF_FIELDS'}; + +#fields are index,time,action,reason,channel,nick,user,host,ip,gecos,account,bynick,byuser,byhost,bygecos,byaccount +my %f = ( + "index" => 0, + "time" => 1, + "action" => 2, + "reason" => 3, + "channel" => 4, + "nick" => 5, + "user" => 6, + "host" => 7, + "ip" => 8, + "gecos" => 9, + "account" => 10, + "bynick" => 11, + "byuser" => 12, + "byhost" => 13, + "bygecos" => 14, + "byaccount" => 15 +); + +print "<table border=\"0\">" unless $debug; +if ($debug) { + for (my $i = 0; $i < $numFields; $i++) { + printf("%s%s", $i ? "," : "", $$names[$i]); + } +} +#print "</tr>" unless $debug; +print "\n"; + +while (my $ref = $sth->fetchrow_arrayref) { +#fields are index,time,action,reason,channel,nick,user,host,ip,gecos,account,bynick,byuser,byhost,bygecos,byaccount + unless ($debug) { + print '<tr>'; + print '<td><a href="logs.pl?index=' . $$ref[$f{'index'}] . '" class="index">#' . $$ref[$f{'index'}] . ':</a></td>'; + print '<td nowrap="nowrap" class="time">' . $$ref[$f{'time'}] . '</td>'; + + print '<td nowrap="nowrap"><span class="nick">' . $$ref[$f{'nick'}] . '</span>'; + print '<span class="uhg">!' . $$ref[$f{'user'}] . '@' . $$ref[$f{'host'}] . ' (' . $$ref[$f{'gecos'}] . ')'; + print ' [' . $$ref[$f{'account'}] . ']' if ($$ref[$f{'account'}] ne ''); + print '</span>'; + print ' <span class="desc">received</span> <span class="action">' . $$ref[$f{'action'}] . '</span>'; + print ' (' . $$ref[$f{'reason'}] . ')' if ($$ref[$f{'reason'}] ne ''); + print ' <span class="desc">on</span> ' . $$ref[$f{'channel'}] if ($$ref[$f{'channel'}] ne ''); + print ' '; +# print '</td>'; +# print '<td>'; + if ($$ref[$f{'bynick'}] ne '') { + print '<span class="desc">by</span> ' . $$ref[$f{'bynick'}]; + print '<span class="uhg">!' . $$ref[$f{'byuser'}] . '@' . $$ref[$f{'byhost'}] . ' (' . $$ref[$f{'bygecos'}] . ')'; + print ' [' . $$ref[$f{'byaccount'}] . ']' if ($$ref[$f{'byaccount'}] ne ''); + print '</span>'; + } + print '</td>'; + print '</tr>'; + } else { + for (my $i = 0; $i < $numFields; $i++) { + printf("%s%s", $i ? "," : "", $$ref[$i]); + } + } + print "\n"; +} +unless ($debug) { + print "</table></body></html>"; +} diff --git a/cgi-bin/secret/logs.pl b/cgi-bin/secret/logs.pl new file mode 100755 index 0000000..419f450 --- /dev/null +++ b/cgi-bin/secret/logs.pl @@ -0,0 +1,34 @@ +#!/usr/bin/perl + +#use warnings; +use Data::Dumper; +use strict; +use DBI; + +use CGI_Lite; +my $cgi = new CGI_Lite; +my %data = $cgi->parse_form_data; +my $index = $data{index}; +print "Content-type: text/plain", "\n\n"; +if ( !defined($index) ) { + print "Nice hax!\n"; + exit 0; +} +$index = int $index; + +if ( $index < 50000) { + my $block; + $block = "50K" if $index < 50000; + $block = "40K" if $index < 40000; + $block = "30K" if $index < 30000; + $block = "20K" if $index < 20000; + $block = "10K" if $index < 10000; + print "tar -Oxf /var/www/actionlogs/$block.tar.gz $index.txt\n\n"; + print `tar -Oxf /var/www/actionlogs/$block.tar.gz $index.txt`; +} elsif ( -e "/var/www/actionlogs/$index.txt.lzma" ) { + print `lzcat /var/www/actionlogs/$index.txt.lzma`; +} elsif ( -e "/var/www/actionlogs/$index.txt" ) { + print `cat /var/www/actionlogs/$index.txt`; +} else { + print "u wot m8?\n"; +} diff --git a/cgi-bin/showUsers.pl b/cgi-bin/showUsers.pl new file mode 100755 index 0000000..913ee22 --- /dev/null +++ b/cgi-bin/showUsers.pl @@ -0,0 +1,75 @@ +#!/usr/bin/perl + +#use warnings; +use Data::Dumper; +use strict; +use DBI; +use XML::Simple qw(:strict); + + +print "Content-type: text/html", "\n\n"; +print <<HTML; +<html> + <head> + <title>AntiSpamMeta User List</title> + </head> + <body> + <h3>Maintaining AntiSpamMeta takes work! Please +<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"> +<input type="hidden" name="cmd" value="_s-xclick"> +<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----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-----END PKCS7----- +"> +<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"> +<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1"> +</form></h3> +<table> +<tr><th>NickServ account</th><th>h</th><th>s</th><th>a</th><th>d</th></tr> +HTML + +my $xs1 = XML::Simple->new( KeyAttr => ['id'], Cache => [ qw/memcopy/ ]); +my $users = $xs1->XMLin( "/home/icxcnika/AntiSpamMeta/config-main/users.xml", ForceArray => 'person'); + +sub printout +{ + my ($user) = @_; + print "<tr><td style=\"text-align:right\">$user</td>"; + print "<td>"; + print "x" if (index($users->{person}->{$user}->{flags}, 'h') != -1); + print "</td>"; + print "<td>"; + print "x" if (index($users->{person}->{$user}->{flags}, 's') != -1); + print "</td>"; + print "<td>"; + print "x" if (index($users->{person}->{$user}->{flags}, 'a') != -1); + print "</td>"; + print "<td>"; + print "x" if (index($users->{person}->{$user}->{flags}, 'd') != -1); + print "</td>"; + print "</tr>\n"; +} + +foreach my $user (keys %{$users->{person}}) { + if (index($users->{person}->{$user}->{flags}, 'd') != -1) { + printout($user); + delete $users->{person}->{$user}; + } +} +foreach my $user (keys %{$users->{person}}) { + if (index($users->{person}->{$user}->{flags}, 'a') != -1) { + printout($user); + delete $users->{person}->{$user}; + } +} +foreach my $user (keys %{$users->{person}}) { + if (index($users->{person}->{$user}->{flags}, 's') != -1) { + printout($user); + delete $users->{person}->{$user} + } +} + +foreach my $user (keys %{$users->{person}}) { + printout($user); +} +print "</table></body></html>"; + +exit 0; diff --git a/checks.txt b/checks.txt new file mode 100644 index 0000000..fed5951 --- /dev/null +++ b/checks.txt @@ -0,0 +1,28 @@ +Overall alert system workings: +There are 6 alert levels. In order of least significant -> most significant, they are: + +debug: New rules that are in need of testing. These can be extremely useful, or extremely spammy. +info: This is not meant to alert about a malicious action, rather to be a "heads up, keep an eye out because..." +low: These are various rules that may have some false positives. The importance of response time to these varies. +medium: These are rules that are not likely to be a false positive, and very likely mean the channel should receive immediate attention. +high: These are rules with a 99% certainty that a user is attempting to be malicious. +"opalert": The bot will never show an "opalert risk threat", rather, this level indicates who to ping when someone calls !ops + +A user set to be pinged for any given level will be pinged for all higher levels. +For example, if a user is set to be pinged for "low", they'll be pinged for "opalert", but not "info". + +In no particular order, the bot: + +* Checks for what it thinks is a botnet cycling in a channel to spam +* Checks for nicks that join just to spam something and then leave +* Checks for various kinds of flooding - even distributed over multiple nicks - and has anti-anti-detection measures +* Checks for ascii-art pasting +* Checks channel messages against a large array of blacklisted strings +* Detects several IRC exploits +* Detects channel-ctcps (mostly deprecated thanks to cmode +C) +* Detects channel-notices +* Checks channel messages against a few regexes that are always spammy +* Detects some phishing attempts +* Detects some types of attempted ban evasion +* Detects some malicious shorturls + diff --git a/index.html b/index.html new file mode 100644 index 0000000..696fabe --- /dev/null +++ b/index.html @@ -0,0 +1,37 @@ +<html><head><title>AntiSpamMeta homepage</title> +</head> +<body> + +<p><h4>AntiSpamMeta is written in my free time. Feel free to give me more monies.<br /> +<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top"> +<input type="hidden" name="cmd" value="_s-xclick"> +<input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----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-----END PKCS7----- +"> +<input type="image" src="https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"> +<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1"> +</form> +<h4>If you want, I'll even put your name or nick up here as extra thanks.<br /> +Thanks is also due to dwfreed, who helps me with some of the coding,<br /> +WildPikachu, who has provided reliable servers for ASM since its inception,<br /> +DLange, for hosting domain/DNS things,<br /> +and tomaw, who puts up with a lot of my crap.</h4> +</p> +<hr /> +<p><h2>About AntiSpamMeta:</h2> +ASM was born on about June 14th, 2006.<br /> +It started out as "AntiSpamBot", a supybot plugin for some rudimentary spam detection.<br /> +It only monitored ##linux, which at the time was a constant target for spam and troll groups.<br /> +However, it quickly outgrew supybot's API, as well as needing to be able to run and process things *much* faster.<br /> +In response to this, the supybot was ditched entirely, and it was re-written using the (legacy) Net::IRC perl framework.<br /> +Despite being a decade old, the framework has proven to be very reliable and only needed a few small changes over time.<br /> +On February 28, 2012, AntiSpamMeta was officially recognized as a freenode FOSS project, and moved base from ##asb-nexus, to #antispammeta.<br /> +As of April 22, 2014, the bot keeps state tracking data for the 58 channels it is in, and the ~11,000 nicks it sees.<br /> +As of September 16, 2015, the bot is in 91 channels, sees ~13,200 nicks, and uses around 50MB of ram.<br /> +It monitors nearly every large channel on freenode, but is still just a client-connection, run as a side project (i.e. not a part of the freenode infrastructure). +</p> +<p>Looking for someone to bug that has a specific amount of access? <a href="/cgi-bin/showUsers.pl">Look no further!</a></p> +<p>For syntax / command info, go <a href="syntax.txt">here.</a></p> +<p>You can find a general idea of what AntiSpamMeta looks for <a href="checks.txt">here</a>.</p> +<p><a href="https://gitlab.devlabs.linuxassist.net/asm/antispammeta/">Source code can be found here.</a></p> + +</body></html> diff --git a/investigate.html b/investigate.html new file mode 100644 index 0000000..e5f0694 --- /dev/null +++ b/investigate.html @@ -0,0 +1,18 @@ +<html> + <head> + <title>AntiSpamMeta database query page</title> + </head> + <body> + <p>Matching is done based on field1 OR field2 OR field3 etc. Wildcards are supported, +except for the realIP field, which must be blank or an IPv4 dotted quad.</p> + <form action="/cgi-bin/secret/investigate.pl" method="get"> + Nickname: <input type="text" name="nick" /> + User: <input type="text" name="user" value="" /> + Hostname: <input type="text" name="host" value="" /><br /> + Gecos: <input type="text" name="gecos" value="" /><br /> + Account: <input type="text" name="account" value="" /><br /> + Real IP: <input type="text" name="realip" value="" /><br /> + <br /><br /><input type="submit" value="Query!" /> + </form> + </body> +</html> diff --git a/query.html b/query.html new file mode 100644 index 0000000..74f98ff --- /dev/null +++ b/query.html @@ -0,0 +1,50 @@ +<html> + <head> + <title>AntiSpamMeta database query page</title> + </head> + <body> + <form action="cgi-bin/query.pl" method="get"> + Channel: <input type="text" name="channel" value="*" /><br /> + Nickname: <input type="text" name="nick" value="" /> + User: <input type="text" name="user" value="" /> + Hostname/IP: <input type="text" name="host" value="" /><br /> + Gecos: <input type="text" name="gecos" value="" /><br /> + <input type="checkbox" name="since" value="1">Since time:</input> + <input type="text" name="syear" value="1990" maxlength="4" />- + <input type="text" name="smonth" value="07" maxlength="2" />- + <input type="text" name="sday" value="28" maxlength="2" />- + <input type="text" name="shour" value="00" maxlength="2" />: + <input type="text" name="smin" value="00" maxlength="2" />: + <input type="text" name="ssec" value="00" maxlength="2" /><br /> + <input type="checkbox" name="before" value="1">Before time:</input> + <input type="text" name="byear" value="2007" maxlength="4" />- + <input type="text" name="bmonth" value="06" maxlength="2" />- + <input type="text" name="bday" value="20" maxlength="2" />- + <input type="text" name="bhour" value="20" maxlength="2" />: + <input type="text" name="bmin" value="02" maxlength="2" />: + <input type="text" name="bsec" value="58" maxlength="2" /><br /> + Threat level: <select name="level"> + <option value="any" selected="selected">any</option> + <option value="debug">debug</option> + <option value="info">info</option> + <option value="low">low</option> + <option value="medium">medium</option> + <option value="high">high</option> + </select> + Threat reason:<sup>1</sup> <input type="text" name="reason" value="*" /><br /> + Sort by: <select name="sort"> + <option value="time">time</option> + <option value="nick">nick</option> + <option value="user">user</option> + <option value="host">host</option> + <option value="level" selected="selected">threat level</option> + <option value="id">threat id</option> + <option value="reason">threat reason</option> + </select> <select name="order"> + <option value="a" selected="selected">ascending</option> + <option value="d">descending</option> + </select> + <br /><br /><input type="submit" value="Query!" /> + </form> + </body> +</html> diff --git a/syntax.txt b/syntax.txt new file mode 100644 index 0000000..0eecddf --- /dev/null +++ b/syntax.txt @@ -0,0 +1,175 @@ +List of ASM's commands. + +!ops <optional:#channel> <optional: message to send> + Generates an alert, and includes the message in that alert. So, via channel message or PM, + you can do "!ops Hey Ops you need to see this" or "!ops #spammychannel bad stuff goin on" + or "!ops" or "!ops #spammychannel". You have to be on the channel in order to call this function, + and there are some stopgaps against abuse. + +;teredo <IPv6 IP> + This will "unmask" an IPv4 connection pretending to be IPv6 - it will report the user's "real" IP. + Use if their IP starts with "2001:0:" + +;status + Reports the bot's memory/cpu/network usage and more. + +;source + Responds with a URL to the bot's source + +;help + Refers you to ASM's website + +;ping + Replies "pong". + +*** These are commands used for check to see if a user has a "bad past" *** + +;db + Returns a link to a page where you can query ASM's alertlog database. + +;query <optional:channel> <nick!user@host> + nick/user/host takes standard wildcards (e.g. AfterDeat*!*@*). + If channel is not specified, checks against all channels. + Responds with how many matches there are for this in ASM's alertlog database. + +;investigate <nick> + <nick> MUST BE A NICK THAT ANTISPAMMETA CAN SEE. + Returns with the number of how many "incidents", e.g. quiets/kills/klines/bans/removes/etc + against that person that ASM is aware of. + +;investigate2 <nick> <optional:skip> Restricted to flag "s" + Like ;investigate, except ASM will PM you the details of the most recent 10 incidents, + unless you provide a skip number, in which case it will skip the first X incidents. + There are index numbers attached to the lines, which correspond with logfiles that may + explain why said person got banned/klined/whatever. These logfiles are stored in a password- + protected directory. See AfterDeath/the url provided/;;addwebuser for access. + +;mship <nick> Restricted to flag "s" + Reports channels that both ASM and <nick> is on. + +;;addwebuser <password> Restricted to flag "s" + THIS COMMAND MUST BE SENT IN PM, WITH TWO SEMICOLONS. + This will give you access to the restricted areas of ASM's database, using an http login of + [yourIRCnick] and [password]. + No assurance of privacy is made with regards to your password. Make it secure, so that someone + else won't use your login, and make it unique - i.e. not something you use for /ns identify. + + + +*** These commands are for managing who has access to the bot *** + +;userx add <nickserv account> <flags> Restricted to flag "a" + Adds <nickserv account> to ASM's config and gives them the specified flags. + BE SURE to use the person's nickserv account, WHICH IS NOT NECESSARILY the nick they're using. + /whois them to be sure. You can't give them a flag you don't have yourself, and for extra security, + you can't ever give them the 'd' flag - this flag has to be assigned via manual config edit. + This means if someone has the 'd' flag, they can't have their flags changed to a new set that + still includes 'd' without a manual edit. + +;userx flags <nickserv account> + Shows what flags that nickserv account has access to. + +;userx flags <nickserv account> <flags> Restricted to flag "a" + Sets the flags for that nickserv account - NOT NECESSARILY THE SAME AS THEIR NICK - to the + provided set of flags. Once again, you can't give flags you don't already have, and you can't + ever give the 'd' flag. + +;userx del <nickserv account> Restricted to flag "a" + Removes the nickserv account from ASM's list of authorized users. + + + +*** These commands are for managing who/where the bot notifies of bad stuff *** + +;target <source channel> <target channel or nick> <optional:level> Restricted to flag "a" + Adds target to the list of places notified for source channel, with the optional level. + So, to send low-risk alerts and above concerning #spammychannel to #opschannel, you'd do + ;target #spammychannel #opschannel info + If a level isn't specified, it defaults to "debug". + +;detarget <source channel> <target channel> Restricted to flag "a" + Stops sending messages concerning <source channel> to <target channel>. + +;showhilights <nick> Restricted to flag "h" + shows all the channels that it's configured to hilight <nick> on, and what level it's for. + +;hilight <channel> <nick><optional:,nick2,nick3> <optional:level> Restricted to flag "h" + Adds <nick[s]> to the list of hilights for <channel> for risks of <level> and above. + If <level> is unspecified, it defaults to "info". + +;dehilight <channel> <nick><optional:,nick2,nick3> Restricted to flag "h" + Removes the list of nicks from the list of hilights for <channel>. + + +*** Blacklist management commands *** + +;blacklist <string> Restricted to flag "s" + Adds <string> to the list of strings ASM will watch out for. No pattern matching or anything + like that is done, although it is case insensitive. Please for the love of everything that + is holy use common sense with this. Don't blacklist "nigger" or something like that... + but definitely blacklist "http://spammyurl.com" etc. + +;blreason <id> <reason> Restricted to flag "s" + use the ID returned by ;blacklist to set a reason for why you blacklisted it. + +;unblacklist <id> Restricted to flag "s" + fix an oopsie + +;bllookup <id> Restricted to flag "s" + Looks up the blacklisted string represented by a given ID and sends details in PM. + + +*** Administrative commands *** + +;join <channel> Restricted to flag "a" + Tells the bot to join said channel. + +;part <channel> Restricted to flag "a" + Tells the bot to part said channel. + +;rehash Restricted to flag "a" + Re-reads the string blacklist files and config files etc etc. + +;monitor <chan> (yes|no) Restricted to flag "a" + Sets whether or not the channel is monitored for spam stuffs. + This is obviously "yes" by default, but some places turn it off for their #blah-ops channel. + +;silence <chan> (yes|no) Restricted to flag "a" + Sets whether or not ASM ignores commands (other than !ops) in the channel. + "no" by default; public channels often want this. + +;quit <reason> Restricted to flag "a" + Quits IRC using reason as the quit message. ASM is run in a while loop, so it'll probably + come right back. + +;sync <channel> Restricted to flag "a" + sends data to gather information about <channel>, in case a sync failed and needs to be + manually forced. + +;restrict (nick|account|host) <nick/acct/host> (+|-)<restriction> Restricted to flag "a" + Adds a restriction for the specified nick or nickserv account or hostname. + Useful restrictions are: + notrigger - prevents the target from causing the bot to generate a spam alert. + nomsgs - don't relay private messages from target to the master channel + nocommands - prevent target from sending the bot commands. + + +*** Highly restricted debug commands *** + +;sql (main|log) <somestring> Restricted to flag "d" + No idea what this does right now. + +;sl <line> Restricted to flag "d" + Sends <line> to the IRC server. + +;ev <code> Restricted to flag "d" + Evaluates raw perl code - output is sent to STDOUT, not to IRC. + + +*** Plugin interface *** + +;plugin <chan> <risk> <reason> Restricted to flag "p" + This is used so another bot can have ASM generate alerts. E.G. + <magicbot> ;plugin #spammychannel debug Magic Bad Stuff Detection Algorithm 1 -> + <AntiSpamMeta> debug risk threat [#spammychannel] - (magicbot plugin) - Magic Bad Stuff + Detection Algorithm 1 - ping lotsofpeople |
