Skip to content
Merged
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
7 changes: 4 additions & 3 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3']
php: ['8.4']
os: [ubuntu-latest]

services:
Expand All @@ -64,6 +64,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: Cacti/cacti
ref: 1.2.x
path: cacti

- name: Checkout audit Plugin
Expand All @@ -86,7 +87,7 @@ jobs:
run: sudo apt-get update

- name: Install System Dependencies
run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }}
run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping

- name: Start SNMPD Agent and Test
run: |
Expand Down Expand Up @@ -243,7 +244,7 @@ jobs:
fi

CLI_STATUS=$(mysql -u cactiuser -p'cactiuser' -h 127.0.0.1 cacti -se "select request_status from audit_log where action = 'cli' order by id desc limit 1;")
if [ "$CLI_STATUS" != "started" ]; then
if [ "$CLI_STATUS" != "completed" ]; then
echo "Unexpected CLI request status: $CLI_STATUS"
exit 1
fi
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

--- develop ---

* feature: Add normalized compliance event identifiers, categories, actors, targets, outcomes, timing, and integrity metadata
* feature: Deliver finalized request outcomes to external log consumers
* feature: Audit audit-log views, searches, event detail access, exports, and purges
* feature: Capture Cacti 1.2.x logout and session-timeout events through the supported logout hook
* feature: Finalize captured CLI activity and make it available to external log delivery
* feature: Add selectable text or JSON formats for external audit logging
* feature: Rename outcome to request_status with started/completed/failed values
* feature: Track and retry failed external audit-log delivery
Expand Down
2 changes: 1 addition & 1 deletion INFO
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

[info]
name = audit
version = 1.3
version = 1.4
longname = Audit Plugin for Cacti
author = The Cacti Group
email =
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ External file delivery is tracked on each database record. Failed appends are
retried by the poller in batches and therefore have at-least-once delivery
semantics; downstream ingestion should deduplicate when necessary.

Version 1.4 records a stable event UUID and request correlation UUID on new
events. External records are written only after request finalization, so SIEM
consumers receive the final request status instead of the earlier transient
`started` state. Consumers should deduplicate on `event_uuid`.

The normalized fields distinguish request processing from the result of the
requested Cacti operation. `request_status=completed` means that PHP request
processing completed without a fatal error or an HTTP error response. It does
not by itself prove that page-specific validation or database work succeeded.
`operation_outcome` remains `unknown` unless an authoritative Cacti 1.2.x hook
or plugin-owned operation supplies the result.

The plugin also audits access to its own event list, searches, event details,
exports and purge operations. Logout and session-timeout events are captured
through Cacti's supported `logout_pre_session_destroy` hook. Database-level
changes, API activity and MFA events are outside the current Cacti 1.2.x scope.

## Possible Bugs

If you figure out this problem, see the Cacti forums!
Expand Down
75 changes: 66 additions & 9 deletions audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@
break;
}

audit_record_event('audit.event.viewed', array(
'event_category' => 'audit',
'target_type' => 'audit_event',
'target_id' => $data['event_uuid'] != '' ? $data['event_uuid'] : $data['id'],
'details' => array('record_id' => $data['id'])
));

$output = audit_render_event_details($data);
echo $output;

Expand All @@ -81,7 +88,10 @@ function audit_render_event_details($data) {
$output .= '<br><span><b>' . __('IP Address:', 'audit') . '</b> <i>' . html_escape($data['ip_address']) . '</i></span>';
$output .= '<br><span><b>' . __('Date:', 'audit') . '</b> <i>' . html_escape($data['event_time']) . '</i></span>';
$output .= '<br><span><b>' . __('Action:', 'audit') . '</b> <i>' . html_escape($data['action']) . '</i></span>';
$output .= '<br><span><b>' . __('Event Type:', 'audit') . '</b> <i>' . html_escape($data['event_type']) . '</i></span>';
$output .= '<br><span><b>' . __('Event ID:', 'audit') . '</b> <i>' . html_escape($data['event_uuid']) . '</i></span>';
$output .= '<br><span><b>' . __('Request Status:', 'audit') . '</b> <i>' . html_escape($data['request_status']) . '</i></span>';
$output .= '<br><span><b>' . __('Operation Outcome:', 'audit') . '</b> <i>' . html_escape($data['operation_outcome']) . '</i></span>';
$output .= '<br><span><b>' . __('External Delivery:', 'audit') . '</b> <i>' . html_escape($data['external_status']) . '</i></span>';
if ($data['external_error'] != '') {
$output .= '<br><span><b>' . __('External Error:', 'audit') . '</b> <i>' . html_escape($data['external_error']) . '</i></span>';
Expand Down Expand Up @@ -153,6 +163,13 @@ function audit_render_value($value) {
function audit_purge() {
db_execute('TRUNCATE TABLE audit_log');

audit_record_event('audit.log.purged', array(
'event_category' => 'audit',
'severity' => 'warning',
'action' => 'purge',
'target_type' => 'audit_log'
));

$_SESSION['audit_message'] = __('Audit Log Purged by %s', get_username($_SESSION['sess_user_id']), 'audit');

cacti_log('NOTE: Audit Log Purged by ' . get_username($_SESSION['sess_user_id']), false, 'WEBUI');
Expand Down Expand Up @@ -192,13 +209,25 @@ function audit_export_rows() {
$sql_where",
$sql_params);

audit_record_event('audit.log.exported', array(
'event_category' => 'audit',
'action' => 'export',
'target_type' => 'audit_log',
'details' => array(
'row_count' => cacti_sizeof($events),
'filter' => get_request_var('filter'),
'event_page' => get_request_var('event_page'),
'user_id' => get_request_var('user_id')
)
));

if (cacti_sizeof($events)) {
header('Content-Disposition: attachment; filename=audit_export.csv');
header('Content-Type: text/csv; charset=UTF-8');
header('X-Content-Type-Options: nosniff');

$output = fopen('php://output', 'w');
fputcsv($output, array('page', 'user_id', 'username', 'action', 'request_status', 'external_status', 'external_error', 'ip_address', 'user_agent', 'event_time', 'post'), ',', '"', '');
fputcsv($output, array('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'), ',', '"', '');

foreach($events as $event) {
if ($event['action'] == 'cli') {
Expand All @@ -208,19 +237,34 @@ function audit_export_rows() {
$poster = is_array($post) ? json_encode($post, JSON_INVALID_UTF8_SUBSTITUTE) : $event['post'];
}

fputcsv($output, array_map('audit_csv_safe_cell', array(
$event['page'],
fputcsv($output, array_map('audit_csv_safe_cell', array(
$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['external_status'],
$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['event_time'],
$poster
)), ',', '"', '');
$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 @@ -275,6 +319,19 @@ function audit_log() {
global $item_rows;

audit_process_request_vars();
$has_filters = get_request_var('filter') != '' ||
get_request_var('event_page') != '-1' ||
(!isempty_request_var('user_id') && get_request_var('user_id') > '-1');
audit_record_event($has_filters ? 'audit.log.searched' : 'audit.log.viewed', array(
'event_category' => 'audit',
'action' => $has_filters ? 'search' : 'view',
'target_type' => 'audit_log',
'details' => array(
'filter' => get_request_var('filter'),
'event_page' => get_request_var('event_page'),
'user_id' => get_request_var('user_id')
)
));

if (get_request_var('rows') == '-1') {
$rows = read_config_option('num_rows_table');
Expand Down
Loading
Loading