#!/usr/local/bin/tops -s /usr/local/tops/sys -u /opt/mytops/usr/
{
  File tops_deny

  Dale R. Williamson  November 2004

  This script runs a watchdog to sense failed ssh passwords in file
  /var/log/auth.log, and append new IP entries to file /etc/hosts.deny.

  File names used:
      /etc/hosts.deny
      /var/log/auth.log
  and patterns sought in /var/log/auth.log:
      "Did not receive identification string from"
      "Failed password"
      "Could not reverse map address"
  are for Debian GNU/Linux.  They may differ for other systems or
  other versions of ssh.

  For sample operation, see below: "Watchdog in action."
}
#-----------------------------------------------------------------------

#  Source user words:

   CATMSG push no catmsg

   "dir_watch" missing IF "dog.v" source THEN

#  This word watches files in directory named DIR which is defined
#  below in the library of word AUTH.LOG:

   inline: AUTH.LOG (hFnew hFgone --- ) \ track /var/log/auth.log
{     This is the "Action" word for a dir_watch word (file dog.v).

      Requires file /etc/hosts.deny.template, containing just the
      top comments of file /etc/hosts.deny, and /etc/hosts.deny.orig,
      which is /etc/hosts.deny before this process is started.

      Optional file AUTH.ALLOW at usrpath holds allowed IP addresses,
      so if you make an error logging in you won't be locked out.

      Doing the following:
         Watching files like /var/log/auth.log for patterns
         Writing IP addresses to be refused to file /etc/hosts.deny
         Logging activity to the LOG file opened below
}
      [ ontheweb
        IF 
         \ Watching this directory:
           "/var/log/" "DIR" book

         \ Watching these files:
           DIR "auth.log" + "AUTH" book
           DIR "user.log" + "USER" book \ see below, "Missed changes"
        ELSE 
         \ Watching this directory:
           "/var/log/" "DIR" book

         \ Watching these files:
           DIR "secure" + "AUTH" book
           "NOT_USED"     "USER" book 
        THEN

        "" "T2save" book

        "plunger diego riggo" words "MACHINES" book
      ]
{
      ontheweb not
      IF www_open not \ exit if no connection to Internet:
         IF " AUTH.LOG: " nl . date .
            " no Internet connection;" . nl " program will exit" . nl
            5 (seconds) "exit" ALARM (hFnew hFgone) 2drop return
         THEN
      THEN
}
      (hFgone) AUTH grepr rows any
      IF (hFnew) drop return THEN

      (hFnew) dup USER grepr rows any swap
      (hFnew) AUTH grepr rows any or
      IF AUTH file? not IF return THEN

         AUTH asciiload any? not IF return THEN (hAuth)
         "" "T2" book

       \ Case 1 pattern to deny:
         "case 1" ERRset
         (hAuth) dup dup "Did not receive identification string from"
         grepr reach any?
         IF (hFailed) " XXX" tail "from" "XXX" between
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 1a pattern to deny (added Jan 10, 2008):
         "case 1a" ERRset
         (hAuth) dup dup "Illegal user"
         grepr reach any?
         IF (hFailed) " XXX" tail "from" "XXX" between
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 2 pattern to deny:
         "case 2" ERRset
         (hAuth) dup dup "Failed password"
         grepr reach any?
         IF (hFailed) "from" "port" between
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 2a pattern to deny (added Jan 10, 2008):
         "case 2a" ERRset
         (hAuth) dup dup "Failed unknown for illegal user"
         grepr reach any?
         IF (hFailed) "from" "port" between
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 3 pattern to deny:
       \    appears as: Could not reverse map address 220.73.136.254.
       \    (note ending . which is eliminated using .XXX below)
         "case 3" ERRset
         (hAuth) dup dup "Could not reverse map address"
         grepr reach any?
         IF (hFailed) "XXX" tail "address" ".XXX" between (note .XXX)
	    noblanklines
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 4 pattern to deny (added Jan 18, 2008):
       \   Appears as:
       \    error: PAM: Authentication failure for root from 80.81.17.18
         "case 4" ERRset
         (hAuth) dup dup "Authentication failure for"
         grepr reach any?
         IF (hFailed) "XXX" tail "from" "XXX" between
	    noblanklines
            T2 swap pile "T2" book
         THEN
         ERR


       \ Case 5 pattern to deny (added May 20, 2008):
       \   Appears as:
       \    fatal: Timeout before authentication for 125.214.66.212.
       \    (note ending . which is eliminated using .XXX below)
         "case 5" ERRset
         (hAuth) dup dup "fatal: Timeout before authentication"
         grepr reach any?
         IF (hFailed) "XXX" tail "authentication for" ".XXX" between
            noblanklines
            T2 swap pile "T2" book
         THEN
         ERR

       \ Case 6 pattern to deny (added June 30, 2008):
       \   Appears as:
       \      error: PAM: User not known ... from 162.red-80-35-241
         "case 6" ERRset
         (hAuth) dup dup "error: PAM: User not known"
         grepr reach any?
         IF (hFailed) "XXX" tail "from" "XXX" between
	    noblanklines
            T2 swap pile "T2" book
         THEN
         ERR

         (hAuth) drop

         T2 noq_alike noblanklines "T2" book \ remove duplicates

       \ Two ways to allow IPs: 1) file mytops/AUTH.ALLOW and 2) list
       \ MACHINES defined above:

         usrpath "AUTH.ALLOW" catpath this file?
         IF (qFile) asciiload chop noblanklines ELSE "" THEN (hALLOW)

       \ Wed Mar  5 01:32:35 UTC 2014
       \ Allow my remote machines, for cases like SSH catching "Did not
       \ receive identification string" when connection is bad and I 
       \ have given up (Ctrl+C) at the ssh client end.  

       \ These machines always register their IPs on the public message
       \ file: 
         (hALLOW) MACHINES rows 1st 
         DO MACHINES I quote msgPub msgPeep (hALLOW hM) pile LOOP 
         noblanklines noq_alike "ALLOW" book

         ALLOW rows 0>
         IF list: ALLOW rows 1st
               DO T2 ALLOW I quote strchop grepr any? IF ontop THEN LOOP
            end (hRows) any?
            IF T2 swap (hRows) those rows teeth rake lop "T2" book THEN
         ELSE (qFile) drop
         THEN

         T2 T2save = IF return THEN \ return if no change from previous

         T2 chars any
         IF "/etc/hosts.deny.template" asciiload (hHead)

            "/etc/hosts.deny" this file?
            IF asciiload these 1st four items catch
               "ALL:" grepe reach chop (hT1)
            ELSE drop ""
            THEN (hT1)

            (hT1) any? not
            IF \ use .orig if empty VOL:
               "/etc/hosts.deny.orig" asciiload
               these 1st four items catch
               "ALL:" grepe reach chop (hT1)
            THEN (hT1)

            (hHead hT1)

          \ Formatting new T2 IP addresses into form "ALL: XXX.YYY.ZZZ."
          \ Prepend string "ALL:" and remove last number in IP address,
          \ but leave an ending dot:

          \ Some sites have this form: ::ffff:207.91.46.139.  Remove
          \ the colon and letters:
            T2 "::ffff:" "" strp strchop left justify (hT2) "T2" book

            T2 "ALL: " nose (hT2)
            (hT2) backward left justify "." tug backward chop (hT2)

          \ Combine T1 from hosts.deny with new T2:
            (hT1 hT2) pile noq_alike noblanklines (hT)
            (hHead hT) pile (hT)

          \ Write new file hosts.deny:
            (hT) "/etc/hosts.deny" save

          \ Write to LOG file opened below:
            nl  
            "AUTH.LOG: " date + . nl
            "AUTH.LOG: IPs from auth.log that will be denied:" . nl
            T2 three indent . nl
            T2 "T2save" book

         THEN
      THEN
   end

