diff --git a/db/zm_update-1.39.1.sql b/db/zm_update-1.39.1.sql
new file mode 100644
index 000000000..3066ecf3e
--- /dev/null
+++ b/db/zm_update-1.39.1.sql
@@ -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';
diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
index 37e3a0ac8..2092a85dc 100644
--- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
+++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
@@ -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',
diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm
index 2c1c13fbb..73484df3d 100644
--- a/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm
+++ b/scripts/ZoneMinder/lib/ZoneMinder/Logger.pm
@@ -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__
diff --git a/scripts/zmaudit.pl.in b/scripts/zmaudit.pl.in
index 2ad29a7a5..66d815886 100644
--- a/scripts/zmaudit.pl.in
+++ b/scripts/zmaudit.pl.in
@@ -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 = '
diff --git a/scripts/zmstats.pl.in b/scripts/zmstats.pl.in
index 2127500de..2301e7f22 100644
--- a/scripts/zmstats.pl.in
+++ b/scripts/zmstats.pl.in
@@ -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 {
diff --git a/src/zm_logger.cpp b/src/zm_logger.cpp
index 51988f7ed..d9a5989e5 100644
--- a/src/zm_logger.cpp
+++ b/src/zm_logger.cpp
@@ -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
diff --git a/src/zm_logger.h b/src/zm_logger.h
index d7d8862d3..81d032060 100644
--- a/src/zm_logger.h
+++ b/src/zm_logger.h
@@ -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__
diff --git a/web/includes/actions/console.php b/web/includes/actions/console.php
index f8e90d1fa..30993e004 100644
--- a/web/includes/actions/console.php
+++ b/web/includes/actions/console.php
@@ -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.'
';
} // end if canedit this monitor
diff --git a/web/includes/actions/controlcap.php b/web/includes/actions/controlcap.php
index 13c30357c..aca8e8fae 100644
--- a/web/includes/actions/controlcap.php
+++ b/web/includes/actions/controlcap.php
@@ -94,6 +94,7 @@ if ( $action == 'Save' ) {
global $error_message;
$error_message .= "Error saving control: " . $Control->get_last_error().'';
} else {
+ ZM\AuditAction((!empty($_REQUEST['cid']) ? 'update' : 'create'), 'control', $Control->Id(), 'Name: '.($Control->Name() ?? ''));
$redirect = '?view=options&tab=control';
}
} // end if action
diff --git a/web/includes/actions/event.php b/web/includes/actions/event.php
index a3c6d56c3..0e597246f 100644
--- a/web/includes/actions/event.php
+++ b/web/includes/actions/event.php
@@ -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)
diff --git a/web/includes/actions/events.php b/web/includes/actions/events.php
index 37e747037..6192411e5 100644
--- a/web/includes/actions/events.php
+++ b/web/includes/actions/events.php
@@ -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");
diff --git a/web/includes/actions/filter.php b/web/includes/actions/filter.php
index 15e047651..87277cb02 100644
--- a/web/includes/actions/filter.php
+++ b/web/includes/actions/filter.php
@@ -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.
';
}
@@ -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();
diff --git a/web/includes/actions/function.php b/web/includes/actions/function.php
index c786abeea..9e7b8a3ca 100644
--- a/web/includes/actions/function.php
+++ b/web/includes/actions/function.php
@@ -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');
diff --git a/web/includes/actions/group.php b/web/includes/actions/group.php
index 37d109d6a..3258a46c8 100644
--- a/web/includes/actions/group.php
+++ b/web/includes/actions/group.php
@@ -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';
}
?>
diff --git a/web/includes/actions/groups.php b/web/includes/actions/groups.php
index bc431998e..312261ccc 100644
--- a/web/includes/actions/groups.php
+++ b/web/includes/actions/groups.php
@@ -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';
diff --git a/web/includes/actions/logout.php b/web/includes/actions/logout.php
index 030ad142a..cc9674dbc 100644
--- a/web/includes/actions/logout.php
+++ b/web/includes/actions/logout.php
@@ -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' ) {
diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php
index 386531c5c..706637c82 100644
--- a/web/includes/actions/monitor.php
+++ b/web/includes/actions/monitor.php
@@ -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;
diff --git a/web/includes/actions/monitors.php b/web/includes/actions/monitors.php
index 6ac6bd6b3..b059c404d 100644
--- a/web/includes/actions/monitors.php
+++ b/web/includes/actions/monitors.php
@@ -38,6 +38,8 @@ if ($action == 'save') {
}
if (!$Monitor->save($_REQUEST['newMonitor'])) {
$error_message .= 'Error saving monitor: ' . $Monitor->get_last_error().'
';
+ } else {
+ ZM\AuditAction('update', 'monitor', $mid, 'Bulk update: '.implode(', ', array_keys($_REQUEST['newMonitor'])));
}
if ($Monitor->Capturing() != 'None' && $Monitor->Type() != 'WebSite') {
$Monitor->zmcControl('start');
diff --git a/web/includes/actions/options.php b/web/includes/actions/options.php
index aa8f072ab..e90d3ed1c 100644
--- a/web/includes/actions/options.php
+++ b/web/includes/actions/options.php
@@ -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' :
diff --git a/web/includes/actions/role.php b/web/includes/actions/role.php
index da5d9c16d..84e476adf 100644
--- a/web/includes/actions/role.php
+++ b/web/includes/actions/role.php
@@ -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
diff --git a/web/includes/actions/server.php b/web/includes/actions/server.php
index 137a95981..5e282ee9f 100644
--- a/web/includes/actions/server.php
+++ b/web/includes/actions/server.php
@@ -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';
diff --git a/web/includes/actions/snapshot.php b/web/includes/actions/snapshot.php
index 949ce4284..04f54a2a9 100644
--- a/web/includes/actions/snapshot.php
+++ b/web/includes/actions/snapshot.php
@@ -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';
}
}
diff --git a/web/includes/actions/state.php b/web/includes/actions/state.php
index d36a9bf85..9fb91c4fe 100644
--- a/web/includes/actions/state.php
+++ b/web/includes/actions/state.php
@@ -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();
?>
diff --git a/web/includes/actions/storage.php b/web/includes/actions/storage.php
index 74845a61d..4d282cca9 100644
--- a/web/includes/actions/storage.php
+++ b/web/includes/actions/storage.php
@@ -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
diff --git a/web/includes/actions/user.php b/web/includes/actions/user.php
index 323817ac8..f2efce55a 100644
--- a/web/includes/actions/user.php
+++ b/web/includes/actions/user.php
@@ -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]);
diff --git a/web/includes/actions/zone.php b/web/includes/actions/zone.php
index e48da16f1..07621aadf 100644
--- a/web/includes/actions/zone.php
+++ b/web/includes/actions/zone.php
@@ -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');
}
diff --git a/web/includes/actions/zones.php b/web/includes/actions/zones.php
index 70209fc02..6e02ef618 100644
--- a/web/includes/actions/zones.php
+++ b/web/includes/actions/zones.php
@@ -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()) {
diff --git a/web/includes/auth.php b/web/includes/auth.php
index 7ddb3576f..7d7a4520d 100644
--- a/web/includes/auth.php
+++ b/web/includes/auth.php
@@ -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' ) {
diff --git a/web/includes/logger.php b/web/includes/logger.php
index c885819bc..f073bb86a 100644
--- a/web/includes/logger.php
+++ b/web/includes/logger.php
@@ -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