feat: add AUDIT logging level for tracking administrative changes

Add a new AUDIT logging level (-5) between PANIC (-4) and NOLOG (shifted
to -6) across C++, PHP, and Perl loggers. AUDIT entries use code 'AUD'
and syslog priority LOG_NOTICE. They record who changed what, from where,
for monitors, filters, users, config, roles, groups, zones, states,
servers, storage, events, snapshots, control caps, and login/logout.

AUDIT entries have their own retention period (ZM_LOG_AUDIT_DATABASE_LIMIT,
default 1 year) separate from regular log pruning. The log pruning in
zmstats.pl and zmaudit.pl now excludes AUDIT rows from regular pruning
and prunes them independently.

Critical safety: the C++ termination logic is changed from
'if (level <= FATAL)' to 'if (level == FATAL || level == PANIC)' to
prevent AUDIT-level log calls from killing the process.

Includes db migration zm_update-1.39.1.sql to shift any stored NOLOG
config values from -5 to -6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pull/4639/head^2
Isaac Connor 2026-02-17 18:17:48 -05:00
parent c0016fa00b
commit e6ace6fcf4
29 changed files with 202 additions and 34 deletions

15
db/zm_update-1.39.1.sql Normal file
View File

@ -0,0 +1,15 @@
--
-- Add AUDIT logging level between PANIC (-4) and NOLOG.
-- AUDIT is now -5; NOLOG shifts from -5 to -6.
-- Migrate any stored NOLOG config values from -5 to -6.
--
UPDATE Config SET Value = '-6'
WHERE Name IN ('ZM_LOG_LEVEL_SYSLOG','ZM_LOG_LEVEL_TERM',
'ZM_LOG_LEVEL_FILE','ZM_LOG_LEVEL_WEBLOG','ZM_LOG_LEVEL_DATABASE')
AND Value = '-5';
UPDATE Config SET DefaultValue = '-6'
WHERE Name IN ('ZM_LOG_LEVEL_SYSLOG','ZM_LOG_LEVEL_TERM',
'ZM_LOG_LEVEL_FILE','ZM_LOG_LEVEL_WEBLOG','ZM_LOG_LEVEL_DATABASE')
AND DefaultValue = '-5';

View File

