1<?php
2
3/**
4 * Places security information in a security log
5 * The logged data includes:
6 * <ul>
7 * 	<li>the ip address of the client browser</li>
8 * 	<li>the type of entry</li>
9 * 	<li>the user/user name</li>
10 * 	<li>the success/failure</li>
11 * 	<li>the <i>authority</i> granting/denying the request</li>
12 * 	<li>Additional information, for instance on failure, the password used</li>
13 * </ul>
14 *
15 * @author Stephen Billard (sbillard)
16 * @package plugins
17 * @subpackage security-logger
18 */
19$plugin_is_filter = 100 | CLASS_PLUGIN;
20$plugin_description = gettext('Logs selected security events.');
21$plugin_author = "Stephen Billard (sbillard)";
22$plugin_category = gettext('Admin');
23
24$option_interface = 'security_logger';
25
26if (getOption('logger_log_admin')) {
27	zp_register_filter('admin_login_attempt', 'security_logger::adminLoginLogger');
28	zp_register_filter('federated_login_attempt', 'security_logger::federatedLoginLogger');
29}
30if (getOption('logger_log_guests')) {
31	zp_register_filter('guest_login_attempt', 'security_logger::guestLoginLogger');
32}
33zp_register_filter('admin_allow_access', 'security_logger::adminGate');
34zp_register_filter('authorization_cookie', 'security_logger::adminCookie', 0);
35zp_register_filter('admin_managed_albums_access', 'security_logger::adminAlbumGate');
36zp_register_filter('save_user', 'security_logger::UserSave');
37zp_register_filter('admin_XSRF_access', 'security_logger::admin_XSRF_access');
38zp_register_filter('admin_log_actions', 'security_logger::log_action');
39zp_register_filter('log_setup', 'security_logger::log_setup');
40zp_register_filter('security_misc', 'security_logger::security_misc');
41
42/**
43 * Option handler class
44 *
45 */
46class security_logger {
47
48	/**
49	 * class instantiation function
50	 *
51	 * @return security_logger
52	 */
53	function __construct() {
54		global $plugin_is_filter;
55		if (OFFSET_PATH == 2) {
56			setOptionDefault('zp_plugin_security-logger', $plugin_is_filter);
57			setOptionDefault('logger_log_guests', 1);
58			setOptionDefault('logger_log_admin', 1);
59			setOptionDefault('logger_log_type', 'all');
60			setOptionDefault('logge_access_log_type', 'all_user');
61			setOptionDefault('security_log_size', 5000000);
62		}
63	}
64
65	/**
66	 * Reports the supported options
67	 *
68	 * @return array
69	 */
70	function getOptionsSupported() {
71		return array(gettext('Record logon attempts of')		 => array('key'				 => 'logger_log_allowed', 'type'			 => OPTION_TYPE_CHECKBOX_ARRAY,
72										'checkboxes' => array(gettext('Administrators') => 'logger_log_admin', gettext('Guests') => 'logger_log_guests'),
73										'desc'			 => gettext('If checked login attempts will be logged.')),
74						gettext('Record failed admin access')	 => array('key'			 => 'logge_access_log_type', 'type'		 => OPTION_TYPE_RADIO,
75										'buttons'	 => array(gettext('All attempts') => 'all', gettext('Only user attempts') => 'all_user'),
76										'desc'		 => gettext('Record admin page access failures.')),
77						gettext('Record logon')								 => array('key'			 => 'logger_log_type', 'type'		 => OPTION_TYPE_RADIO,
78										'buttons'	 => array(gettext('All attempts') => 'all', gettext('Successful attempts') => 'success', gettext('unsuccessful attempts') => 'fail'),
79										'desc'		 => gettext('Record login failures, successes, or all attempts.'))
80		);
81	}
82
83	function handleOption($option, $currentValue) {
84
85	}
86
87	/**
88	 * Does the log handling
89	 *
90	 * @param int $success
91	 * @param string $user
92	 * @param string $name
93	 * @param string $ip
94	 * @param string $type
95	 * @param string $authority kind of login
96	 * @param string $addl more info
97	 */
98	private static function Logger($success, $user, $name, $action, $authority, $addl = NULL) {
99		global $_zp_authority, $_zp_mutex;
100		$pattern = '~^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$~';
101		$forwardedIP = NULL;
102		$ip = sanitize($_SERVER['REMOTE_ADDR']);
103		if (!preg_match($pattern, $ip)) {
104			$ip = NULL;
105		} else {
106			$ip = getAnonymIP($ip);
107		}
108		if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
109			$forwardedIP = sanitize($_SERVER['HTTP_X_FORWARDED_FOR']);
110			if (preg_match($pattern, $forwardedIP)) {
111				$ip .= ' {' . getAnonymIP($forwardedIP) . '}';
112			}
113		}
114		$admin = $_zp_authority->getMasterUser();
115		$locale = $admin->getLanguage();
116		if (empty($locale)) {
117			$locale = 'en_US';
118		}
119		$cur_locale = getUserLocale();
120		setupCurrentLocale($locale); //	the log will be in the language of the master user.
121		switch ($action) {
122			case 'clear_log':
123				$type = gettext('Log reset');
124				break;
125			case 'delete_log':
126				$type = gettext('Log deleted');
127				break;
128			case 'download_log':
129				$type = gettext('Log downloaded');
130				break;
131			case 'setup_install':
132				$type = gettext('Install');
133				$addl = gettext('version') . ' ' . ZENPHOTO_VERSION;
134				if (!hasPrimaryScripts()) {
135					$addl .= ' ' . gettext('clone');
136				}
137				break;
138			case 'setup_ignore_setup':
139				$type = gettext('Setup run request skipped.');
140				break;
141			case 'setup_protect':
142				$type = gettext('Protect setup scripts');
143				break;
144			case 'user_new':
145				$type = gettext('Request add user');
146				break;
147			case 'user_update':
148				$type = gettext('Request update user');
149				break;
150			case 'user_delete':
151				$type = gettext('Request delete user');
152				break;
153			case 'XSRF_blocked':
154				$type = gettext('Cross Site Reference');
155				break;
156			case 'blocked_album':
157				$type = gettext('Album access');
158				break;
159			case 'blocked_access':
160				$type = gettext('Admin access');
161				break;
162			case 'Front-end':
163				$type = gettext('Guest login');
164				break;
165			case 'Back-end':
166				$type = gettext('Admin login');
167				break;
168			case 'auth_cookie':
169				$type = gettext('Authorization cookie check');
170				break;
171			default:
172				$type = $action;
173				break;
174		}
175
176		$file = SERVERPATH . '/' . DATA_FOLDER . '/security.log';
177		$max = getOption('security_log_size');
178		$_zp_mutex->lock();
179		if ($max && @filesize($file) > $max) {
180			switchLog('security');
181		}
182		$preexists = file_exists($file) && filesize($file) > 0;
183		$f = fopen($file, 'a');
184		if ($f) {
185			if (!$preexists) { // add a header
186				fwrite($f, gettext('date' . "\t" . 'requestor’s IP' . "\t" . 'type' . "\t" . 'user ID' . "\t" . 'user name' . "\t" . 'outcome' . "\t" . 'authority' . "\tadditional information\n"));
187			}
188			$message = date('Y-m-d H:i:s') . "\t";
189			$message .= $ip . "\t";
190			$message .= $type . "\t";
191			$message .= $user . "\t";
192			$message .= $name . "\t";
193			switch ($success) {
194				case 0:
195					$message .= gettext("Failed") . "\t";
196					break;
197				case 1:
198					$message .= gettext("Success") . "\t";
199					$message .= substr($authority, 0, strrpos($authority, '_auth'));
200					break;
201				case 2:
202					$message .= gettext("Blocked") . "\t";
203					break;
204				default:
205					$message .= $success . "\t";
206			}
207			if ($addl) {
208				$message .= "\t" . $addl;
209			}
210			fwrite($f, $message . "\n");
211			fclose($f);
212			clearstatcache();
213			if (!$preexists) {
214				@chmod($file, LOGS_MOD);
215				if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
216					$permission = fileperms($file) & 0700; //	on Windows owner==group==public
217					$check = $permission != LOGS_MOD;
218				} else {
219					$permission = fileperms($file) & 0777;
220					$check = $permission != LOGS_MOD;
221				}
222				if ($check) {
223					$f = fopen($file, 'a');
224					fwrite($f, "\t\t" . gettext('Set Security log permissions') . "\t\t\t" . gettext('Failed') . "\t\t" . sprintf(gettext('File permissions of Security log are %04o'), $permission) . "\n");
225					fclose($f);
226					clearstatcache();
227				}
228			}
229		}
230		$_zp_mutex->unlock();
231		setupCurrentLocale($cur_locale); //	restore to whatever was in effect.
232	}
233
234	/**
235	 * returns the user id and name of the logged in user
236	 */
237	private static function populate_user() {
238		global $_zp_current_admin_obj;
239		if (is_object($_zp_current_admin_obj)) {
240			$user = $_zp_current_admin_obj->getUser();
241			$name = $_zp_current_admin_obj->getName();
242		} else {
243			$user = $name = '';
244		}
245		return array($user, $name);
246	}
247
248	/**
249	 * Logs an attempt to log onto the back-end or as an admin user
250	 * Returns the rights to grant
251	 *
252	 * @param int $success the admin rights granted
253	 * @param string $user
254	 * @param string $pass
255	 * @return int
256	 */
257	static function adminLoginLogger($success, $user, $pass, $auth = 'zp_admin_auth') {
258		switch (getOption('logger_log_type')) {
259			case 'all':
260				break;
261			case 'success':
262				if (!$success)
263					return false;
264				break;
265			case 'fail':
266				if ($success)
267					return true;
268				break;
269		}
270		$name = '';
271		if ($success) {
272			$admin = Zenphoto_Authority::getAnAdmin(array('`user`=' => $user, '`valid`=' => 1));
273			$pass = ''; // mask it from display
274			if (is_object($admin)) {
275				$name = $admin->getName();
276			}
277		}
278		security_logger::Logger((int) ($success && true), $user, $name, 'Back-end', $auth, null);
279		return $success;
280	}
281
282	/**
283	 * Logs an attempt to log on via the federated_logon plugin
284	 * Returns the rights to grant
285	 *
286	 * @param int $success the admin rights granted
287	 * @param string $user
288	 * @param string $pass
289	 * @return int
290	 */
291	static function federatedLoginLogger($success, $user) {
292		return security_logger::adminLoginLogger($success, $user, 'n/a', 'federated_logon_auth');
293	}
294
295	/**
296	 * Logs an attempt for a guest user to log onto the site
297	 * Returns the "success" parameter.
298	 *
299	 * @param bool $success
300	 * @param string $user
301	 * @param string $pass
302	 * @param string $athority what kind of login
303	 * @return bool
304	 */
305	static function guestLoginLogger($success, $user, $pass, $athority) {
306		switch (getOption('logger_log_type')) {
307			case 'all':
308				break;
309			case 'success':
310				if (!$success)
311					return false;
312				break;
313			case 'fail':
314				if ($success)
315					return true;
316				break;
317		}
318		$name = '';
319		if ($success) {
320			$admin = Zenphoto_Authority::getAnAdmin(array('`user`=' => $user, '`valid`=' => 1));
321			$pass = ''; // mask it from display
322			if (is_object($admin)) {
323				$name = $admin->getName();
324			}
325		}
326		security_logger::Logger((int) ($success && true), $user, $name, 'Front-end', $athority, null);
327		return $success;
328	}
329
330	/**
331	 * Logs blocked accesses to Admin pages
332	 * @param bool $allow set to true to override the block
333	 * @param string $page the "return" link
334	 */
335	static function adminGate($allow, $page) {
336		list($user, $name) = security_logger::populate_user();
337		switch (getOption('logger_log_type')) {
338			case 'all':
339				break;
340			case 'all_user':
341				if (!$user)
342					return $allow;
343				break;
344		}
345		security_logger::Logger(0, $user, $name, 'blocked_access', '', $page);
346		return $allow;
347	}
348
349	static function adminCookie($allow, $auth, $id) {
350		if (!$allow && $auth) {
351			switch (getOption('logger_log_type')) {
352				case 'all':
353				case 'fail':
354					security_logger::Logger(0, NULL, NULL, 'auth_cookie', '', $id . ':' . $auth);
355			}
356		}
357		return $allow;
358	}
359
360	/**
361	 * Logs blocked accesses to Managed albums
362	 * @param bool $allow set to true to override the block
363	 * @param string $page the "return" link
364	 */
365	static function adminAlbumGate($allow, $page) {
366		list($user, $name) = security_logger::populate_user();
367		switch (getOption('logger_log_type')) {
368			case 'all':
369				break;
370			case 'all_user':
371				if (!$user)
372					return $allow;
373				break;
374		}
375		if (!$allow)
376			security_logger::Logger(2, $user, $name, 'blocked_album', '', $page);
377		return $allow;
378	}
379
380	/**
381	 * logs attempts to save on the user tab
382	 * @param string $discard
383	 * @param object $userobj user object upon which the save was targeted
384	 * @param string $class what the action was.
385	 */
386	static function UserSave($discard, $userobj, $class) {
387		list($user, $name) = security_logger::populate_user();
388		security_logger::Logger(1, $user, $name, 'user_' . $class, 'zp_admin_auth', $userobj->getUser());
389		return $discard;
390	}
391
392	/**
393	 * Loggs Cross Site Request Forgeries
394	 *
395	 * @param bool $discard
396	 * @param string $token
397	 * @return bool
398	 */
399	static function admin_XSRF_access($discard, $token) {
400		list($user, $name) = security_logger::populate_user();
401		security_logger::Logger(2, $user, $name, 'XSRF_blocked', '', $token);
402		return false;
403	}
404
405	/**
406	 * logs security log actions
407	 * @param bool $allow
408	 * @param string $log
409	 * @param string $action
410	 */
411	static function log_action($allow, $log, $action) {
412		list($user, $name) = security_logger::populate_user();
413		security_logger::Logger((int) ($allow && true), $user, $name, $action, 'zp_admin_auth', basename($log));
414		return $allow;
415	}
416
417	/**
418	 * Logs setup actions
419	 * @param bool $success
420	 * @param string $action
421	 * @param string $file
422	 */
423	static function log_setup($success, $action, $txt) {
424		list($user, $name) = security_logger::populate_user();
425		security_logger::Logger((int) ($success && true), $user, $name, 'setup_' . $action, 'zp_admin_auth', $txt);
426		return $success;
427	}
428
429	/**
430	 * Catch all logger for miscellaneous security records
431	 * @param bool $success
432	 * @param string $requestor
433	 * @param string $auth
434	 * @param string $txt
435	 */
436	static function security_misc($success, $requestor, $auth, $txt) {
437		security_logger::Logger((int) ($success && true), NULL, NULL, $requestor, 'zp_admin_auth', $txt);
438		return $success;
439	}
440
441}
442
443?>