#-----------------------------------------------------------------------
{
   Make a directory-watch word called D1, and bank the ptr to word
   AUTH.LOG into D1 as its Action word, so AUTH.LOG will be run when
   files in AUTH.LOG(DIR) change.
}
\  Making watchdog word called D1:
   "/tmp" "AUTH.LOG" "DIR" yank "D1" dir_watch

\  When D1 was just made, network dT was set.  Make the setting 0
\  to avoid time mismatches that can cause files not be recoginzed
\  as recently changed:
   0 "D1" "dT" bank 

\  Note that output from D1 will go to DGLOG defined in dog.v, word
\  dir_watch(), while output from Action word AUTH.LOG will go to LOG 
\  defined below.

   "AUTH.LOG" ptr "D1" "Action" bank \ action of D1 is to run AUTH.LOG

   pull catmsg

   keys? IF halt THEN \ interactive testing, cannot run daemon server

#  SYSOUT must be defined for daemon input and output.  Below, a log
#  file is defined for SYSOUT using word set_sysout.

#  Vector output to this log file:
   "HOME" env "tops_deny.log" catpath "LOG" book LOG set_sysout

#  Display first lines in the log file:
   "-" 80 cats nl dot nl
   "PID " getpid int$ cat spaced date cat dot nl

   1 (Hz) "D1" PLAY \ times a second

   host "plunger" = IF 5 "NIST_DELTA" ELSE 5 "NIST_SYNC" THEN ALARM

