1<?php
2
3/**
4 * Class helper_plugin_loglog_alert
5 */
6class helper_plugin_loglog_alert extends DokuWiki_Plugin
7{
8    /**
9     * @var \helper_plugin_loglog_main
10     */
11    protected $mainHelper;
12
13    /**
14     * @var \helper_plugin_loglog_logging
15     */
16    protected $logHelper;
17
18    /** @var int */
19    protected $interval;
20
21    /** @var int */
22    protected $threshold;
23
24    /** @var int */
25    protected $now;
26
27    /** @var int */
28    protected $multiplier;
29
30    /** @var string */
31    protected $statfile;
32
33    public function __construct()
34    {
35        $this->mainHelper = $this->loadHelper('loglog_main');
36        $this->logHelper = $this->loadHelper('loglog_logging');
37    }
38
39    /**
40     * Check if any configured thresholds have been exceeded and trigger
41     * alert notifications accordingly.
42     *
43     * @return void
44     */
45    public function checkAlertThresholds()
46    {
47        $this->handleThreshold(
48            \helper_plugin_loglog_main::LOGTYPE_AUTH_FAIL,
49            $this->getConf('login_failed_max'),
50            $this->getConf('login_failed_interval'),
51            $this->getConf('login_failed_email')
52        );
53
54        $this->handleThreshold(
55            \helper_plugin_loglog_main::LOGTYPE_AUTH_OK,
56            $this->getConf('login_success_max'),
57            $this->getConf('login_success_interval'),
58            $this->getConf('login_success_email')
59        );
60    }
61
62    /**
63     * Evaluates threshold configuration for given type of logged event
64     * and triggers email alerts.
65     *
66     * @param string $logType
67     * @param int $threshold
68     * @param int $minuteInterval
69     * @param string $email
70     */
71    protected function handleThreshold($logType, $threshold, $minuteInterval, $email)
72    {
73        // proceed only if we have sufficient configuration
74        if (! $email || ! $threshold || ! $minuteInterval) {
75            return;
76        }
77        $this->resetMultiplier();
78        $this->threshold = $threshold;
79        $this->interval = $minuteInterval * 60;
80        $this->now = time();
81        $max = $this->now;
82        $min = $this->now - ($this->interval);
83
84        $msgNeedle = $this->mainHelper->getNotificationString($logType, 'msgNeedle');
85        $lines = $this->logHelper->readLines($min, $max);
86        $cnt = $this->logHelper->countMatchingLines($lines, $msgNeedle);
87        if ($cnt < $threshold) {
88            return;
89        }
90
91        global $conf;
92        $this->statfile = $conf['cachedir'] . '/loglog.' . $logType . '.stat';
93
94        if ($this->actNow()) {
95            io_saveFile($this->statfile, $this->multiplier);
96            $this->sendAlert($logType, $email);
97        }
98    }
99
100    /**
101     * Send alert email
102     *
103     * @param string $logType
104     * @param string $email
105     */
106    protected function sendAlert($logType, $email)
107    {
108        $template = $this->localFN($logType);
109        $text = file_get_contents($template);
110        $this->mainHelper->sendEmail(
111            $email,
112            $this->getLang($this->mainHelper->getNotificationString($logType, 'emailSubjectLang')),
113            $text,
114            [
115                'threshold' => $this->threshold,
116                'interval' => $this->interval / 60, // falling back to minutes for the view
117                'now' => date('Y-m-d H:i', $this->now),
118                'sequence' => $this->getSequencePhase(),
119                'next_alert' => date('Y-m-d H:i', $this->getNextAlert()),
120            ]
121        );
122    }
123
124    /**
125     * Check if it is time to act or wait this interval out
126     *
127     * @return bool
128     */
129    protected function actNow()
130    {
131        $act = true;
132
133        if (!is_file($this->statfile)) {
134            return $act;
135        }
136
137        $lastAlert = filemtime($this->statfile);
138        $this->multiplier = (int)file_get_contents($this->statfile);
139
140        $intervalsAfterLastAlert = (int)floor(($this->now - $lastAlert) / $this->interval);
141
142        if ($intervalsAfterLastAlert === $this->multiplier) {
143            $this->increaseMultiplier();
144        } elseif ($intervalsAfterLastAlert < $this->multiplier) {
145            $act = false;
146        } elseif ($intervalsAfterLastAlert > $this->multiplier) {
147            $this->resetMultiplier(); // no longer part of series, reset multiplier
148        }
149
150        return $act;
151    }
152
153    /**
154     * Calculate which phase of sequential events we are in (possible attacks),
155     * based on the interval multiplier. 1 indicates the first incident,
156     * otherwise evaluate the exponent (because we multiply the interval by 2 on each alert).
157     *
158     * @return int
159     */
160    protected function getSequencePhase()
161    {
162        return $this->multiplier === 1 ? $this->multiplier : log($this->multiplier, 2) + 1;
163    }
164
165    /**
166     * Calculate when the next alert is due based on the current multiplier
167     *
168     * @return int
169     */
170    protected function getNextAlert()
171    {
172        return $this->now + $this->interval * $this->multiplier * 2;
173    }
174
175    /**
176     * Reset multiplier. Called when the triggering event is not part of a sequence.
177     */
178    protected function resetMultiplier()
179    {
180        $this->multiplier = 1;
181    }
182
183    /**
184     * Increase multiplier. Called when the triggering event belongs to a sequence.
185     */
186    protected function increaseMultiplier()
187    {
188        $this->multiplier *= 2;
189    }
190}
191