I have a remote Linux (Debian) box on which most of my development work is done. As recommended by many, one should install fail2ban to slow down bots attempting ssh brute-forcing. Installing/configuring fail2ban is quite straightforward, but after enabling it it’s unclear to me whether it’s getting the job done. Therefore, I write one program to count the number of failed ssh attempts to check. The full haskell program is attached in the end.

Running it gives the following output. It’s clear that fail2ban is effective in blocking those malicious IPs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ runghc ssh.hs
(2020-03-24 02:00:00 UTC,141)
(2020-03-24 03:00:00 UTC,173)
(2020-03-24 04:00:00 UTC,138)
(2020-03-24 05:00:00 UTC,79)
(2020-03-24 06:00:00 UTC,94)
(2020-03-24 07:00:00 UTC,115)
(2020-03-24 08:00:00 UTC,164)
(2020-03-24 09:00:00 UTC,258)
(2020-03-24 10:00:00 UTC,190)
(2020-03-24 11:00:00 UTC,183)
(2020-03-24 12:00:00 UTC,46)
(2020-03-24 13:00:00 UTC,17) # I enabled fail2ban here
(2020-03-24 14:00:00 UTC,10)
(2020-03-24 15:00:00 UTC,22)
(2020-03-24 16:00:00 UTC,11)
(2020-03-24 17:00:00 UTC,5)
(2020-03-24 18:00:00 UTC,9)
(2020-03-24 19:00:00 UTC,4)
(2020-03-24 20:00:00 UTC,8)
(2020-03-24 21:00:00 UTC,2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as S
import qualified Data.Text.IO as S
import Data.Time
import Data.List
import Data.Fixed
import Data.Function
import Control.Lens
import Control.Monad

type Line = S.Text
type Date = UTCTime
type IP = S.Text

file = "/var/log/auth.log"

extract_tuple :: Line -> (Date, IP)
extract_tuple line =
let
date_str = S.unpack . S.unwords . take 3 . S.words $ line
date = parseTimeOrError False defaultTimeLocale "%b %e %H:%M:%S" date_str
date_hour_precision = date & minute .~ 0 & second .~ 0
ip = (!! 2) . reverse . S.words $ line
in
(date_hour_precision, ip)
where
hms :: Lens' UTCTime (Int, Int, Pico)
hms = lens getter setter
where
getter t =
let TimeOfDay h m s = t & utctDayTime & timeToTimeOfDay
in (h, m, s)
setter t (h, m, s) =
t{utctDayTime = timeOfDayToTime (TimeOfDay h m s)}

minute :: Lens' UTCTime Int
minute = lens (^.hms._2) setter
where setter t v = t & hms ._2 .~ v

second :: Lens' UTCTime Pico
second = lens (^.hms._3) setter
where setter t v = t & hms ._3 .~ v

process contents = do
y <- (^.year) <$> getCurrentTime
S.lines contents
& filter predicate
& map extract_tuple
& aggregate
& reverse & take 20 & reverse
-- & take 4
& map (_1 . year .~ y)
& mapM_ print
where
predicate = liftM2 (&&) (S.isInfixOf "sshd") (S.isInfixOf "Invalid")

aggregate :: [(Date, IP)] -> [(Date, Int)]
aggregate pairs = pairs
& groupBy ((==) `on` fst)
& map (\l -> (l ^?! (element 0 . _1), length l))

ymd :: Lens' UTCTime (Integer, Int, Int)
ymd = lens getter setter
where
getter t = t & utctDay & toGregorian
setter t (y, m, d) = t{utctDay = fromGregorian y m d}

year :: Lens' UTCTime Integer
year = lens (^.ymd._1) setter
where
setter t y =
let (_, m, d) = t^.ymd
in t{utctDay = fromGregorian y m d}

main :: IO ()
main = do
S.readFile file >>= process