#-----------------------------------------------------------------------
{
   Start the daemon server, running forever.

   The daemon running as a server makes it handy for maintenance from
   outside using word remoteprompt.

   Word remoteprompt allows interactive work right on the stack of the
   running daemon.  New word AUTH.LOG can be tested and its code updated
   without ever stopping and restarting the daemon.
}
   "*" def_port nextport DSERVER

#-----------------------------------------------------------------------

;  Appendix

Watchdog in action.

1. At :37, connect allowed.
Entry in file /var/log/daemon.log:
Nov 15 13:51:37 umlcoop sshd[29484]: connect from 218.234.22.148 (218.234.22.148)

2. At :38, bogus user named "test" and bad password.
Entries in file /var/log/auth.log:
Nov 15 13:51:38 umlcoop sshd[29484]: Illegal user test from 218.234.22.148
Nov 15 13:51:38 umlcoop sshd[29484]: error: Could not get shadow information for NOUSER
Nov 15 13:51:38 umlcoop sshd[29484]: Failed password for illegal user test from 218.234.22.148 port 55408 ssh2

3. At :38, AUTH.LOG Case 2 matches "Failed password" in file /var/log/auth.log and adds 218.234.22.148
to file /etc/hosts.deny.
This shows entries in file /tmp/dirwatch.log made by AUTH.LOG:
Mon Nov 15 13:51:38 PST 2004 D1: dir_watch files new or changed:
   /var/log/auth.log
AUTH.LOG: IPs from auth.log that will be denied:
   80.20.205.240
   210.239.139.54
   61.218.77.59
   211.21.15.42
   221.239.18.212
   212.23.129.171
   218.24.205.20
   61.220.144.84
   67.136.246.84
   203.98.145.104
   81.193.117.99
   24.203.246.197
   203.131.105.230
   70.71.4.190
   218.234.22.148 <<<<<<<<<<<<< another IP address to deny

4. At :39, connection refused--stopped at the door, no more chances to submit passwords.
Entry in file /var/log/daemon.log:
Nov 15 13:51:39 umlcoop sshd[29486]: refused connect from 218.234.22.148 (218.234.22.148)

------------------------------------------------------------------------------------------------------

Watchdog in action; here is the same game:

1. Connect (/var/log/daemon.log):
Nov 15 18:49:59 umlcoop sshd[30229]: connect from nbrypk-cuda1-c1a-128-82.vnnyca.adelphia.net (67.22.128.82)

2. No identification string (/var/log/auth.log):
Nov 15 18:49:59 umlcoop sshd[30229]: Did not receive identification string from 67.22.128.82

3. AUTH.LOG Case 1 matches "Did not receive identification", and IP address is added to list in file
/etc/hosts.deny.
Showing text from /tmp/dirwatch.log:
Mon Nov 15 18:49:59 PST 2004 D1: dir_watch files new or changed:
   /var/log/syslog
   /var/log/auth.log
   /var/log/daemon.log
AUTH.LOG: IPs from auth.log that will be denied:
   80.20.205.240
   210.239.139.54
   61.218.77.59
   211.21.15.42
   221.239.18.212
   212.23.129.171
   218.24.205.20
   61.220.144.84
   67.136.246.84
   203.98.145.104
   81.193.117.99
   67.22.128.82  <<<<<<<<<<<<< another IP address to deny
   24.203.246.197
   203.131.105.230
   70.71.4.190
   218.234.22.148

4. Refused on next attempt about 90 minutes later (/var/log/daemon.log):
Nov 15 20:03:11 umlcoop sshd[30232]: refused connect from nbrypk-cuda1-c1a-128-82.vnnyca.adelphia.net (67.22.128.82)

------------------------------------------------------------------------------------------------------

   Notes on permissions.

   For non-root user, permissions like these are required, where comm
   is a group to which the non-root user running this daemon belongs:

      [riggo@umlcoop] / > ll
      drwxr-xr-x  45 root comm   4096 Nov 14 07:55 etc/
      drwxr-xr-x  14 root comm   4096 Aug 29 04:18 var/

      [riggo@umlcoop] /var > ll
      drwxr-xr-x   5 root comm  4096 Nov 14 08:34 log/

      [riggo@umlcoop] /etc > ll hosts.d*
      -rw-rw----  1 root comm 1062 Oct 14 13:37 hosts.deny
      -rw-rw----  1 root comm 1062 Oct 14 13:37 hosts.deny.orig
      -rw-rw----  1 root comm  910 Nov 13 23:41 hosts.deny.template

      [riggo@umlcoop] /var/log > ll auth.log
      -rw-r-----  1 root comm 3672 Nov 14 09:41 auth.log

   But it was discovered that a non-root user has a problem when logs
   are rotated on Sunday, because the new auth.log file will revert
   to adm group and the file cannot be read by group comm:
      [riggo@umlcoop] /var/log > ll auth.log
      -rw-r-----  1 root adm  66061 Dec  5 06:47 auth.log

   It is easier just to have root run this daemon.

