Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ on:
- develop

jobs:
php74-runtime-syntax:
name: PHP 7.4 advertised runtime syntax
runs-on: ubuntu-latest

steps:
- name: Checkout audit Plugin
uses: actions/checkout@v4

- name: Install PHP 7.4
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: none

- name: Lint runtime PHP
run: |
find . -path './.git' -prune -o -path './phpstan' -prune -o -path './tests' -prune \
-o -name '*.php' -print0 | xargs -0 -n1 php -l

code-quality:
runs-on: ubuntu-latest

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

services:
mysql:
image: mysql:8.0
image: mariadb:10.11.18
env:
MYSQL_ROOT_PASSWORD: cactiroot
MYSQL_DATABASE: cacti
Expand Down
4 changes: 2 additions & 2 deletions .phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ parameters:
- '*.po'
- '*.pot'
scanFiles:
- phpstan/stubs/cacti.stubs.php
- phpstan/stubs/cacti.stub
level: 8
treatPhpDocTypesAsCertain: false
reportUnmatchedIgnoredErrors: false
Expand All @@ -44,4 +44,4 @@ parameters:
- identifier: booleanAnd.rightAlwaysFalse
- identifier: booleanAnd.rightAlwaysTrue
- identifier: property.notFound
- identifier: parameterByRef.unusedType
- identifier: parameterByRef.unusedType
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* feature: Finalize request outcomes and expose external-log delivery status
* issue: Harden external file logging, retention, malformed records, and replication
* issue#38: Graph Template table does not exist
* issue#66: Restore the advertised PHP 7.4 runtime floor and fail-closed redaction
* issue: If the audit log does not exist or is not set, set it and create it
* issue: Audit assumes that all selected_items are numeric resulting in fatal error
* feature: Support for Cacti 1.3
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# audit

The plugin supports Cacti 1.2.20 and later and keeps its runtime files compatible
with PHP 7.4. Development analysis and the full security suite run on current PHP
versions; CI separately parses every production PHP file with PHP 7.4 so the
advertised installation floor cannot drift unnoticed.

This plugin is to be used to track transactions in the Cacti database, when they
were made, by what IP address and by what login account. This can be used to
determine the root cause of issues created by users of the Cacti system.
Expand Down
142 changes: 73 additions & 69 deletions audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@
WHERE id = ?',
[get_filter_request_var('id')]);