@ -1179,7 +1179,7 @@ our @options = (
`,
type => {
db_type => 'integer',
hint => 'None=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
hint => 'None=-6|Audit=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
pattern => qr|^(\d+)$|,
format => q( $1 )
},
@ -1205,7 +1205,7 @@ our @options = (
`,
type => {
db_type => 'integer',
hint => 'None=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
hint => 'None=-6|Audit=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
pattern => qr|^(\d+)$|,
format => q( $1 )
},
@ -1235,7 +1235,7 @@ our @options = (
`,
type => {
db_type => 'integer',
hint => 'None=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
hint => 'None=-6|Audit=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
pattern => qr|^(\d+)$|,
format => q( $1 )
},
@ -1243,7 +1243,7 @@ our @options = (
},
{
name => 'ZM_LOG_LEVEL_WEBLOG',
default => '-5',
default => '-6',
description => 'Save logging output to the weblog',
help => q`
ZoneMinder logging is now more integrated between
@ -1262,7 +1262,7 @@ our @options = (
`,
type => {
db_type => 'integer',
hint => 'None=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
hint => 'None=-6|Audit=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
pattern => qr|^(\d+)$|,
format => q( $1 )
},
@ -1293,7 +1293,7 @@ our @options = (
`,
type => {
db_type => 'integer',
hint => 'None=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
hint => 'None=-6|Audit=-5|Panic=-4|Fatal=-3|Error=-2|Warning=-1|Info=0|Debug=1',
pattern => qr|^(\d+)$|,
format => q( $1 )
},
@ -1321,6 +1321,21 @@ our @options = (
type => $types{string},
category => 'logging',
},
{
name => 'ZM_LOG_AUDIT_DATABASE_LIMIT',
default => '1 year',
description => 'Maximum retention period for audit log entries',
help => q`
Audit log entries record administrative changes such as monitor
configuration, user management, filter changes, and system settings.
These entries are typically retained longer than regular log entries
for compliance and troubleshooting. Set to a row count (integer) or
time interval such as '1 year', '6 month', '90 day'. Set to empty
to retain audit logs indefinitely.
`,
type => $types{string},
category => 'logging',
},
{
name => 'ZM_LOG_FFMPEG',
default => 'yes',

View File

@ -57,6 +57,7 @@ our %EXPORT_TAGS = (
ERROR
FATAL
PANIC
AUDIT
NOLOG
) ],
functions => [ qw(
@ -79,6 +80,7 @@ our %EXPORT_TAGS = (
Error
Fatal
Panic
Audit
) ]
);
@ -122,7 +124,8 @@ use constant {
ERROR => -2,
FATAL => -3,
PANIC => -4,
NOLOG => -5
AUDIT => -5,
NOLOG => -6
};
our %codes = (
@ -141,6 +144,7 @@ our %codes = (
&ERROR => 'ERR',
&FATAL => 'FAT',
&PANIC => 'PNC',
&AUDIT => 'AUD',
&NOLOG => 'OFF'
);
@ -159,7 +163,8 @@ our %priorities = (
&WARNING => 'warning',
&ERROR => 'err',
&FATAL => 'err',
&PANIC => 'err'
&PANIC => 'err',
&AUDIT => 'notice'
);
our $logger;
@ -764,6 +769,8 @@ sub Panic {
confess($_[0]);
}
sub Audit { fetch()->logPrint(AUDIT, @_, caller); }
1;
__END__

View File

@ -844,22 +844,23 @@ FROM `Frames` WHERE `EventId`=?';
File::Find::find( { wanted=>\&deleteSwapImage, untaint=>1 }, $swap_image_root );
};
# Prune the Logs table if required
# Prune the Logs table if required (excluding AUDIT entries)
if ( $Config{ZM_LOG_DATABASE_LIMIT} ) {
my $audit_level = ZoneMinder::Logger::AUDIT;
if ( $Config{ZM_LOG_DATABASE_LIMIT} =~ /^\d+$/ ) {
# Number of rows
my $selectLogRowCountSql = 'SELECT count(*) AS `Rows` FROM `Logs`';
my $selectLogRowCountSql = 'SELECT count(*) AS `Rows` FROM `Logs` WHERE `Level` != ?';
my $selectLogRowCountSth = $dbh->prepare_cached( $selectLogRowCountSql )
or Fatal("Can't prepare '$selectLogRowCountSql': ".$dbh->errstr());
$res = $selectLogRowCountSth->execute()
$res = $selectLogRowCountSth->execute($audit_level)
or Fatal("Can't execute: ".$selectLogRowCountSth->errstr());
my $row = $selectLogRowCountSth->fetchrow_hashref();
my $logRows = $row->{Rows};
if ( $logRows > $Config{ZM_LOG_DATABASE_LIMIT} ) {
my $deleteLogByRowsSql = 'DELETE low_priority FROM `Logs` ORDER BY `TimeKey` ASC LIMIT ?';
my $deleteLogByRowsSql = 'DELETE low_priority FROM `Logs` WHERE `Level` != ? ORDER BY `TimeKey` ASC LIMIT ?';
my $deleteLogByRowsSth = $dbh->prepare_cached( $deleteLogByRowsSql )
or Fatal("Can't prepare '$deleteLogByRowsSql': ".$dbh->errstr());
$res = $deleteLogByRowsSth->execute( $logRows - $Config{ZM_LOG_DATABASE_LIMIT} )
$res = $deleteLogByRowsSth->execute( $audit_level, $logRows - $Config{ZM_LOG_DATABASE_LIMIT} )
or Fatal("Can't execute: ".$deleteLogByRowsSth->errstr());
if ( $deleteLogByRowsSth->rows() ) {
aud_print('Deleted '.$deleteLogByRowsSth->rows().' log table entries by count');
@ -867,7 +868,7 @@ FROM `Frames` WHERE `EventId`=?';
}
} else {
# Time of record
# 7 days is invalid. We need to remove the s
if ( $Config{ZM_LOG_DATABASE_LIMIT} =~ /^(.*)s$/ ) {
$Config{ZM_LOG_DATABASE_LIMIT} = $1;
@ -876,16 +877,53 @@ FROM `Frames` WHERE `EventId`=?';
do {
my $deleteLogByTimeSql =
'DELETE FROM `Logs`
WHERE `TimeKey` < unix_timestamp(now() - interval '.$Config{ZM_LOG_DATABASE_LIMIT}.') LIMIT 10';
WHERE `Level` != ? AND `TimeKey` < unix_timestamp(now() - interval '.$Config{ZM_LOG_DATABASE_LIMIT}.') LIMIT 10';
my $deleteLogByTimeSth = $dbh->prepare_cached( $deleteLogByTimeSql )
or Fatal("Can't prepare '$deleteLogByTimeSql': ".$dbh->errstr());
$res = $deleteLogByTimeSth->execute()
$res = $deleteLogByTimeSth->execute($audit_level)
or Fatal("Can't execute: ".$deleteLogByTimeSth->errstr());
$deleted_rows = $deleteLogByTimeSth->rows();
aud_print("Deleted $deleted_rows log table entries by time");
} while ( $deleted_rows );
}
} # end if ZM_LOG_DATABASE_LIMIT
# Prune AUDIT log entries separately with their own retention period
if ( $Config{ZM_LOG_AUDIT_DATABASE_LIMIT} ) {
my $audit_level = ZoneMinder::Logger::AUDIT;
my $audit_limit = $Config{ZM_LOG_AUDIT_DATABASE_LIMIT};
if ( $audit_limit =~ /^\d+$/ ) {
# Number of rows
my $sth = $dbh->prepare_cached('SELECT count(*) AS `Rows` FROM `Logs` WHERE `Level` = ?')
or Fatal("Can't prepare audit log count: ".$dbh->errstr());
$res = $sth->execute($audit_level)
or Fatal("Can't execute audit log count: ".$sth->errstr());
my $row = $sth->fetchrow_hashref();
my $logRows = $row->{Rows};
if ( $logRows > $audit_limit ) {
my $del_sth = $dbh->prepare_cached('DELETE low_priority FROM `Logs` WHERE `Level` = ? ORDER BY `TimeKey` ASC LIMIT ?')
or Fatal("Can't prepare audit log delete: ".$dbh->errstr());
$res = $del_sth->execute($audit_level, $logRows - $audit_limit)
or Fatal("Can't execute audit log delete: ".$del_sth->errstr());
if ( $del_sth->rows() ) {
aud_print('Deleted '.$del_sth->rows().' audit log entries by count');
}
}
} else {
# Time of record
$audit_limit =~ s/s$//;
my $deleted_rows;
do {
my $del_sth = $dbh->prepare_cached(
'DELETE FROM `Logs` WHERE `Level` = ? AND `TimeKey` < unix_timestamp(now() - interval '.$audit_limit.') LIMIT 10')
or Fatal("Can't prepare audit log time delete: ".$dbh->errstr());
$res = $del_sth->execute($audit_level)
or Fatal("Can't execute audit log time delete: ".$del_sth->errstr());
$deleted_rows = $del_sth->rows();
aud_print("Deleted $deleted_rows audit log entries by time");
} while ( $deleted_rows );
}
} # end if ZM_LOG_AUDIT_DATABASE_LIMIT
$loop = $continuous;
my $eventcounts_sql = '

View File

@ -99,19 +99,20 @@ while (!$zm_terminate) {
$event_ids = $dbh->selectcol_arrayref('SELECT EventId FROM Events_Month WHERE StartDateTime < DATE_SUB(NOW(), INTERVAL 1 month)');
zmDbDo('DELETE FROM Events_Month WHERE EventId IN ('.join(',', map { '?' } @$event_ids).')', @$event_ids) if $event_ids and @$event_ids;
# Prune the Logs table if required
# Prune the Logs table if required (excluding AUDIT entries)
if ( $Config{ZM_LOG_DATABASE_LIMIT} ) {
my $audit_level = ZoneMinder::Logger::AUDIT;
if ( $Config{ZM_LOG_DATABASE_LIMIT} =~ /^\d+$/ ) {
# Number of rows
my $selectLogRowCountSql = 'SELECT count(*) AS `Rows` FROM `Logs`';
my $selectLogRowCountSql = 'SELECT count(*) AS `Rows` FROM `Logs` WHERE `Level` != ?';
my $selectLogRowCountSth = $dbh->prepare_cached( $selectLogRowCountSql )
or Fatal("Can't prepare '$selectLogRowCountSql': ".$dbh->errstr());
my $res = $selectLogRowCountSth->execute()
my $res = $selectLogRowCountSth->execute($audit_level)
or Fatal("Can't execute: ".$selectLogRowCountSth->errstr());
my $row = $selectLogRowCountSth->fetchrow_hashref();
my $logRows = $row->{Rows};
if ( $logRows > $Config{ZM_LOG_DATABASE_LIMIT} ) {
my $rows = zmDbDo('DELETE low_priority FROM `Logs` ORDER BY `TimeKey` ASC LIMIT ?', $logRows - $Config{ZM_LOG_DATABASE_LIMIT});
my $rows = zmDbDo('DELETE low_priority FROM `Logs` WHERE `Level` != ? ORDER BY `TimeKey` ASC LIMIT ?', $audit_level, $logRows - $Config{ZM_LOG_DATABASE_LIMIT});
Debug('Deleted '.$rows.' log table entries by count') if defined $rows;
}
} else {
@ -123,12 +124,39 @@ while (!$zm_terminate) {
}
my $rows;
do {
$rows = zmDbDo('DELETE low_priority FROM `Logs` WHERE `TimeKey` < unix_timestamp(now() - interval '.$Config{ZM_LOG_DATABASE_LIMIT}.') LIMIT 100');
$rows = zmDbDo('DELETE low_priority FROM `Logs` WHERE `Level` != ? AND `TimeKey` < unix_timestamp(now() - interval '.$Config{ZM_LOG_DATABASE_LIMIT}.') LIMIT 100', $audit_level);
Debug("Deleted $rows log table entries by time") if $rows;
} while ($rows and ($rows == 100) and !$zm_terminate);
}
} # end if ZM_LOG_DATABASE_LIMIT
# Prune AUDIT log entries separately with their own retention period
if ( $Config{ZM_LOG_AUDIT_DATABASE_LIMIT} ) {
my $audit_level = ZoneMinder::Logger::AUDIT;
my $audit_limit = $Config{ZM_LOG_AUDIT_DATABASE_LIMIT};
if ( $audit_limit =~ /^\d+$/ ) {
# Number of rows
my $sth = $dbh->prepare_cached('SELECT count(*) AS `Rows` FROM `Logs` WHERE `Level` = ?')
or Fatal("Can't prepare audit log count: ".$dbh->errstr());
my $res = $sth->execute($audit_level)
or Fatal("Can't execute audit log count: ".$sth->errstr());
my $row = $sth->fetchrow_hashref();
my $logRows = $row->{Rows};
if ( $logRows > $audit_limit ) {
my $rows = zmDbDo('DELETE low_priority FROM `Logs` WHERE `Level` = ? ORDER BY `TimeKey` ASC LIMIT ?', $audit_level, $logRows - $audit_limit);
Debug('Deleted '.$rows.' audit log entries by count') if defined $rows;
}
} else {
# Time of record
$audit_limit =~ s/s$//;
my $rows;
do {
$rows = zmDbDo('DELETE low_priority FROM `Logs` WHERE `Level` = ? AND `TimeKey` < unix_timestamp(now() - interval '.$audit_limit.') LIMIT 100', $audit_level);
Debug("Deleted $rows audit log entries by time") if $rows;
} while ($rows and ($rows == 100) and !$zm_terminate);
}
} # end if ZM_LOG_AUDIT_DATABASE_LIMIT
{
my $rows;
do {

View File

@ -74,6 +74,7 @@ Logger::Logger() :
smCodes[ERROR] = "ERR";
smCodes[FATAL] = "FAT";
smCodes[PANIC] = "PNC";
smCodes[AUDIT] = "AUD";
smCodes[NOLOG] = "OFF";
smSyslogPriorities[INFO] = LOG_INFO;
@ -81,6 +82,7 @@ Logger::Logger() :
smSyslogPriorities[ERROR] = LOG_ERR;
smSyslogPriorities[FATAL] = LOG_ERR;
smSyslogPriorities[PANIC] = LOG_ERR;
smSyslogPriorities[AUDIT] = LOG_NOTICE;
char code[4] = "";
// Extra comparison against DEBUG1 to ensure GCC knows we are printing a single byte.
@ -420,7 +422,7 @@ void Logger::closeSyslog() {
void Logger::logPrint(bool hex, const char *filepath, int line, int level, const char *fstring, ...) {
if (level > mEffectiveLevel) return;
if (level < PANIC || level > DEBUG9)
if (level < AUDIT || level > DEBUG9)
Panic("Invalid logger level %d", level);
log_mutex.lock();
@ -544,12 +546,12 @@ void Logger::logPrint(bool hex, const char *filepath, int line, int level, const
}
log_mutex.unlock();
if (level <= FATAL) {
if (level == FATAL || level == PANIC) {
zm_terminate = true;
dbQueue.stop();
zmDbClose();
logTerm();
if (level <= PANIC) abort();
if (level == PANIC) abort();
exit(-1);
}
} // end logPrint

View File

@ -34,8 +34,9 @@
class Logger {
public:
enum {
NOOPT = -6,
NOLOG, // -5
NOOPT = -7,
NOLOG, // -6
AUDIT, // -5
PANIC, // -4
FATAL, // -3
ERROR, // -2
@ -229,6 +230,7 @@ inline Logger::Level logDebugging() {
#define Error(params...) logPrintf(Logger::ERROR, ##params)
#define Fatal(params...) logPrintf(Logger::FATAL, ##params)
#define Panic(params...) logPrintf(Logger::PANIC, ##params)
#define Audit(params...) logPrintf(Logger::AUDIT, ##params)
#define Mark() Info("Mark/%s/%d", __FILE__, __LINE__)
#define Log() Info("Log")
#ifdef __GNUC__

View File

@ -31,7 +31,10 @@ if ($action == 'delete') {
foreach ($_REQUEST['markMids'] as $markMid) {
if (canEdit('Monitors', $markMid)) {
$monitor = ZM\Monitor::find_one(['Id'=>$markMid]);
if ($monitor) $monitor->delete();
if ($monitor) {
$monitor->delete();
ZM\AuditAction('delete', 'monitor', $markMid, 'Name: '.$monitor->Name());
}
} else {
$error_message .= 'You do not have permission to delete monitor '.$markMid.'<br/>';
} // end if canedit this monitor

View File

@ -94,6 +94,7 @@ if ( $action == 'Save' ) {
global $error_message;
$error_message .= "Error saving control: " . $Control->get_last_error().'</br>';
} else {
ZM\AuditAction((!empty($_REQUEST['cid']) ? 'update' : 'create'), 'control', $Control->Id(), 'Name: '.($Control->Name() ?? ''));
$redirect = '?view=options&tab=control';
}
} // end if action

View File

@ -29,6 +29,7 @@ if ( canEdit('Events') ) {
if ( ($action == 'rename') && isset($_REQUEST['eventName']) ) {
dbQuery('UPDATE Events SET Name=? WHERE Id=?', array($_REQUEST['eventName'], $_REQUEST['eid']));
ZM\AuditAction('rename', 'event', $_REQUEST['eid'], 'Name: '.$_REQUEST['eventName']);
} else if ( $action == 'eventdetail' ) {
dbQuery('UPDATE Events SET Cause=?, Notes=? WHERE Id=?',
array(
@ -37,6 +38,7 @@ if ( canEdit('Events') ) {
$_REQUEST['eid']
)
);
ZM\AuditAction('update', 'event', $_REQUEST['eid'], 'Detail update');
$refreshParent = true;
$closePopup = true;
} else if ( $action == 'archive' ) {
@ -45,6 +47,7 @@ if ( canEdit('Events') ) {
dbQuery('UPDATE Events SET Archived=? WHERE Id=?', array(0, $_REQUEST['eid']));
} else if ( $action == 'delete' ) {
deleteEvent($_REQUEST['eid']);
ZM\AuditAction('delete', 'event', $_REQUEST['eid'], '');
$refreshParent = true;
}
} // end if canEdit(Events)

View File

@ -48,9 +48,11 @@ if ( $action == 'archive' ) {
$dbConn->commit();
$refreshParent = true;
} else if ( $action == 'delete' ) {
foreach ( getAffectedIds('eids') as $markEid ) {
$deletedEids = getAffectedIds('eids');
foreach ( $deletedEids as $markEid ) {
deleteEvent($markEid);
}
ZM\AuditAction('delete', 'events', 0, 'Count: '.count($deletedEids));
$refreshParent = true;
} else {
ZM\Warning("Unsupported action $action in events");

View File

@ -42,6 +42,7 @@ if (isset($_REQUEST['object']) and ($_REQUEST['object'] == 'filter')) {
$filter->control('stop');
}
$filter->delete();
ZM\AuditAction('delete', 'filter', $_REQUEST['Id'], 'Name: '.$filter->Name());
} else {
$error_message .= 'You do not have permission to delete the filter.<br/>';
}
@ -89,6 +90,7 @@ if (isset($_REQUEST['object']) and ($_REQUEST['object'] == 'filter')) {
$error_message = $filter->get_last_error();
return;
}
ZM\AuditAction('save', 'filter', $filter->Id(), 'Name: '.$filter->Name().' Action: '.$action);
if ($action == 'Save' or $action == 'SaveAs' ) {
// We update the request id so that the newly saved filter is auto-selected
$_REQUEST['Id'] = $filter->Id();

View File

@ -46,6 +46,7 @@ if ($action == 'save') {
$oldDecodingEnabled = $monitor->DecodingEnabled();
if ( $newFunction != $oldFunction || $newEnabled != $oldEnabled || $newDecodingEnabled != $oldDecodingEnabled ) {
$monitor->save(array('Function'=>$newFunction, 'Enabled'=>$newEnabled, 'DecodingEnabled'=>$newDecodingEnabled));
ZM\AuditAction('update', 'monitor', $mid, "Function: $oldFunction->$newFunction Enabled: $oldEnabled->$newEnabled");
if ( daemonCheck() && ($monitor->Type() != 'WebSite') ) {
$monitor->zmcControl(($newFunction != 'None') ? 'restart' : 'stop');

View File

@ -44,6 +44,7 @@ if ( $action == 'save' ) {
dbQuery('INSERT INTO `Groups_Monitors` (`GroupId`,`MonitorId`) VALUES (?,?)', array($group_id, $mid));
}
}
ZM\AuditAction((!empty($_REQUEST['gid']) ? 'update' : 'create'), 'group', $group->Id(), 'Name: '.$_REQUEST['newGroup']['Name']);
$redirect = '?view=groups';
}
?>

View File

@ -41,6 +41,7 @@ if ( $action == 'delete' ) {
if ( !empty($_REQUEST['gid']) ) {
foreach ( ZM\Group::find(array('Id'=>$_REQUEST['gid'])) as $Group ) {
$Group->delete();
ZM\AuditAction('delete', 'group', $Group->Id(), 'Name: '.$Group->Name());
}
}
$redirect = '?view=groups';

View File

@ -20,6 +20,7 @@
if ( $action == 'logout' ) {
ZM\Audit("user=".($user ? $user->Username() : 'unknown')." action=logout id=".($user ? $user->Id() : 0)." from=".($_SERVER['REMOTE_ADDR'] ?? 'local'));
userLogout();
$view = 'login';
} elseif ( $action == 'config' ) {

View File

@ -226,6 +226,7 @@ if ($action == 'save') {
} // end foreach zone
} // end if rotation or just size change
} // end if changes in width or height
ZM\AuditAction('update', 'monitor', $mid, 'Changed: '.implode(', ', array_keys($changes)));
} else {
$error_message .= $monitor->get_last_error();
} // end if successful save
@ -263,6 +264,7 @@ if ($action == 'save') {
$error_message .= $zone->get_last_error();
ZM\Error('Error adding zone:' . $error_message);
}
ZM\AuditAction('create', 'monitor', $mid, 'Name: '.($newMonitor['Name'] ?? ''));
} else {
ZM\Error('Error saving new Monitor.');
return;

View File

@ -38,6 +38,8 @@ if ($action == 'save') {
}
if (!$Monitor->save($_REQUEST['newMonitor'])) {
$error_message .= 'Error saving monitor: ' . $Monitor->get_last_error().'<br/>';
} else {
ZM\AuditAction('update', 'monitor', $mid, 'Bulk update: '.implode(', ', array_keys($_REQUEST['newMonitor'])));
}
if ($Monitor->Capturing() != 'None' && $Monitor->Type() != 'WebSite') {
$Monitor->zmcControl('start');

View File

@ -30,27 +30,35 @@ if ( $action == 'delete' ) {
if ( isset($_REQUEST['object']) ) {
if ( $_REQUEST['object'] == 'server' ) {
if ( !empty($_REQUEST['markIds']) ) {
foreach( $_REQUEST['markIds'] as $Id )
foreach ( $_REQUEST['markIds'] as $Id ) {
dbQuery('DELETE FROM Servers WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'server', $Id, '');
}
}
$refreshParent = true;
} else if ( $_REQUEST['object'] == 'storage' ) {
if ( !empty($_REQUEST['markIds']) ) {
foreach( $_REQUEST['markIds'] as $Id )
foreach ( $_REQUEST['markIds'] as $Id ) {
dbQuery('DELETE FROM Storage WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'storage', $Id, '');
}
}
$refreshParent = true;
} else if ( $_REQUEST['object'] == 'role' ) {
if ( !empty($_REQUEST['markRids']) ) {
foreach( $_REQUEST['markRids'] as $Id )
foreach ( $_REQUEST['markRids'] as $Id ) {
dbQuery('DELETE FROM User_Roles WHERE Id=?', array($Id));
ZM\AuditAction('delete', 'role', $Id, '');
}
}
$redirect = '?view=options&tab=roles';
} # end if isset($_REQUEST['object'] )
} else if ( isset($_REQUEST['markUids']) ) {
// deletes users
foreach ($_REQUEST['markUids'] as $markUid)
foreach ($_REQUEST['markUids'] as $markUid) {
dbQuery('DELETE FROM Users WHERE Id = ?', array($markUid));
ZM\AuditAction('delete', 'user', $markUid, '');
}
if ($markUid == $user->Id()) {
userLogout();
$redirect = '?view=login';
@ -90,6 +98,7 @@ if ( $action == 'delete' ) {
} # end if value changed
} # end foreach config entry
if ( $changed ) {
ZM\AuditAction('update', 'config', 0, 'Tab: '.$_REQUEST['tab']);
switch ( $_REQUEST['tab'] ) {
case 'system' :
case 'config' :

View File

@ -58,6 +58,7 @@ if ($action == 'Save') {
unset($_REQUEST['redirect']);
return;
}
ZM\AuditAction(($rid ? 'update' : 'create'), 'role', $dbRole->Id(), 'Name: '.$dbRole->Name());
}
# Save group permissions

View File

@ -41,6 +41,7 @@ if ($action == 'save') {
} else {
dbQuery('INSERT INTO Servers SET '.implode(', ', $changes));
}
ZM\AuditAction((!empty($_REQUEST['id']) ? 'update' : 'create'), 'server', $_REQUEST['id'] ?? 0, 'Changed: '.implode(', ', array_keys($changes)));
$refreshParent = true;
}
$redirect = '?view=options&tab=servers';

View File

@ -28,6 +28,7 @@ if ( $action == 'create' ) {
}
$snapshot = new ZM\Snapshot();
$snapshot->save(array('CreatedBy'=>$user->Id()));
ZM\AuditAction('create', 'snapshot', $snapshot->Id(), '');
foreach ( $_REQUEST['monitor_ids'] as $monitor_id ) {
if (!validCardinal($monitor_id)) {
@ -73,12 +74,14 @@ if ( isset($_REQUEST['id']) ) {
$changes = $snapshot->changes($_REQUEST['snapshot']);
if ( count($changes) ) {
$snapshot->save($changes);
ZM\AuditAction('update', 'snapshot', $snapshot->Id(), 'Changed: '.implode(', ', array_keys($changes)));
}
$redirect = '?view=snapshots';
}
} else if ( $action == 'delete' ) {
if ( canEdit('Events') ) {
$snapshot->delete();
ZM\AuditAction('delete', 'snapshot', $_REQUEST['id'], '');
$redirect = '?view=snapshots';
}
}

View File

@ -26,6 +26,7 @@ if (!canEdit('System')) {
if ($action == 'state') {
if (!empty($_REQUEST['runState'])) {
packageControl($_REQUEST['runState']);
ZM\AuditAction('apply', 'state', 0, 'State: '.$_REQUEST['runState']);
$refreshParent = true;
}
} else if ($action == 'save') {
@ -39,10 +40,13 @@ if ($action == 'state') {
if ( $_REQUEST['newState'] )
$_REQUEST['runState'] = $_REQUEST['newState'];
dbQuery('REPLACE INTO `States` SET `Name`=?, `Definition`=?', array($_REQUEST['runState'], $definition));
ZM\AuditAction('save', 'state', 0, 'Name: '.$_REQUEST['runState']);
}
} else if ($action == 'delete') {
if (isset($_REQUEST['runState']))
if (isset($_REQUEST['runState'])) {
dbQuery('DELETE FROM `States` WHERE `Name`=?', array($_REQUEST['runState']));
ZM\AuditAction('delete', 'state', 0, 'Name: '.$_REQUEST['runState']);
}
}
$redirect = '?view='.getHomeView();
?>

View File

@ -33,6 +33,7 @@ if ($action == 'save') {
if (count($changes)) {
if ($storage->save($changes)) {
ZM\AuditAction(($_REQUEST['id'] ? 'update' : 'create'), 'storage', $storage->Id(), 'Changed: '.implode(', ', array_keys($changes)));
} else {
$error_message .= $storage->get_last_error();
} // end if successful save

View File

@ -60,6 +60,7 @@ if ($action == 'Save') {
unset($_REQUEST['redirect']);
return;
}
ZM\AuditAction(($uid ? 'update' : 'create'), 'user', $dbUser->Id(), 'Username: '.$dbUser->Username().' Changed: '.implode(', ', array_keys($changes)));
if ($uid) {
if ($user and ($dbUser->Username() == $user->Username())) {
@ -120,6 +121,7 @@ if ($action == 'Save') {
unset($_REQUEST['redirect']);
return;
}
ZM\AuditAction('self_update', 'user', $dbUser->Id(), 'Changed: '.implode(', ', array_keys($changes)));
# We are the logged in user, need to update the $user object and generate a new auth_hash
$user = ZM\User::find_one(['Enabled'=>1, 'Id'=>$uid]);

View File

@ -60,6 +60,7 @@ if ( !empty($_REQUEST['mid']) && canEdit('Monitors', $_REQUEST['mid']) ) {
} else {
dbQuery('INSERT INTO Zones SET MonitorId=?, '.implode(', ', $changes), array($mid));
}
ZM\AuditAction(($zid > 0 ? 'update' : 'create'), 'zone', $zid, 'MonitorId: '.$mid);
if ( daemonCheck() && ($monitor->Type() != 'WebSite') ) {
$monitor->zmcControl('reload');
}

View File

@ -38,6 +38,7 @@ if ($action == 'delete') {
# Could use true but store the object instead for easy access later
$monitors_to_restart[] = $monitor;
$error_message .= $zone->delete();
ZM\AuditAction('delete', 'zone', $markZid, 'MonitorId: '.$monitor->Id());
} # end foreach Zone
if (daemonCheck()) {

View File

@ -604,6 +604,7 @@ if (ZM_OPT_USE_AUTH) {
$password = $_REQUEST['password'];
ZM\Info("Login successful for user \"$username\"");
ZM\Audit("user=$username action=login id=".$user->Id()." from=".($_SERVER['REMOTE_ADDR'] ?? 'local'));
$password_type = password_type($user->Password());
if ( $password_type == 'mysql' or $password_type == 'mysql+bcrypt' ) {

View File

@ -12,7 +12,8 @@ class Logger {
const ERROR = -2;
const FATAL = -3;
const PANIC = -4;
const NOLOG = -5; // Special artificial level to prevent logging
const AUDIT = -5;
const NOLOG = -6; // Special artificial level to prevent logging
private $initialised = false;
@ -46,6 +47,7 @@ class Logger {
self::ERROR => 'ERR',
self::FATAL => 'FAT',
self::PANIC => 'PNC',
self::AUDIT => 'AUD',
self::NOLOG => 'OFF',
);
private static $syslogPriorities = array(
@ -55,6 +57,7 @@ class Logger {
self::ERROR => LOG_ERR,
self::FATAL => LOG_ERR,
self::PANIC => LOG_ERR,
self::AUDIT => LOG_NOTICE,
);
private static $phpErrorLevels = array(
self::DEBUG => E_USER_NOTICE,
@ -63,6 +66,7 @@ class Logger {
self::ERROR => E_USER_WARNING,
self::FATAL => E_USER_ERROR,
self::PANIC => E_USER_ERROR,
self::AUDIT => E_USER_NOTICE,
);
private function __construct() {
@ -512,6 +516,20 @@ function Panic( $string ) {
exit(1);
}
function Audit($string) {
Logger::fetch()->logPrint(Logger::AUDIT, $string);
}
function AuditAction($action, $target_type, $target_id, $details) {
global $user;
$username = $user ? $user->Username() : 'system';
$ip = !empty($_SERVER['HTTP_X_FORWARDED_FOR'])
? $_SERVER['HTTP_X_FORWARDED_FOR']
: ($_SERVER['REMOTE_ADDR'] ?? 'local');
Audit("user=$username action=$action target=$target_type"
." id=$target_id details=\"$details\" from=$ip");
}
function ErrorHandler( $error, $string, $file, $line ) {
if ( ! (error_reporting() & $error) ) {
// This error code is not included in error_reporting