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