if (!is_array($data)) {
if (!cacti_sizeof($data)) {
http_response_code(404);
print html_escape(__('Audit event not found.', 'audit'));

break;
}
/** @var array<string,mixed> $data */

audit_record_event('audit.event.viewed', [
'event_category' => 'audit',
Expand Down Expand Up @@ -185,7 +186,8 @@ function audit_render_event_details(array $data): string {
LIMIT 1',
[$data['id']]);

if (is_array($syslog)) {
if (cacti_sizeof($syslog)) {
/** @var array<string,mixed> $syslog */
$output .= '<br><span><b>' . __('Remote Syslog Delivery:', 'audit') . '</b> <i>' . html_escape($syslog['state']) . '</i></span>';
$output .= '<br><span><b>' . __('Syslog Attempts:', 'audit') . '</b> <i>' . (int) $syslog['attempts'] . '</i></span>';

Expand Down Expand Up @@ -251,7 +253,10 @@ function audit_render_event_details(array $data): string {
return $output . '</table></td></tr></table>';
}

function audit_render_value(mixed $value): string {
/**
* @param mixed $value
*/
function audit_render_value($value): string {
if (is_array($value) || is_object($value)) {
return '<pre>' . html_escape(json_encode($value, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE)) . '</pre>';
}
Expand All @@ -266,22 +271,22 @@ function audit_render_value(mixed $value): string {
}

function audit_purge(): void {
$protected = db_fetch_cell("SELECT COUNT(*)
$protected = db_fetch_cell_prepared("SELECT COUNT(*)
FROM audit_log
WHERE EXISTS (
SELECT 1
FROM audit_syslog_delivery
WHERE audit_syslog_delivery.audit_id = audit_log.id
AND audit_syslog_delivery.state IN ('pending', 'retry', 'dead_letter')
)");
)", []);

db_execute("DELETE FROM audit_log
db_execute_prepared("DELETE FROM audit_log
WHERE NOT EXISTS (
SELECT 1
FROM audit_syslog_delivery
WHERE audit_syslog_delivery.audit_id = audit_log.id
AND audit_syslog_delivery.state IN ('pending', 'retry', 'dead_letter')
)");
)", []);
$purged = db_affected_rows();

audit_record_event('audit.log.purged', [
Expand Down Expand Up @@ -348,6 +353,7 @@ function audit_export_rows(): void {
]);

if (cacti_sizeof($events)) {
/** @var array<int,array<string,mixed>> $events */
header('Content-Disposition: attachment; filename=audit_export.csv');
header('Content-Type: text/csv; charset=UTF-8');
header('X-Content-Type-Options: nosniff');
Expand All @@ -357,44 +363,42 @@ function audit_export_rows(): void {
if ($output !== false) {
fputcsv($output, ['event_uuid', 'correlation_id', 'event_type', 'event_category', 'severity', 'page', 'user_id', 'username', 'action', 'request_status', 'operation_outcome', 'outcome_reason', 'target_type', 'target_id', 'external_status', 'external_error', 'ip_address', 'user_agent', 'http_method', 'http_status', 'event_time', 'completed_time', 'duration_ms', 'integrity_hash', 'post', 'details'], ',', '"', '');

if (is_array($events)) {
foreach ($events as $event) {
if ($event['action'] == 'cli') {
$poster = $event['post'];
} else {
$post = audit_json_decode($event['post'], $json_error);
$poster = is_array($post) ? json_encode($post, JSON_INVALID_UTF8_SUBSTITUTE) : $event['post'];
}

fputcsv($output, array_map('audit_csv_safe_cell', [
$event['event_uuid'],
$event['correlation_id'],
$event['event_type'],
$event['event_category'],
$event['severity'],
$event['page'],
$event['user_id'],
get_username($event['user_id']),
$event['action'],
$event['request_status'],
$event['operation_outcome'],
$event['outcome_reason'],
$event['target_type'],
$event['target_id'],
$event['external_status'],
$event['external_error'],
$event['ip_address'],
$event['user_agent'],
$event['http_method'],
$event['http_status'],
$event['event_time'],
$event['completed_time'],
$event['duration_ms'],
$event['integrity_hash'],
$poster,
$event['details']
]), ',', '"', '');
foreach ($events as $event) {
if ($event['action'] == 'cli') {
$poster = $event['post'];
} else {
$post = audit_json_decode($event['post'], $json_error);
$poster = is_array($post) ? json_encode($post, JSON_INVALID_UTF8_SUBSTITUTE) : $event['post'];
}

fputcsv($output, array_map('audit_csv_safe_cell', [
$event['event_uuid'],
$event['correlation_id'],
$event['event_type'],
$event['event_category'],
$event['severity'],
$event['page'],
$event['user_id'],
get_username($event['user_id']),
$event['action'],
$event['request_status'],
$event['operation_outcome'],
$event['outcome_reason'],
$event['target_type'],
$event['target_id'],
$event['external_status'],
$event['external_error'],
$event['ip_address'],
$event['user_agent'],
$event['http_method'],
$event['http_status'],
$event['event_time'],
$event['completed_time'],
$event['duration_ms'],
$event['integrity_hash'],
$poster,
$event['details']
]), ',', '"', '');
}

fclose($output);
Expand Down Expand Up @@ -677,31 +681,31 @@ function audit_log(): void {
$i = 0;

if (cacti_sizeof($events)) {
if (is_array($events)) {
foreach ($events as $e) {
if ($e['action'] == 'cli') {
form_alternate_row('line' . $e['id'], false);
form_selectable_ecell($e['page'], $e['id']);
form_selectable_ecell($e['user_agent'], $e['id']);
form_selectable_cell('<span id="event' . (int) $e['id'] . '" class="linkEditMain">' . html_escape(ucfirst($e['action'])) . '</span>', $e['id']);
form_selectable_ecell($e['request_status'], $e['id']);
form_selectable_ecell($e['external_status'], $e['id']);
form_selectable_cell(__('N/A', 'audit'), $e['id']);
form_selectable_ecell($e['ip_address'], $e['id'], '', 'right');
form_selectable_ecell($e['event_time'], $e['id'], '', 'right');
form_end_row();
} else {
form_alternate_row('line' . $e['id'], false);
form_selectable_cell(filter_value($e['page'], get_request_var('filter')), $e['id']);
form_selectable_ecell($e['username'], $e['id']);
form_selectable_cell('<span id="event' . (int) $e['id'] . '" class="linkEditMain">' . html_escape(ucfirst($e['action'])) . '</span>', $e['id']);
form_selectable_ecell($e['request_status'], $e['id']);
form_selectable_ecell($e['external_status'], $e['id']);
form_selectable_ecell($e['user_agent'], $e['id']);
form_selectable_ecell($e['ip_address'], $e['id'], '', 'right');
form_selectable_ecell($e['event_time'], $e['id'], '', 'right');
form_end_row();
}
/** @var array<int,array<string,mixed>> $events */

foreach ($events as $e) {
if ($e['action'] == 'cli') {
form_alternate_row('line' . $e['id'], false);
form_selectable_ecell($e['page'], $e['id']);
form_selectable_ecell($e['user_agent'], $e['id']);
form_selectable_cell('<span id="event' . (int) $e['id'] . '" class="linkEditMain">' . html_escape(ucfirst($e['action'])) . '</span>', $e['id']);
form_selectable_ecell($e['request_status'], $e['id']);
form_selectable_ecell($e['external_status'], $e['id']);
form_selectable_cell(__('N/A', 'audit'), $e['id']);
form_selectable_ecell($e['ip_address'], $e['id'], '', 'right');
form_selectable_ecell($e['event_time'], $e['id'], '', 'right');
form_end_row();
} else {
form_alternate_row('line' . $e['id'], false);
form_selectable_cell(filter_value($e['page'], get_request_var('filter')), $e['id']);
form_selectable_ecell($e['username'], $e['id']);
form_selectable_cell('<span id="event' . (int) $e['id'] . '" class="linkEditMain">' . html_escape(ucfirst($e['action'])) . '</span>', $e['id']);
form_selectable_ecell($e['request_status'], $e['id']);
form_selectable_ecell($e['external_status'], $e['id']);
form_selectable_ecell($e['user_agent'], $e['id']);
form_selectable_ecell($e['ip_address'], $e['id'], '', 'right');
form_selectable_ecell($e['event_time'], $e['id'], '', 'right');
form_end_row();
}
}
} else {
Expand Down
Loading
Loading