#-----------------------------------------------------------------------

   Missed changes to file.

   Here is a case where change in auth.log was missed.  Since the change
   is correlated with user.log, user.log is added to the list of files
   that trigger processing in AUTH.LOG.

      Fri Nov 19 00:33:30 PST 2004 D1: dir_watch files new or changed:
         /var/log/syslog
         /var/log/daemon.log
         /var/log/user.log

   It is not known why changes to auth.log were missed.  Here is 
   auth.log during this period:

      Nov 19 00:33:30 umlcoop sshd[30517]: Illegal user test from 148.244.79.145
      Nov 19 00:33:30 umlcoop sshd[30518]: input_userauth_request: illegal user test
      Nov 19 00:33:30 umlcoop sshd[30517]: reverse mapping checking getaddrinfo for umad.edu.mx failed - POSSIBLE BREAKIN ATTEMPT!
      Nov 19 00:33:30 umlcoop sshd[30517]: error: Could not get shadow information for NOUSER
      Nov 19 00:33:30 umlcoop sshd[30517]: Failed password for illegal user test from 148.244.79.145 port 2079 ssh2
      Nov 19 00:33:31 umlcoop sshd[30519]: Illegal user guest from 148.244.79.145
      Nov 19 00:33:31 umlcoop sshd[30520]: input_userauth_request: illegal user guest
      Nov 19 00:33:31 umlcoop sshd[30519]: reverse mapping checking getaddrinfo for umad.edu.mx failed - POSSIBLE BREAKIN ATTEMPT!
      Nov 19 00:33:31 umlcoop sshd[30519]: error: Could not get shadow information for NOUSER
      Nov 19 00:33:31 umlcoop sshd[30519]: Failed password for illegal user guest from 148.244.79.145 port 2113 ssh2

   About 40 seconds later, /var/log/user.log was reported changed in
   this entry to /tmp/dirwatch.log:
      Fri Nov 19 00:34:10 PST 2004 D1: dir_watch files new or changed:
         /var/log/user.log

   But there were no changes to user.log at 00:34:10, so the time 00:34:10
   must be delayed.  These are the entries in /var/log/user.log that 
   correlate with entries in auth.log:

      Nov 19 00:33:30 umlcoop sshd: warning: /etc/hosts.allow, line 14: can't verify hostname: getaddrinfo(umad.edu.mx, AF_INET) failed
      Nov 19 00:33:31 umlcoop sshd: warning: /etc/hosts.allow, line 14: can't verify hostname: getaddrinfo(umad.edu.mx, AF_INET) failed
      Nov 19 00:53:19 umlcoop sshd: warning: /etc/hosts.allow, line 14: can't verify hostname: getaddrinfo(umad.edu.mx, AF_INET) failed


   It is concluded that something hung things up from 00:33:31 when
   user.log shows an entry, and 00:34:10 when dirwatch.log says usr.log
   changed (about 39 seconds).


   Finally at 00:53 these changes to /var/log/auth.log were caught:

      Nov 19 00:53:20 umlcoop sshd[30521]: Illegal user test from 148.244.79.145
      Nov 19 00:53:20 umlcoop sshd[30522]: input_userauth_request: illegal user test
      Nov 19 00:53:20 umlcoop sshd[30521]: reverse mapping checking getaddrinfo for umad.edu.mx failed - POSSIBLE BREAKIN ATTEMPT!
      Nov 19 00:53:20 umlcoop sshd[30521]: error: Could not get shadow information for NOUSER
      Nov 19 00:53:20 umlcoop sshd[30521]: Failed password for illegal user test from 148.244.79.145 port 2562 ssh2

    and /etc/hosts.deny was updated.  Here are the entries from /tmp/dirwatch.log:

      Fri Nov 19 00:53:19 PST 2004 D1: dir_watch files new or changed:
         /var/log/syslog
         /var/log/daemon.log
         /var/log/user.log
      Fri Nov 19 00:53:20 PST 2004 D1: dir_watch files new or changed:
         /var/log/auth.log
      AUTH.LOG: IPs from auth.log that will be denied:
         80.20.205.240
         210.239.139.54
         61.218.77.59
         ...
         148.244.79.145

