diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d4407a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.php] +indent_style = tab +indent_size = 4 + +[*.{js,sh}] +indent_style = tab +indent_size = 4 + +[*.{po,pot,mo}] +binary = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..0e9994a --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,100 @@ +# +-------------------------------------------------------------------------+ +# | Copyright (C) 2004-2026 The Cacti Group | +# | | +# | This program is free software; you can redistribute it and/or | +# | modify it under the terms of the GNU General Public License | +# | as published by the Free Software Foundation; either version 2 | +# | of the License, or (at your option) any later version. | +# | | +# | This program is distributed in the hope that it will be useful, | +# | but WITHOUT ANY WARRANTY; without even the implied warranty of | +# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +# | GNU General Public License for more details. | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | This code is designed, written, and maintained by the Cacti Group. See | +# | about.php and/or the AUTHORS file for specific developer information. | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ + +name: Code Quality + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + code-quality: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + + name: PHP ${{ matrix.php }} Code Quality + + steps: + - name: Checkout Cacti + uses: actions/checkout@v4 + with: + repository: Cacti/cacti + ref: develop + path: cacti + + - name: Checkout audit Plugin + uses: actions/checkout@v4 + with: + path: cacti/plugins/audit + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, mysql, gd, ldap, gmp, mbstring, dom, xml, json, sockets + coverage: none + ini-values: "memory_limit=512M" + + - name: Check PHP version + run: php -v + + - name: Validate Cacti Composer files + run: composer validate --strict + working-directory: cacti + + - name: Install Cacti development dependencies + run: composer install --prefer-dist --no-progress + working-directory: cacti + + - name: Lint audit Plugin + run: include/vendor/bin/phplint --no-cache --exclude=locales plugins/audit + working-directory: cacti + + - name: Run PHP-CS-Fixer (dry-run) + run: include/vendor/bin/php-cs-fixer fix --dry-run --diff --config=plugins/audit/.php-cs-fixer.php --allow-unsupported-php-version=yes + working-directory: cacti + + - name: Run PHPStan + run: include/vendor/bin/phpstan analyse --configuration=plugins/audit/.phpstan.neon --memory-limit=512M --no-progress + working-directory: cacti + + - name: Run Pest Tests + if: ${{ matrix.php != '8.1' }} + run: include/vendor/bin/pest plugins/audit/tests/Security --display-warnings + working-directory: cacti + + - name: Run Audit Security Helper Tests + run: | + php tests/security_functions_test.php + php tests/controller_security_test.php + php tests/syslog_queue_test.php + timeout 60 php tests/syslog_functions_test.php + working-directory: cacti/plugins/audit diff --git a/.gitignore b/.gitignore index 3dd84d9..fac17a3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,13 @@ locales/po/*.mo + +# PHPStan +phpstan.cache/ +.phpstan-baseline.neon.local + +# PHP-CS-Fixer +.php-cs-fixer.cache + +# Pest / PHPUnit +.phpunit.result.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..b1cccc6 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,227 @@ +exclude('locales') + ->exclude('js') + ->exclude('.github') + ->exclude('openspec') + ->exclude('vendor') + ->in(__DIR__); + +$config = new PhpCsFixer\Config(); +$config + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRiskyAllowed(true) + ->setIndent("\t") + ->setLineEnding("\n") + ->setRules(array( + 'header_comment' => false, + 'comment_to_phpdoc' => true, + 'phpdoc_align' => true, + 'list_syntax' => ['syntax' => 'short'], + 'array_syntax' => ['syntax' => 'short'], + 'trim_array_spaces' => false, + 'no_whitespace_before_comma_in_array' => true, + 'whitespace_after_comma_in_array' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_whitespace_in_blank_line' => true, + 'no_trailing_whitespace' => true, + 'normalize_index_brace' => true, + 'no_mixed_echo_print' => ['use' => 'print'], + 'no_spaces_after_function_name' => true, + 'braces' => [ + 'position_after_functions_and_oop_constructs' => 'same', + 'position_after_control_structures' => 'same', + 'allow_single_line_closure' => true + ], + 'braces_position' => [ + 'anonymous_classes_opening_brace' => 'same_line', + 'anonymous_functions_opening_brace' => 'same_line', + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line' + ], + 'single_blank_line_at_eof' => true, + 'method_chaining_indentation' => true, + 'indentation_type' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'line_ending' => true, + 'magic_constant_casing' => true, + 'native_function_casing' => true, + 'elseif' => true, + 'include' => false, + 'no_alternative_syntax' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_braces' => true, + 'no_useless_else' => false, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => null, + 'always_move_variable' => false + ], + 'declare_equal_normalize' => ['space' => 'single'], + 'dir_constant' => true, + 'single_space_around_construct' => [ + 'constructs_followed_by_a_single_space' => [ + 'abstract', + 'as', + 'attribute', + 'break', + 'case', + 'catch', + 'class', + 'clone', + 'const', + 'const_import', + 'continue', + 'do', + 'echo', + 'else', + 'elseif', + 'extends', + 'final', + 'finally', + 'for', + 'foreach', + 'function', + 'function_import', + 'global', + 'goto', + 'if', + 'implements', + 'instanceof', + 'insteadof', + 'interface', + 'match', + 'named_argument', + 'new', + 'open_tag_with_echo', + 'php_open', + 'print', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'throw', + 'trait', + 'try', + 'use', + 'use_lambda', + 'use_trait', + 'var', + 'while', + 'yield', + 'yield_from' + ] + ], + 'concat_space' => ['spacing' => 'one'], + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'switch_continue_to_break' => true, + 'logical_operators' => true, + 'function_declaration' => ['closure_function_spacing' => 'one'], + 'spaces_inside_parentheses' => true, + 'binary_operator_spaces' => [ + 'operators' => [ + '+=' => 'align_single_space', + '===' => 'align_single_space_minimal', + '=' => 'align_single_space', + '|' => 'single_space', + '=>' => 'align', + '!=' => 'align' + ] + ], + 'not_operator_with_space' => false, + 'no_spaces_around_offset' => ['positions' => ['outside', 'inside']], + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'full_opening_tag' => false, + 'linebreak_after_opening_tag' => false, + 'phpdoc_add_missing_param_annotation' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use' + ] + ], + 'no_empty_statement' => true, + 'multiline_whitespace_before_semicolons' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'semicolon_after_instruction' => false, + 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true], + 'blank_line_before_statement' => [ + 'statements' => [ + 'continue', + 'break', + 'declare', + 'do', + 'for', + 'foreach', + 'goto', + 'if', + 'return', + 'switch', + 'throw', + 'try', + 'while', + 'yield', + 'yield_from' + ] + ], + 'explicit_string_variable' => false, + 'single_quote' => true, + 'string_line_ending' => true, + 'strict_param' => true, + 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], + 'single_line_comment_spacing' => true, + 'single_line_comment_style' => true, + 'multiline_comment_opening_closing' => true + )) + ->setFinder($finder); + +return $config; \ No newline at end of file diff --git a/.phpstan.neon b/.phpstan.neon new file mode 100644 index 0000000..6087a8c --- /dev/null +++ b/.phpstan.neon @@ -0,0 +1,47 @@ +includes: + - phpstan-baseline.neon + +parameters: + parallel: + maximumNumberOfProcesses: 8 + paths: + - setup.php + - audit.php + - audit_functions.php + - audit_syslog.php + - index.php + - locales/index.php + - locales/LC_MESSAGES/index.php + - tests/ + excludePaths: + analyse: + - locales/* + - js/* + - vendor/* + - '*.mo' + - '*.po' + - '*.pot' + scanFiles: + - phpstan/stubs/cacti.stubs.php + level: 8 + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: if.alwaysFalse + - identifier: notEqual.alwaysFalse + - identifier: notEqual.alwaysTrue + - identifier: equal.alwaysTrue + - identifier: if.alwaysTrue + - identifier: while.alwaysTrue + - identifier: greater.alwaysFalse + - identifier: identical.alwaysTrue + - identifier: identical.alwaysFalse + - identifier: booleanAnd.leftAlwaysTrue + - identifier: booleanAnd.leftAlwaysFalse + - identifier: function.alreadyNarrowedType + - identifier: includeOnce.fileNotFound + - identifier: booleanAnd.rightAlwaysFalse + - identifier: booleanAnd.rightAlwaysTrue + - identifier: property.notFound + - identifier: parameterByRef.unusedType \ No newline at end of file diff --git a/audit.php b/audit.php index 8703ec0..dcbf213 100644 --- a/audit.php +++ b/audit.php @@ -28,130 +28,134 @@ set_default_action(); switch(get_request_var('action')) { -case 'export': - audit_export_rows(); - - break; -case 'syslog_test': - if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - header('Allow: POST'); - exit; - } + case 'export': + audit_export_rows(); - if (!audit_user_is_admin() || !csrf_check(false)) { - http_response_code(403); - exit; - } + break; + case 'syslog_test': + if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + header('Allow: POST'); + exit; + } - $result = audit_syslog_test_delivery(); - $success = $result['status'] === 'sent_unconfirmed'; - audit_record_event('audit.syslog.test', array( - 'event_category' => 'audit', - 'severity' => $success ? 'notice' : 'warning', - 'action' => 'test', - 'target_type' => 'syslog_receiver', - 'operation_outcome' => $success ? 'success' : 'failure', - 'outcome_reason' => $success ? 'socket_write_completed' : ($result['error_code'] ?? 'delivery_failed'), - 'details' => array( - 'transport' => audit_syslog_config()['transport'], - 'status' => $result['status'], - 'error_code' => $result['error_code'] ?? '' - ) - )); - $_SESSION['audit_message'] = $success - ? __('Syslog test record was written to the local socket. Receiver storage is unconfirmed.', 'audit') - : __('Syslog test failed: %s', html_escape($result['error'] ?? 'Unknown error'), 'audit'); - raise_message('audit_message'); + if (!audit_user_is_admin() || !csrf_check(false)) { + http_response_code(403); + exit; + } - top_header(); - audit_log(); - bottom_footer(); + $result = audit_syslog_test_delivery(); + $success = $result['status'] === 'sent_unconfirmed'; + audit_record_event('audit.syslog.test', [ + 'event_category' => 'audit', + 'severity' => $success ? 'notice' : 'warning', + 'action' => 'test', + 'target_type' => 'syslog_receiver', + 'operation_outcome' => $success ? 'success' : 'failure', + 'outcome_reason' => $success ? 'socket_write_completed' : ($result['error_code'] ?? 'delivery_failed'), + 'details' => [ + 'transport' => audit_syslog_config()['transport'], + 'status' => $result['status'], + 'error_code' => $result['error_code'] ?? '' + ] + ]); + $_SESSION['audit_message'] = $success + ? __('Syslog test record was written to the local socket. Receiver storage is unconfirmed.', 'audit') + : __('Syslog test failed: %s', html_escape($result['error'] ?? 'Unknown error'), 'audit'); + raise_message('audit_message'); + + top_header(); + audit_log(); + bottom_footer(); - break; -case 'syslog_retry': - if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - header('Allow: POST'); - exit; - } - - if (!audit_user_is_admin() || !csrf_check(false)) { - http_response_code(403); - exit; - } + break; + case 'syslog_retry': + if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + header('Allow: POST'); + exit; + } - $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); - $post = is_array($post) ? $post : array(); - $delivery_ids = isset($post['delivery_ids']) && is_array($post['delivery_ids']) - ? $post['delivery_ids'] - : array(); - $retried = audit_syslog_retry_dead_letters($delivery_ids); - audit_record_event('audit.syslog.dead_letter.retried', array( - 'event_category' => 'audit', - 'severity' => 'notice', - 'action' => 'retry', - 'target_type' => 'syslog_delivery', - 'operation_outcome' => 'success', - 'details' => array('row_count' => $retried) - )); - $_SESSION['audit_message'] = __('Reset %d dead-letter Syslog deliveries for retry.', $retried, 'audit'); - raise_message('audit_message'); + if (!audit_user_is_admin() || !csrf_check(false)) { + http_response_code(403); + exit; + } - top_header(); - audit_log(); - bottom_footer(); + $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); + $post = is_array($post) ? $post : []; + $delivery_ids = isset($post['delivery_ids']) && is_array($post['delivery_ids']) + ? $post['delivery_ids'] + : []; + $retried = audit_syslog_retry_dead_letters($delivery_ids); + audit_record_event('audit.syslog.dead_letter.retried', [ + 'event_category' => 'audit', + 'severity' => 'notice', + 'action' => 'retry', + 'target_type' => 'syslog_delivery', + 'operation_outcome' => 'success', + 'details' => ['row_count' => $retried] + ]); + $_SESSION['audit_message'] = __('Reset %d dead-letter Syslog deliveries for retry.', $retried, 'audit'); + raise_message('audit_message'); + + top_header(); + audit_log(); + bottom_footer(); - break; -case 'purge': - if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { - http_response_code(405); - header('Allow: POST'); - exit; - } + break; + case 'purge': + if (!isset($_SERVER['REQUEST_METHOD']) || $_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + header('Allow: POST'); + exit; + } - if (!audit_user_is_admin() || !csrf_check(false)) { - http_response_code(403); - exit; - } + if (!audit_user_is_admin() || !csrf_check(false)) { + http_response_code(403); + exit; + } - audit_purge(); + audit_purge(); - top_header(); - audit_log(); - bottom_footer(); + top_header(); + audit_log(); + bottom_footer(); - break; -case 'getdata': - $data = db_fetch_row_prepared('SELECT * + break; + case 'getdata': + $data = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', - array(get_filter_request_var('id'))); + [get_filter_request_var('id')]); - if (!cacti_sizeof($data)) { - http_response_code(404); - print html_escape(__('Audit event not found.', 'audit')); - break; - } + if (!is_array($data)) { + http_response_code(404); + print html_escape(__('Audit event not found.', 'audit')); - 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; - - break; -default: - top_header(); - audit_log(); - bottom_footer(); + break; + } + + audit_record_event('audit.event.viewed', [ + 'event_category' => 'audit', + 'target_type' => 'audit_event', + 'target_id' => $data['event_uuid'] != '' ? $data['event_uuid'] : $data['id'], + 'details' => ['record_id' => $data['id']] + ]); + + $output = audit_render_event_details($data); + print $output; + + break; + default: + top_header(); + audit_log(); + bottom_footer(); } -function audit_render_event_details($data) { +/** + * @param array $data + */ +function audit_render_event_details(array $data): string { $width = 'wide'; $output = '
'; $output .= '' . __('Page:', 'audit') . ' ' . html_escape($data['page']) . ''; @@ -163,10 +167,12 @@ function audit_render_event_details($data) { $output .= '
' . __('Event ID:', 'audit') . ' ' . html_escape($data['event_uuid']) . ''; $output .= '
' . __('Request Status:', 'audit') . ' ' . html_escape($data['request_status']) . ''; $output .= '
' . __('Operation Outcome:', 'audit') . ' ' . html_escape($data['operation_outcome']) . ''; + if ($data['outcome_reason'] != '') { $output .= '
' . __('Outcome Reason:', 'audit') . ' ' . html_escape($data['outcome_reason']) . ''; } $output .= '
' . __('External File Delivery:', 'audit') . ' ' . html_escape($data['external_status']) . ''; + if ($data['external_error'] != '') { $output .= '
' . __('External File Error:', 'audit') . ' ' . html_escape($data['external_error']) . ''; } @@ -177,14 +183,16 @@ function audit_render_event_details($data) { WHERE audit_id = ? ORDER BY id DESC LIMIT 1', - array($data['id'])); + [$data['id']]); - if (cacti_sizeof($syslog)) { + if (is_array($syslog)) { $output .= '
' . __('Remote Syslog Delivery:', 'audit') . ' ' . html_escape($syslog['state']) . ''; $output .= '
' . __('Syslog Attempts:', 'audit') . ' ' . (int) $syslog['attempts'] . ''; + if ($syslog['sent_time'] != '') { $output .= '
' . __('Syslog Socket Write:', 'audit') . ' ' . html_escape($syslog['sent_time']) . ''; } + if ($syslog['last_error'] != '') { $output .= '
' . __('Syslog Error:', 'audit') . ' ' . html_escape($syslog['last_error']) . ''; } @@ -194,13 +202,14 @@ function audit_render_event_details($data) { if ($data['action'] == 'cli') { $output .= '' . __('Script:', 'audit') . ' ' . html_escape($data['post']) . ''; + return $output . '
'; } $attribs = audit_json_decode($data['post'], $json_error); - $attribs = is_array($attribs) ? $attribs : array( + $attribs = is_array($attribs) ? $attribs : [ __('Stored Data', 'audit') => __('The stored request data is not valid JSON: %s', $json_error, 'audit') - ); + ]; ksort($attribs); $columns = cacti_sizeof($attribs) > 16 ? 2 : 1; @@ -210,6 +219,7 @@ function audit_render_event_details($data) { : '' . __('Attrib', 'audit') . '' . __('Value', 'audit') . ''; $i = 0; + foreach ($attribs as $field => $content) { if ($i % $columns == 0) { $output .= ''; @@ -228,6 +238,7 @@ function audit_render_event_details($data) { } $record_data = audit_json_decode($data['object_data'], $object_error); + if (is_array($record_data) && !empty($record_data)) { $output .= '
'; $output .= '' . __('Record Data:', 'audit') . ''; @@ -240,7 +251,7 @@ function audit_render_event_details($data) { return $output . ''; } -function audit_render_value($value) { +function audit_render_value(mixed $value): string { if (is_array($value) || is_object($value)) { return '
' . html_escape(json_encode($value, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE)) . '
'; } @@ -254,7 +265,7 @@ function audit_render_value($value) { return html_escape((string) $value); } -function audit_purge() { +function audit_purge(): void { $protected = db_fetch_cell("SELECT COUNT(*) FROM audit_log WHERE EXISTS ( @@ -273,16 +284,16 @@ function audit_purge() { )"); $purged = db_affected_rows(); - audit_record_event('audit.log.purged', array( + audit_record_event('audit.log.purged', [ 'event_category' => 'audit', - 'severity' => 'warning', - 'action' => 'purge', - 'target_type' => 'audit_log', - 'details' => array( - 'purged_count' => $purged, + 'severity' => 'warning', + 'action' => 'purge', + 'target_type' => 'audit_log', + 'details' => [ + 'purged_count' => $purged, 'protected_count' => (int) $protected - ) - )); + ] + ]); $_SESSION['audit_message'] = __('Audit Log purge by %s removed %d events and protected %d events with unfinished Syslog delivery.', get_username($_SESSION['sess_user_id']), $purged, (int) $protected, 'audit'); @@ -292,12 +303,12 @@ function audit_purge() { raise_message('audit_message'); } -function audit_export_rows() { +function audit_export_rows(): void { audit_process_request_vars(); - /* form the 'where' clause for our main sql query */ - $sql_clauses = array(); - $sql_params = array(); + // form the 'where' clause for our main sql query + $sql_clauses = []; + $sql_params = []; if (get_request_var('filter') != '') { $sql_clauses[] = '(page LIKE ? OR post LIKE ?)'; @@ -324,17 +335,17 @@ function audit_export_rows() { $sql_where", $sql_params); - audit_record_event('audit.log.exported', array( + audit_record_event('audit.log.exported', [ 'event_category' => 'audit', - 'action' => 'export', - 'target_type' => 'audit_log', - 'details' => array( - 'row_count' => cacti_sizeof($events), - 'filter' => get_request_var('filter'), + 'action' => 'export', + 'target_type' => 'audit_log', + 'details' => [ + 'row_count' => cacti_sizeof($events), + 'filter' => get_request_var('filter'), 'event_page' => get_request_var('event_page'), - 'user_id' => get_request_var('user_id') - ) - )); + 'user_id' => get_request_var('user_id') + ] + ]); if (cacti_sizeof($events)) { header('Content-Disposition: attachment; filename=audit_export.csv'); @@ -342,111 +353,116 @@ function audit_export_rows() { header('X-Content-Type-Options: nosniff'); $output = fopen('php://output', 'w'); - 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') { - $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']; + + 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'] + ]), ',', '"', ''); + } } - 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['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); } - - fclose($output); } } -function audit_process_request_vars() { - /* ================= input validation and session storage ================= */ - $filters = array( - 'rows' => array( - 'filter' => FILTER_VALIDATE_INT, +function audit_process_request_vars(): void { + // ================= input validation and session storage ================= + $filters = [ + 'rows' => [ + 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( - 'filter' => FILTER_VALIDATE_INT, + ], + 'page' => [ + 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'filter' => array( - 'filter' => FILTER_DEFAULT, + ], + 'filter' => [ + 'filter' => FILTER_DEFAULT, 'pageset' => true, 'default' => '' - ), - 'sort_column' => array( - 'filter' => FILTER_CALLBACK, + ], + 'sort_column' => [ + 'filter' => FILTER_CALLBACK, 'default' => 'event_time', - 'options' => array('options' => 'sanitize_search_string') - ), - 'sort_direction' => array( - 'filter' => FILTER_CALLBACK, + 'options' => ['options' => 'sanitize_search_string'] + ], + 'sort_direction' => [ + 'filter' => FILTER_CALLBACK, 'default' => 'DESC', - 'options' => array('options' => 'sanitize_search_string') - ), - 'user_id' => array( - 'filter' => FILTER_VALIDATE_INT, + 'options' => ['options' => 'sanitize_search_string'] + ], + 'user_id' => [ + 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'event_page' => array( - 'filter' => FILTER_CALLBACK, + ], + 'event_page' => [ + 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '-1', - 'options' => array('options' => 'sanitize_search_string') - ) - ); + 'options' => ['options' => 'sanitize_search_string'] + ] + ]; validate_store_request_vars($filters, 'sess_audit'); - /* ================= input validation ================= */ + // ================= input validation ================= } -function audit_log() { +function audit_log(): void { global $item_rows; audit_process_request_vars(); $has_filters = get_request_var('filter') != '' || - get_request_var('event_page') != '-1' || + 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( + audit_record_event($has_filters ? 'audit.log.searched' : 'audit.log.viewed', [ 'event_category' => 'audit', - 'action' => $has_filters ? 'search' : 'view', - 'target_type' => 'audit_log', - 'details' => array( - 'filter' => get_request_var('filter'), + 'action' => $has_filters ? 'search' : 'view', + 'target_type' => 'audit_log', + 'details' => [ + 'filter' => get_request_var('filter'), 'event_page' => get_request_var('event_page'), - 'user_id' => get_request_var('user_id') - ) - )); + 'user_id' => get_request_var('user_id') + ] + ]); if (get_request_var('rows') == '-1') { $rows = read_config_option('num_rows_table'); @@ -467,73 +483,89 @@ function audit_log() {
- + - '> + '> - + - + - + - - - + + + - +
- '> + '> @@ -541,9 +573,9 @@ function audit_log() { html_end_box(); - /* form the 'where' clause for our main sql query */ - $sql_clauses = array(); - $sql_params = array(); + // form the 'where' clause for our main sql query + $sql_clauses = []; + $sql_params = []; if (get_request_var('filter') != '') { $sql_clauses[] = '(page LIKE ? OR post LIKE ?)'; @@ -572,7 +604,7 @@ function audit_log() { $sql_params); $sql_order = get_order_string(); - $sql_limit = ' LIMIT ' . ($rows*(get_request_var('page')-1)) . ',' . $rows; + $sql_limit = ' LIMIT ' . ($rows * (get_request_var('page') - 1)) . ',' . $rows; $events = db_fetch_assoc_prepared("SELECT audit_log.*, user_auth.username FROM audit_log @@ -589,61 +621,63 @@ function audit_log() { html_start_box('', '100%', '', '3', 'center', ''); - $display_text = array( - 'page' => array( + $display_text = [ + 'page' => [ 'display' => __('Page Name', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('The page where the event was generated.', 'audit') - ), - 'username' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('The page where the event was generated.', 'audit') + ], + 'username' => [ 'display' => __('User Name', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('The user who generated the event.', 'audit') - ), - 'action' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('The user who generated the event.', 'audit') + ], + 'action' => [ 'display' => __('Requested Action', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('The requested Cacti action. Hover over the action to see request data.', 'audit') - ), - 'request_status' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('The requested Cacti action. Hover over the action to see request data.', 'audit') + ], + 'request_status' => [ 'display' => __('Request Status', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('Request processing state; completion does not guarantee that every operation succeeded.', 'audit') - ), - 'external_status' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('Request processing state; completion does not guarantee that every operation succeeded.', 'audit') + ], + 'external_status' => [ 'display' => __('External File Delivery', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('Delivery state for the optional external audit log file. Remote Syslog health is shown separately.', 'audit') - ), - 'user_agent' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('Delivery state for the optional external audit log file. Remote Syslog health is shown separately.', 'audit') + ], + 'user_agent' => [ 'display' => __('User Agent', 'audit'), - 'align' => 'left', - 'sort' => 'ASC', - 'tip' => __('The browser type of the requester.', 'audit') - ), - 'ip_address' => array( + 'align' => 'left', + 'sort' => 'ASC', + 'tip' => __('The browser type of the requester.', 'audit') + ], + 'ip_address' => [ 'display' => __('IP Address', 'audit'), - 'align' => 'right', - 'sort' => 'ASC', - 'tip' => __('The IP Address of the requester.', 'audit') - ), - 'event_time' => array( + 'align' => 'right', + 'sort' => 'ASC', + 'tip' => __('The IP Address of the requester.', 'audit') + ], + 'event_time' => [ 'display' => __('Event Time', 'audit'), - 'align' => 'right', - 'sort' => 'DESC', - 'tip' => __('The time the Event took place.', 'audit') - ) - ); + 'align' => 'right', + 'sort' => 'DESC', + 'tip' => __('The time the Event took place.', 'audit') + ] + ]; html_header_sort($display_text, get_request_var('sort_column'), get_request_var('sort_direction'), false); $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); @@ -666,11 +700,12 @@ function audit_log() { 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(); + form_end_row(); + } } } } else { - print "" . __('No Audit Log Events Found', 'audit') . "\n"; + print "" . __('No Audit Log Events Found', 'audit') . "\n"; } html_end_box(false); @@ -684,10 +719,10 @@ function audit_log() { = $config['dead_letter_warning'] || @@ -711,6 +746,7 @@ function audit_render_syslog_health() { print '' . __('Last Socket Write', 'audit') . '' . html_escape($health['last_sent'] ?? __('Never', 'audit')) . ''; print ''; print " '; + if ($health['dead_letter'] > 0) { print "'; diff --git a/audit_functions.php b/audit_functions.php index 90e7eef..db65fe9 100644 --- a/audit_functions.php +++ b/audit_functions.php @@ -1,125 +1,134 @@ $selected_items + */ +function audit_process_page_data(string $page, mixed $drop_action, array $selected_items): string { + $objects = []; + if ($drop_action !== false) { switch ($page) { case 'host.php': - //loop over array and perform query for each item + // loop over array and perform query for each item foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT id AS host_id,site_id,description,hostname,status,status_fail_date AS last_failed_date,status_rec_date AS last_recovered_date FROM host WHERE id IN (?)', - array($item)); - } + [$item]); + } + break; case 'host_templates.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM host_template WHERE id IN (?)', - array($item)); + [$item]); } - break; - case 'templates_export.php': - foreach ($selected_items as $item) { - $objects[] = db_fetch_assoc_prepared('SELECT name FROM graph_templates + break; + case 'templates_export.php': + foreach ($selected_items as $item) { + $objects[] = db_fetch_assoc_prepared('SELECT name FROM graph_templates WHERE id IN (?)', - array($item)); - } - break; - + [$item]); + } - case 'automation_devices.php': - foreach ($selected_items as $item) { - $result = db_fetch_assoc_prepared('SELECT id, network_id,hostname,ip,sysName,syslocation,snmp,up + break; + case 'automation_devices.php': + foreach ($selected_items as $item) { + $result = db_fetch_assoc_prepared('SELECT id, network_id,hostname,ip,sysName,syslocation,snmp,up FROM automation_devices WHERE id IN (?)', - array($item)); + [$item]); + if (is_array($result)) { foreach ($result as &$row) { $row['snmp'] = ($row['snmp'] == 1) ? 'UP' : 'Down'; - $row['up'] = ($row['up'] == 1) ? 'Yes' : 'No'; + $row['up'] = ($row['up'] == 1) ? 'Yes' : 'No'; } $objects[] = $result; } - break; - + } + break; case 'graph_templates.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM graph_templates WHERE id IN (?)', - array($item)); + [$item]); } - break; + break; case 'thold.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT id,name_cache AS THOLD_NAME,data_source_name AS Data_Source FROM thold_data WHERE id IN (?)', - array($item)); + [$item]); } + break; case 'data_sources.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('select name_cache AS Data_Source_Name,active from data_template_data WHERE local_data_id IN (?)', - array($item)); + [$item]); } - break; + break; case 'data_templates.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM data_template WHERE id IN (?)', - array($item)); + [$item]); } - break; + break; case 'aggregate_templates.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM aggregate_graph_template WHERE id IN (?)', - array($item)); + [$item]); } - break; + break; case 'thold_templates.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM thold_template WHERE id IN (?)', - array($item)); + [$item]); } + break; case 'user_admin.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT username FROM user_auth WHERE id IN (?)', - array($item)); + [$item]); } + break; case 'user_group_admin.php': foreach ($selected_items as $item) { $objects[] = db_fetch_assoc_prepared('SELECT name FROM user_auth_group WHERE id IN (?)', - array($item)); + [$item]); } + break; } } @@ -127,16 +136,16 @@ function audit_process_page_data($page, $drop_action, $selected_items) { return audit_json_encode($objects); } -function audit_is_sensitive_key($key) { +function audit_is_sensitive_key(mixed $key): int|false { return preg_match('/(?:pass(?:word)?|phrase|token|secret|api[_-]?key|private[_-]?key|community|credential|authorization|authentication)/i', (string) $key); } -function audit_redact_sensitive_data($data) { +function audit_redact_sensitive_data(mixed $data): mixed { if (!is_array($data)) { return $data; } - $redacted = array(); + $redacted = []; foreach ($data as $key => $value) { if (audit_is_sensitive_key($key)) { @@ -151,7 +160,7 @@ function audit_redact_sensitive_data($data) { return $redacted; } -function audit_redact_sensitive_value($value) { +function audit_redact_sensitive_value(mixed $value): mixed { if (!is_string($value)) { return $value; } @@ -165,9 +174,9 @@ function audit_redact_sensitive_value($value) { return preg_replace('#^([a-z][a-z0-9+.-]*://[^:/@\s]+):[^@\s]+@#i', '$1:[REDACTED]@', $value); } -function audit_bound_log_data($data, $depth = 0, $state = null) { +function audit_bound_log_data(mixed $data, int $depth = 0, ?object $state = null): mixed { if ($state === null) { - $state = (object) array('fields' => 0); + $state = (object) ['fields' => 0]; } if ($depth >= 12) { @@ -175,11 +184,13 @@ function audit_bound_log_data($data, $depth = 0, $state = null) { } if (is_array($data)) { - $bounded = array(); + $bounded = []; foreach ($data as $key => $value) { + // @phpstan-ignore-next-line (property.notFound: dynamic property on stdClass state object) if ($state->fields >= 1000) { $bounded['audit_truncated'] = 'Additional fields were omitted.'; + break; } @@ -197,66 +208,76 @@ function audit_bound_log_data($data, $depth = 0, $state = null) { return $data; } -function audit_redact_cli_arguments($arguments) { - $redacted = array(); +/** + * @param array $arguments + * @return array + */ +function audit_redact_cli_arguments(array $arguments): array { + $redacted = []; $redact_next = false; foreach ($arguments as $argument) { if ($redact_next) { - $redacted[] = '[REDACTED]'; + $redacted[] = '[REDACTED]'; $redact_next = false; + continue; } if (preg_match('/^(--?[^=]*(?:pass(?:word)?|phrase|token|secret|api[_-]?key|private[_-]?key|community|credential|authorization|authentication)[^=]*)=(.*)$/i', $argument, $matches)) { $redacted[] = $matches[1] . '=[REDACTED]'; + continue; } if (preg_match('/^--?[^=]*(?:pass(?:word)?|phrase|token|secret|api[_-]?key|private[_-]?key|community|credential|authorization|authentication)/i', $argument)) { - $redacted[] = $argument; + $redacted[] = $argument; $redact_next = true; + continue; } - $redacted[] = preg_replace('#^([a-z][a-z0-9+.-]*://[^:/@\s]+):[^@\s]+@#i', '$1:[REDACTED]@', $argument); + $redacted[] = preg_replace('#^([a-z][a-z0-9+.-]*://[^:/@\s]+):[^@\s]+@#i', '$1:[REDACTED]@', $argument) ?? $argument; } return $redacted; } -function audit_json_encode($data, $options = 0) { +function audit_json_encode(mixed $data, int $options = 0): string { $json = json_encode(audit_bound_log_data($data), JSON_INVALID_UTF8_SUBSTITUTE | $options, 16); if ($json === false) { - return json_encode(array('audit_encoding_error' => json_last_error_msg())); + $fallback = json_encode(['audit_encoding_error' => json_last_error_msg()]); + + return $fallback !== false ? $fallback : '{}'; } return $json; } -function audit_json_decode($json, &$error = null) { +function audit_json_decode(mixed $json, ?string &$error = null): mixed { $error = null; try { return json_decode($json, true, 16, JSON_THROW_ON_ERROR); } catch (Throwable $exception) { $error = $exception->getMessage(); + return null; } } -function audit_uuid_v4() { - $bytes = random_bytes(16); +function audit_uuid_v4(): string { + $bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); - $hex = bin2hex($bytes); + $hex = bin2hex($bytes); return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20); } -function audit_request_correlation_id() { +function audit_request_correlation_id(): string { static $correlation_id; if ($correlation_id === null) { @@ -266,7 +287,7 @@ function audit_request_correlation_id() { return $correlation_id; } -function audit_utc_time($microtime = null) { +function audit_utc_time(?float $microtime = null): string { $microtime = $microtime === null ? microtime(true) : $microtime; $seconds = (int) $microtime; $micros = (int) round(($microtime - $seconds) * 1000000); @@ -279,8 +300,11 @@ function audit_utc_time($microtime = null) { return gmdate('Y-m-d H:i:s', $seconds) . '.' . sprintf('%06d', $micros); } -function audit_event_integrity_hash($event) { - $material = array( +/** + * @param array $event + */ +function audit_event_integrity_hash(array $event): string { + $material = [ 'event_uuid' => $event['event_uuid'] ?? '', 'correlation_id' => $event['correlation_id'] ?? '', 'event_type' => $event['event_type'] ?? '', @@ -291,31 +315,35 @@ function audit_event_integrity_hash($event) { 'target_type' => $event['target_type'] ?? '', 'target_id' => $event['target_id'] ?? '', 'details' => $event['details'] ?? '' - ); + ]; return hash('sha256', audit_json_encode($material, JSON_UNESCAPED_SLASHES)); } -function audit_event_type_for_request($page, $action) { +function audit_event_type_for_request(mixed $page, mixed $action): string { $page_name = preg_replace('/\.php$/', '', (string) $page); - $page_name = preg_replace('/[^a-z0-9_]+/i', '_', $page_name); + $page_name = preg_replace('/[^a-z0-9_]+/i', '_', $page_name ?? ''); $verb = preg_replace('/[^a-z0-9_]+/i', '_', strtolower((string) $action)); - $verb = trim($verb, '_'); + $verb = trim($verb ?? '', '_'); return 'cacti.' . ($page_name !== '' ? $page_name : 'request') . '.' . ($verb !== '' && $verb !== 'none' ? $verb : 'submitted'); } -function audit_external_event_data($event) { - $fields = array( +/** + * @param array $event + * @return array + */ +function audit_external_event_data(array $event): array { + $fields = [ 'id', 'event_uuid', 'correlation_id', 'event_type', 'event_category', 'severity', 'actor_type', 'page', 'user_id', 'action', 'request_status', 'operation_outcome', 'outcome_reason', 'target_type', 'target_id', 'ip_address', 'user_agent', 'http_method', 'http_status', 'event_time', 'completed_time', 'duration_ms', 'post', 'object_data', 'details', 'previous_hash', 'integrity_hash' - ); - $data = array(); + ]; + $data = []; foreach ($fields as $field) { $data[$field] = $event[$field] ?? null; @@ -324,9 +352,12 @@ function audit_external_event_data($event) { return $data; } -function audit_external_log_format($data, $format = 'json') { +/** + * @param array $data + */ +function audit_external_log_format(array $data, string $format = 'json'): string { if ($format === 'text') { - $fields = array(); + $fields = []; foreach ($data as $name => $value) { if (is_array($value) || is_object($value)) { @@ -338,8 +369,8 @@ function audit_external_log_format($data, $format = 'json') { } $value = str_replace( - array('\\', "\r", "\n", "\t", '"'), - array('\\\\', '\r', '\n', '\t', '\"'), + ['\\', "\r", "\n", "\t", '"'], + ['\\\\', '\r', '\n', '\t', '\"'], (string) $value ); $fields[] = $name . '="' . $value . '"'; @@ -348,7 +379,7 @@ function audit_external_log_format($data, $format = 'json') { return implode(' ', $fields) . "\n"; } - foreach (array('post', 'object_data', 'details') as $name) { + foreach (['post', 'object_data', 'details'] as $name) { if (isset($data[$name]) && is_string($data[$name])) { $decoded = audit_json_decode($data[$name], $error); @@ -361,7 +392,7 @@ function audit_external_log_format($data, $format = 'json') { return audit_json_encode($data, JSON_UNESCAPED_SLASHES) . "\n"; } -function audit_csv_safe_cell($value) { +function audit_csv_safe_cell(mixed $value): string { $value = (string) $value; if (preg_match('/^[=+\-@]/', ltrim($value))) { @@ -371,7 +402,7 @@ function audit_csv_safe_cell($value) { return $value; } -function audit_retention_cutoff($retention, $now = null) { +function audit_retention_cutoff(mixed $retention, ?DateTimeImmutable $now = null): DateTimeImmutable { $now = $now instanceof DateTimeImmutable ? $now->setTimezone(new DateTimeZone('UTC')) : new DateTimeImmutable('now', new DateTimeZone('UTC')); @@ -379,20 +410,24 @@ function audit_retention_cutoff($retention, $now = null) { return $now->sub(new DateInterval('P' . max(0, (int) $retention) . 'D')); } -function audit_append_external_log($path, $message) { +/** + * @return array + */ +function audit_append_external_log(string $path, string $message): array { if ($path == '' || !is_file($path) || is_link($path)) { - return array('status' => 'failed', 'error' => 'Destination is not a regular file or is a symbolic link.'); + return ['status' => 'failed', 'error' => 'Destination is not a regular file or is a symbolic link.']; } $written = file_put_contents($path, $message, FILE_APPEND | LOCK_EX); + if ($written !== strlen($message)) { - return array('status' => 'failed', 'error' => 'Unable to append a complete record.'); + return ['status' => 'failed', 'error' => 'Unable to append a complete record.']; } - return array('status' => 'delivered', 'error' => ''); + return ['status' => 'delivered', 'error' => '']; } -function audit_set_external_status($id, $status, $error = '') { +function audit_set_external_status(int $id, string $status, string $error = ''): void { db_execute_prepared('UPDATE audit_log SET external_status = ?, external_error = ?, @@ -400,22 +435,25 @@ function audit_set_external_status($id, $status, $error = '') { external_last_attempt = UTC_TIMESTAMP(6), external_delivered_time = CASE WHEN ? = "delivered" THEN UTC_TIMESTAMP(6) ELSE external_delivered_time END WHERE id = ?', - array($status, $error, $status, $id)); + [$status, $error, $status, $id]); } -function audit_deliver_external_event($id) { +function audit_deliver_external_event(int $id): void { if (read_config_option('audit_log_external') != 'on') { return; } - $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', array($id)); - if (!cacti_sizeof($event) || $event['request_status'] == 'started') { + $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', [$id]); + + if (!is_array($event) || $event === [] || ($event['request_status'] ?? '') === 'started') { return; } $path = read_config_option('audit_log_external_path'); + if ($path == '' || !is_file($path) || is_link($path)) { audit_set_external_status($id, 'failed', 'Destination is not a regular file or is a symbolic link.'); + return; } @@ -425,12 +463,13 @@ function audit_deliver_external_event($id) { audit_set_external_status($id, $delivery['status'], $delivery['error']); } -function audit_retry_external_logs() { +function audit_retry_external_logs(): void { if (read_config_option('audit_log_external') != 'on') { return; } $path = read_config_option('audit_log_external_path'); + if ($path == '' || !is_file($path) || is_link($path)) { return; } @@ -445,19 +484,24 @@ function audit_retry_external_logs() { ORDER BY id LIMIT 100"); - foreach ($events as $event) { - $message = audit_external_log_format(audit_external_event_data($event), $format); - $delivery = audit_append_external_log($path, $message); - audit_set_external_status($event['id'], $delivery['status'], $delivery['error']); + if (is_array($events)) { + foreach ($events as $event) { + $message = audit_external_log_format(audit_external_event_data($event), $format); + $delivery = audit_append_external_log($path, $message); + audit_set_external_status($event['id'], $delivery['status'], $delivery['error']); - if ($delivery['status'] != 'delivered') { - break; + if ($delivery['status'] != 'delivered') { + break; + } } } } -function audit_request_status($error = null, $status_code = 200) { - $fatal_types = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR); +/** + * @param array $error + */ +function audit_request_status(?array $error = null, int $status_code = 200): string { + $fatal_types = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR]; if ((is_array($error) && in_array($error['type'] ?? null, $fatal_types, true)) || $status_code >= 400) { @@ -467,21 +511,27 @@ function audit_request_status($error = null, $status_code = 200) { return 'completed'; } -function audit_operation_verifier_for_request($page, $post) { +/** + * @param array $post + * @return array|null + */ +function audit_operation_verifier_for_request(string $page, array $post): ?array { if ($page != 'user_admin.php' || !array_key_exists('save_component_realm_perms', $post)) { return null; } $target_user_id = $post['id'] ?? null; + if (!is_scalar($target_user_id) || !preg_match('/^[1-9][0-9]*$/', (string) $target_user_id)) { - return array( - 'type' => 'invalid', + return [ + 'type' => 'invalid', 'outcome_reason' => 'realm_permissions_request_invalid' - ); + ]; } - $expected_realm_ids = array(); + $expected_realm_ids = []; + foreach ($post as $field => $value) { $field = (string) $field; @@ -490,10 +540,10 @@ function audit_operation_verifier_for_request($page, $post) { } if (!preg_match('/^section([1-9][0-9]*)$/', $field, $matches)) { - return array( - 'type' => 'invalid', + return [ + 'type' => 'invalid', 'outcome_reason' => 'realm_permissions_request_invalid' - ); + ]; } $expected_realm_ids[] = (int) $matches[1]; @@ -502,55 +552,59 @@ function audit_operation_verifier_for_request($page, $post) { $expected_realm_ids = array_values(array_unique($expected_realm_ids)); sort($expected_realm_ids, SORT_NUMERIC); - return array( - 'type' => 'user_realm_permissions', - 'target_user_id' => (int) $target_user_id, + return [ + 'type' => 'user_realm_permissions', + 'target_user_id' => (int) $target_user_id, 'expected_realm_ids' => $expected_realm_ids - ); + ]; } -function audit_verify_operation($verifier) { +/** + * @return array + */ +function audit_verify_operation(mixed $verifier): array { if (!is_array($verifier) || empty($verifier['type'])) { - return array('outcome' => 'unknown', 'reason' => null); + return ['outcome' => 'unknown', 'reason' => null]; } if ($verifier['type'] == 'invalid') { - return array( + return [ 'outcome' => 'unknown', - 'reason' => $verifier['outcome_reason'] ?? 'verification_request_invalid' - ); + 'reason' => $verifier['outcome_reason'] ?? 'verification_request_invalid' + ]; } if ($verifier['type'] != 'user_realm_permissions') { - return array('outcome' => 'unknown', 'reason' => 'verification_type_unsupported'); + return ['outcome' => 'unknown', 'reason' => 'verification_type_unsupported']; } - $target_user_id = (int) ($verifier['target_user_id'] ?? 0); - $expected_realm_ids = $verifier['expected_realm_ids'] ?? array(); - $user_count = db_fetch_cell_prepared('SELECT COUNT(*) FROM user_auth WHERE id = ?', array($target_user_id)); + $target_user_id = (int) ($verifier['target_user_id'] ?? 0); + $expected_realm_ids = $verifier['expected_realm_ids'] ?? []; + $user_count = db_fetch_cell_prepared('SELECT COUNT(*) FROM user_auth WHERE id = ?', [$target_user_id]); if ($user_count === false) { - return array('outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'); + return ['outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed']; } if ((int) $user_count !== 1) { - return array('outcome' => 'failure', 'reason' => 'target_user_not_found'); + return ['outcome' => 'failure', 'reason' => 'target_user_not_found']; } $rows = db_fetch_assoc_prepared('SELECT realm_id FROM user_auth_realm WHERE user_id = ? ORDER BY realm_id', - array($target_user_id)); + [$target_user_id]); if (!is_array($rows)) { - return array('outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'); + return ['outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed']; } - $actual_realm_ids = array(); + $actual_realm_ids = []; + foreach ($rows as $row) { if (!isset($row['realm_id']) || !is_numeric($row['realm_id'])) { - return array('outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'); + return ['outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed']; } $actual_realm_ids[] = (int) $row['realm_id']; @@ -560,26 +614,29 @@ function audit_verify_operation($verifier) { sort($actual_realm_ids, SORT_NUMERIC); if ($actual_realm_ids === $expected_realm_ids) { - return array('outcome' => 'success', 'reason' => 'realm_permissions_verified'); + return ['outcome' => 'success', 'reason' => 'realm_permissions_verified']; } - return array('outcome' => 'failure', 'reason' => 'realm_permissions_mismatch'); + return ['outcome' => 'failure', 'reason' => 'realm_permissions_mismatch']; } -function audit_finalize_request($id, $started_at = null, $verifier = null) { - $status_code = http_response_code(); - $status_code = is_int($status_code) ? $status_code : 200; +/** + * @param array|null $verifier + */ +function audit_finalize_request(int $id, ?float $started_at = null, ?array $verifier = null): void { + $status_code = http_response_code(); + $status_code = is_int($status_code) ? $status_code : 200; $request_status = audit_request_status(error_get_last(), $status_code); - $outcome = $request_status == 'failed' ? 'failure' : 'unknown'; + $outcome = $request_status == 'failed' ? 'failure' : 'unknown'; $outcome_reason = $request_status == 'failed' ? 'request_failed' : null; if ($request_status == 'completed' && $verifier !== null) { - $verification = audit_verify_operation($verifier); - $outcome = $verification['outcome']; + $verification = audit_verify_operation($verifier); + $outcome = $verification['outcome']; $outcome_reason = $verification['reason']; } - $duration_ms = $started_at === null ? null : max(0, (int) round((microtime(true) - $started_at) * 1000)); + $duration_ms = $started_at === null ? null : max(0, (int) round((microtime(true) - $started_at) * 1000)); $completed_time = audit_utc_time(); db_execute_prepared("UPDATE audit_log @@ -591,19 +648,23 @@ function audit_finalize_request($id, $started_at = null, $verifier = null) { duration_ms = ? WHERE id = ? AND request_status = 'started'", - array($request_status, $outcome_reason, $outcome, $status_code, $completed_time, $duration_ms, $id)); + [$request_status, $outcome_reason, $outcome, $status_code, $completed_time, $duration_ms, $id]); - $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', array($id)); - if (cacti_sizeof($event)) { + $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', [$id]); + + if (is_array($event)) { db_execute_prepared('UPDATE audit_log SET integrity_hash = ? WHERE id = ?', - array(audit_event_integrity_hash($event), $id)); + [audit_event_integrity_hash($event), $id]); } audit_deliver_external_event($id); audit_enqueue_syslog_event($id); } -function audit_record_event($event_type, $options = array()) { +/** + * @param array $options + */ +function audit_record_event(string $event_type, array $options = []): int { if (read_config_option('audit_enabled') != 'on') { return 0; } @@ -615,7 +676,7 @@ function audit_record_event($event_type, $options = array()) { $event_suffix = strrchr($event_type, '.'); $action = $options['action'] ?? ($event_suffix === false ? $event_type : substr($event_suffix, 1)); $event_time = $options['event_time'] ?? audit_utc_time(); - $details = audit_json_encode(audit_redact_sensitive_data($options['details'] ?? array())); + $details = audit_json_encode(audit_redact_sensitive_data($options['details'] ?? [])); $external = read_config_option('audit_log_external') == 'on'; $ip_address = $options['ip_address'] ?? (function_exists('get_client_addr') ? get_client_addr() : ''); $user_agent = $options['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? ''); @@ -627,7 +688,7 @@ function audit_record_event($event_type, $options = array()) { operation_outcome, outcome_reason, http_method, http_status, completed_time, duration_ms, details ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - array( + [ $page, $user_id, $action, 'completed', $ip_address, $user_agent, $event_time, '{}', '[]', $external ? 'pending' : 'disabled', $event_uuid, $correlation_id, $event_type, $options['event_category'] ?? 'security', @@ -637,13 +698,14 @@ function audit_record_event($event_type, $options = array()) { $options['http_method'] ?? ($_SERVER['REQUEST_METHOD'] ?? null), $options['http_status'] ?? null, $options['completed_time'] ?? $event_time, $options['duration_ms'] ?? 0, $details - )); + ]); $id = db_fetch_insert_id(); - $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', array($id)); - if (cacti_sizeof($event)) { + $event = db_fetch_row_prepared('SELECT * FROM audit_log WHERE id = ?', [$id]); + + if (is_array($event)) { db_execute_prepared('UPDATE audit_log SET integrity_hash = ? WHERE id = ?', - array(audit_event_integrity_hash($event), $id)); + [audit_event_integrity_hash($event), $id]); } audit_deliver_external_event($id); audit_enqueue_syslog_event($id); @@ -651,19 +713,19 @@ function audit_record_event($event_type, $options = array()) { return $id; } -function audit_logout_pre_session_destroy() { +function audit_logout_pre_session_destroy(): void { $reason = get_nfilter_request_var('action', 'user'); $type = $reason == 'timeout' ? 'authentication.session.expired' : 'authentication.logout'; - audit_record_event($type, array( + audit_record_event($type, [ 'event_category' => 'authentication', - 'action' => $reason == 'timeout' ? 'timeout' : 'logout', - 'details' => array('reason' => $reason) - )); + 'action' => $reason == 'timeout' ? 'timeout' : 'logout', + 'details' => ['reason' => $reason] + ]); } -function audit_enforce_syslog_settings_request() { - $page = basename($_SERVER['SCRIPT_NAME'] ?? ''); +function audit_enforce_syslog_settings_request(): void { + $page = basename($_SERVER['SCRIPT_NAME'] ?? ''); $method = $_SERVER['REQUEST_METHOD'] ?? ''; if ($page !== 'settings.php' || $method !== 'POST') { @@ -671,15 +733,18 @@ function audit_enforce_syslog_settings_request() { } $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); - $post = is_array($post) ? $post : array(); + $post = is_array($post) ? $post : []; + if (($post['action'] ?? '') !== 'save' || ($post['tab'] ?? '') !== 'audit') { return; } $has_syslog_fields = false; + foreach ($post as $name => $value) { if (strpos((string) $name, 'audit_syslog_') === 0) { $has_syslog_fields = true; + break; } } @@ -689,48 +754,48 @@ function audit_enforce_syslog_settings_request() { } if (!audit_user_is_admin()) { - audit_record_event('audit.syslog.configuration.denied', array( - 'event_category' => 'audit', - 'severity' => 'warning', - 'action' => 'save', - 'target_type' => 'syslog_configuration', + audit_record_event('audit.syslog.configuration.denied', [ + 'event_category' => 'audit', + 'severity' => 'warning', + 'action' => 'save', + 'target_type' => 'syslog_configuration', 'operation_outcome' => 'failure', - 'outcome_reason' => 'audit_admin_required' - )); + 'outcome_reason' => 'audit_admin_required' + ]); http_response_code(403); exit; } - $names = array( + $names = [ 'receiver', 'port', 'transport', 'format', 'facility', 'application', 'node_id', 'timeout', 'udp_max_size', 'retry_base', 'retry_max', 'max_attempts', 'batch_size', 'pending_age_warning', 'dead_letter_warning', 'tls_ca_file', 'tls_client_cert', 'tls_client_key' - ); - $overrides = array(); + ]; + $overrides = []; foreach ($names as $name) { - $setting = 'audit_syslog_' . $name; + $setting = 'audit_syslog_' . $name; $overrides[$name] = isset($post[$setting]) && is_scalar($post[$setting]) ? (string) $post[$setting] : ''; } - $config = audit_syslog_config($overrides); - $enabling = isset($post['audit_syslog_enabled']) && $post['audit_syslog_enabled'] === 'on'; - $configuring = trim($overrides['receiver']) !== ''; + $config = audit_syslog_config($overrides); + $enabling = isset($post['audit_syslog_enabled']) && $post['audit_syslog_enabled'] === 'on'; + $configuring = trim($overrides['receiver'] ?? '') !== ''; if (($enabling || $configuring) && !$config['valid']) { - audit_record_event('audit.syslog.configuration.rejected', array( - 'event_category' => 'audit', - 'severity' => 'warning', - 'action' => 'save', - 'target_type' => 'syslog_configuration', + audit_record_event('audit.syslog.configuration.rejected', [ + 'event_category' => 'audit', + 'severity' => 'warning', + 'action' => 'save', + 'target_type' => 'syslog_configuration', 'operation_outcome' => 'failure', - 'outcome_reason' => 'configuration_invalid', - 'details' => array('errors' => $config['errors']) - )); + 'outcome_reason' => 'configuration_invalid', + 'details' => ['errors' => $config['errors']] + ]); raise_message( 'audit_syslog_configuration', @@ -742,38 +807,36 @@ function audit_enforce_syslog_settings_request() { } } - - -function audit_config_insert() { +function audit_config_insert(): void { global $action, $config; audit_enforce_syslog_settings_request(); if (audit_log_valid_event()) { $started_at = microtime(true); - /* prepare post */ + // prepare post $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); - $post = is_array($post) ? $post : array(); + $post = is_array($post) ? $post : []; - /* remove unsafe variables */ + // remove unsafe variables unset($post['__csrf_magic']); unset($post['header']); $post = audit_redact_sensitive_data($post); - /* check if drp_action is present and update action accordingly */ + // check if drp_action is present and update action accordingly if (isset($post['drp_action']) && $post['drp_action'] == 1) { $action = 'delete'; - } else if (isset($post['drp_action']) && $post['drp_action'] == 4) { + } elseif (isset($post['drp_action']) && $post['drp_action'] == 4) { $action = 'disable'; } - /* sanitize and serialize selected items */ + // sanitize and serialize selected items if (isset($post['selected_items']) && is_string($post['selected_items'])) { - $selected_items = @unserialize(stripslashes($post['selected_items']), array('allowed_classes' => false)); - $selected_items = is_array($selected_items) ? $selected_items : array(); + $selected_items = @unserialize(stripslashes($post['selected_items']), ['allowed_classes' => false]); + $selected_items = is_array($selected_items) ? $selected_items : []; $drop_action = $post['drp_action'] ?? false; } else { - $selected_items = array(); + $selected_items = []; $drop_action = false; } @@ -784,10 +847,10 @@ function audit_config_insert() { $user_id = (isset($_SESSION['sess_user_id']) ? $_SESSION['sess_user_id'] : 0); $event_time = audit_utc_time($started_at); - /* Retrieve IP address */ + // Retrieve IP address $ip_address = get_client_addr(); - /* Get the User Agent */ + // Get the User Agent $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (empty($action) && isset_request_var('action')) { @@ -803,9 +866,11 @@ function audit_config_insert() { switch ($drop_action) { case 2: $action = 'Delete Device'; + break; case 1: $action = 'Create Device'; + break; } @@ -814,16 +879,18 @@ function audit_config_insert() { switch ($drop_action) { case 2: $action = 'Host Enabled'; + break; case 3: $action = 'Host Disabled'; + break; } break; } - $audit_log = read_config_option('audit_log_external_path'); + $audit_log = read_config_option('audit_log_external_path'); $external_logging = read_config_option('audit_log_external') == 'on'; $external_status = $external_logging ? 'pending' : 'disabled'; @@ -836,20 +903,20 @@ function audit_config_insert() { $event_uuid = audit_uuid_v4(); $correlation_id = audit_request_correlation_id(); $event_type = audit_event_type_for_request($page, $action); - $category = in_array($page, array('user_admin.php', 'user_group_admin.php'), true) ? 'identity_access' : 'configuration'; + $category = in_array($page, ['user_admin.php', 'user_group_admin.php'], true) ? 'identity_access' : 'configuration'; db_execute_prepared('INSERT INTO audit_log ( page, user_id, action, request_status, ip_address, user_agent, event_time, post, object_data, external_status, event_uuid, correlation_id, event_type, event_category, severity, actor_type, target_type, target_id, operation_outcome, http_method ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - array( + [ $page, $user_id, $action, 'started', $ip_address, $user_agent, $event_time, $post, $object_data, $external_status, $event_uuid, $correlation_id, $event_type, $category, 'info', $user_id ? 'user' : 'system', preg_replace('/\.php$/', '', $page), $target_id, 'unknown', $_SERVER['REQUEST_METHOD'] ?? null - )); + ]); $audit_id = db_fetch_insert_id(); register_shutdown_function('audit_finalize_request', $audit_id, $started_at, $verifier); @@ -861,6 +928,7 @@ function audit_config_insert() { if ($external_logging && $audit_log != '' && !file_exists($audit_log)) { if (is_writable(dirname($audit_log))) { cacti_log(sprintf('NOTE: The Audit Log file \'%s\' does not exist. Creating it.', $audit_log), false, 'AUDIT'); + if (!touch($audit_log)) { cacti_log(sprintf('ERROR: Unable to create Audit Log file \'%s\'.', $audit_log), false, 'AUDIT'); } else { @@ -882,18 +950,17 @@ function audit_config_insert() { $page = basename($arguments[0]); $user_id = 0; $action = 'cli'; - $ip_address = getHostByName(php_uname('n')); + $ip_address = gethostbyname(php_uname('n')); $user_agent = get_current_user(); $event_time = audit_utc_time($started_at); $post = implode(' ', $arguments); - /* don't insert poller records */ - if (strpos($arguments[0], 'poller') === false && - strpos($arguments[0], 'cmd.php') === false && - strpos($arguments[0], '/scripts/') === false && + // don't insert poller records + if (strpos($arguments[0], 'poller') === false && + strpos($arguments[0], 'cmd.php') === false && + strpos($arguments[0], '/scripts/') === false && strpos($arguments[0], 'script_server.php') === false && - strpos($arguments[0], '_process.php') === false) { - + strpos($arguments[0], '_process.php') === false) { $external_status = read_config_option('audit_log_external') == 'on' ? 'pending' : 'disabled'; db_execute_prepared('INSERT INTO audit_log ( page, user_id, action, request_status, ip_address, user_agent, event_time, @@ -901,12 +968,12 @@ function audit_config_insert() { event_category, severity, actor_type, target_type, target_id, operation_outcome ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - array( + [ $page, $user_id, $action, 'started', $ip_address, $user_agent, $event_time, $post, '[]', $external_status, audit_uuid_v4(), audit_request_correlation_id(), 'cacti.cli.executed', 'system', 'info', 'system', 'cli_command', $page, 'unknown' - )); + ]); $audit_id = db_fetch_insert_id(); register_shutdown_function('audit_finalize_request', $audit_id, $started_at); } diff --git a/audit_syslog.php b/audit_syslog.php index f73f406..10ff4f0 100644 --- a/audit_syslog.php +++ b/audit_syslog.php @@ -15,32 +15,38 @@ +-------------------------------------------------------------------------+ */ -function audit_syslog_enabled() { +function audit_syslog_enabled(): bool { return read_config_option('audit_syslog_enabled') == 'on'; } -function audit_syslog_read_setting($name, $default) { +function audit_syslog_read_setting(string $name, mixed $default): mixed { $value = read_config_option($name); return $value === '' || $value === null ? $default : $value; } -function audit_syslog_bounded_integer($value, $default, $minimum, $maximum, &$errors, $name) { +/** + * @param array $errors + */ +function audit_syslog_bounded_integer(mixed $value, int $default, int $minimum, int $maximum, array &$errors, string $name): int { if (!is_scalar($value) || !preg_match('/^[0-9]+$/', (string) $value)) { $errors[] = $name . '_invalid'; + return $default; } $value = (int) $value; + if ($value < $minimum || $value > $maximum) { $errors[] = $name . '_out_of_range'; + return $default; } return $value; } -function audit_syslog_valid_receiver($receiver) { +function audit_syslog_valid_receiver(string $receiver): bool { if ($receiver === '' || strlen($receiver) > 253 || preg_match('/[[:cntrl:][:space:]\\/@]/', $receiver) || strpos($receiver, '://') !== false) { @@ -60,6 +66,7 @@ function audit_syslog_valid_receiver($receiver) { } $labels = explode('.', $receiver); + foreach ($labels as $label) { if ($label === '' || strlen($label) > 63 || !preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $label)) { @@ -70,12 +77,15 @@ function audit_syslog_valid_receiver($receiver) { return true; } -function audit_syslog_valid_header_value($value, $maximum) { +function audit_syslog_valid_header_value(string $value, int $maximum): bool { return $value !== '' && strlen($value) <= $maximum && !preg_match('/[^\\x21-\\x7e]|[\\[\\]="]/', $value); } -function audit_syslog_validate_optional_file($path, $name, &$errors) { +/** + * @param array $errors + */ +function audit_syslog_validate_optional_file(string $path, string $name, array &$errors): string { if ($path === '') { return ''; } @@ -87,93 +97,103 @@ function audit_syslog_validate_optional_file($path, $name, &$errors) { return $path; } -function audit_syslog_config($overrides = array()) { - $defaults = array( - 'receiver' => '', - 'port' => '', - 'transport' => 'udp', - 'format' => 'json', - 'facility' => 'local0', - 'application' => 'cacti-audit', - 'node_id' => php_uname('n'), - 'timeout' => '5', - 'udp_max_size' => '8192', - 'retry_base' => '30', - 'retry_max' => '3600', - 'max_attempts' => '10', - 'batch_size' => '100', +/** + * @param array $overrides + * @return array + */ +function audit_syslog_config(array $overrides = []): array { + $defaults = [ + 'receiver' => '', + 'port' => '', + 'transport' => 'udp', + 'format' => 'json', + 'facility' => 'local0', + 'application' => 'cacti-audit', + 'node_id' => php_uname('n'), + 'timeout' => '5', + 'udp_max_size' => '8192', + 'retry_base' => '30', + 'retry_max' => '3600', + 'max_attempts' => '10', + 'batch_size' => '100', 'pending_age_warning' => '900', 'dead_letter_warning' => '1', - 'tls_ca_file' => '', - 'tls_client_cert' => '', - 'tls_client_key' => '' - ); - $values = array(); + 'tls_ca_file' => '', + 'tls_client_cert' => '', + 'tls_client_key' => '' + ]; + $values = []; foreach ($defaults as $name => $default) { - $setting = 'audit_syslog_' . $name; + $setting = 'audit_syslog_' . $name; $values[$name] = array_key_exists($name, $overrides) ? $overrides[$name] : audit_syslog_read_setting($setting, $default); } - $errors = array(); - $receiver = trim((string) $values['receiver']); + $errors = []; + $receiver = trim((string) ($values['receiver'] ?? '')); + if (!audit_syslog_valid_receiver($receiver)) { $errors[] = 'receiver_invalid'; } - $transport = strtolower((string) $values['transport']); - if (!in_array($transport, array('udp', 'tcp', 'tls'), true)) { - $errors[] = 'transport_invalid'; + $transport = strtolower((string) ($values['transport'] ?? '')); + + if (!in_array($transport, ['udp', 'tcp', 'tls'], true)) { + $errors[] = 'transport_invalid'; $transport = 'udp'; } - $format = strtolower((string) $values['format']); - if (!in_array($format, array('rfc5424', 'cef', 'json'), true)) { + $format = strtolower((string) ($values['format'] ?? '')); + + if (!in_array($format, ['rfc5424', 'cef', 'json'], true)) { $errors[] = 'format_invalid'; - $format = 'json'; + $format = 'json'; } $facility_map = audit_syslog_facilities(); - $facility = strtolower((string) $values['facility']); + $facility = strtolower((string) ($values['facility'] ?? '')); + if (!isset($facility_map[$facility])) { $errors[] = 'facility_invalid'; $facility = 'local0'; } - $application = trim((string) $values['application']); + $application = trim((string) ($values['application'] ?? '')); + if (!audit_syslog_valid_header_value($application, 48)) { - $errors[] = 'application_invalid'; + $errors[] = 'application_invalid'; $application = 'cacti-audit'; } - $node_id = trim((string) $values['node_id']); + $node_id = trim((string) ($values['node_id'] ?? '')); + if (!audit_syslog_valid_header_value($node_id, 255)) { $errors[] = 'node_id_invalid'; - $node_id = 'cacti'; + $node_id = 'cacti'; } - $port = trim((string) $values['port']) === '' + $port = trim((string) ($values['port'] ?? '')) === '' ? ($transport === 'tls' ? 6514 : 514) - : audit_syslog_bounded_integer($values['port'], $transport === 'tls' ? 6514 : 514, 1, 65535, $errors, 'port'); - $timeout = audit_syslog_bounded_integer($values['timeout'], 5, 1, 30, $errors, 'timeout'); - $udp_max_size = audit_syslog_bounded_integer($values['udp_max_size'], 8192, 512, 65507, $errors, 'udp_max_size'); - $retry_base = audit_syslog_bounded_integer($values['retry_base'], 30, 1, 3600, $errors, 'retry_base'); - $retry_max = audit_syslog_bounded_integer($values['retry_max'], 3600, 1, 86400, $errors, 'retry_max'); - $max_attempts = audit_syslog_bounded_integer($values['max_attempts'], 10, 1, 100, $errors, 'max_attempts'); - $batch_size = audit_syslog_bounded_integer($values['batch_size'], 100, 1, 1000, $errors, 'batch_size'); - $pending_age_warning = audit_syslog_bounded_integer($values['pending_age_warning'], 900, 60, 604800, $errors, 'pending_age_warning'); - $dead_letter_warning = audit_syslog_bounded_integer($values['dead_letter_warning'], 1, 1, 1000000, $errors, 'dead_letter_warning'); + : audit_syslog_bounded_integer($values['port'] ?? '', $transport === 'tls' ? 6514 : 514, 1, 65535, $errors, 'port'); + $timeout = audit_syslog_bounded_integer($values['timeout'] ?? 5, 5, 1, 30, $errors, 'timeout'); + $udp_max_size = audit_syslog_bounded_integer($values['udp_max_size'] ?? 8192, 8192, 512, 65507, $errors, 'udp_max_size'); + $retry_base = audit_syslog_bounded_integer($values['retry_base'] ?? 30, 30, 1, 3600, $errors, 'retry_base'); + $retry_max = audit_syslog_bounded_integer($values['retry_max'] ?? 3600, 3600, 1, 86400, $errors, 'retry_max'); + $max_attempts = audit_syslog_bounded_integer($values['max_attempts'] ?? 10, 10, 1, 100, $errors, 'max_attempts'); + $batch_size = audit_syslog_bounded_integer($values['batch_size'] ?? 100, 100, 1, 1000, $errors, 'batch_size'); + $pending_age_warning = audit_syslog_bounded_integer($values['pending_age_warning'] ?? 900, 900, 60, 604800, $errors, 'pending_age_warning'); + $dead_letter_warning = audit_syslog_bounded_integer($values['dead_letter_warning'] ?? 1, 1, 1, 1000000, $errors, 'dead_letter_warning'); if ($retry_max < $retry_base) { - $errors[] = 'retry_max_less_than_base'; + $errors[] = 'retry_max_less_than_base'; $retry_max = $retry_base; } - $tls_ca_file = trim((string) $values['tls_ca_file']); - $tls_client_cert = trim((string) $values['tls_client_cert']); - $tls_client_key = trim((string) $values['tls_client_key']); + $tls_ca_file = trim((string) ($values['tls_ca_file'] ?? '')); + $tls_client_cert = trim((string) ($values['tls_client_cert'] ?? '')); + $tls_client_key = trim((string) ($values['tls_client_key'] ?? '')); if ($transport === 'tls') { audit_syslog_validate_optional_file($tls_ca_file, 'tls_ca_file', $errors); @@ -185,89 +205,96 @@ function audit_syslog_config($overrides = array()) { } } - $config = array( - 'receiver' => $receiver, - 'port' => $port, - 'transport' => $transport, - 'format' => $format, - 'facility' => $facility, - 'application' => $application, - 'node_id' => $node_id, - 'timeout' => $timeout, - 'udp_max_size' => $udp_max_size, - 'retry_base' => $retry_base, - 'retry_max' => $retry_max, - 'max_attempts' => $max_attempts, - 'batch_size' => $batch_size, - 'pending_age_warning' => $pending_age_warning, - 'dead_letter_warning' => $dead_letter_warning, - 'tls_ca_file' => $tls_ca_file, - 'tls_client_cert' => $tls_client_cert, - 'tls_client_key' => $tls_client_key, - 'poller_id' => defined('POLLER_ID') ? (string) POLLER_ID : '', - 'tls_verify_peer' => true, + $config = [ + 'receiver' => $receiver, + 'port' => $port, + 'transport' => $transport, + 'format' => $format, + 'facility' => $facility, + 'application' => $application, + 'node_id' => $node_id, + 'timeout' => $timeout, + 'udp_max_size' => $udp_max_size, + 'retry_base' => $retry_base, + 'retry_max' => $retry_max, + 'max_attempts' => $max_attempts, + 'batch_size' => $batch_size, + 'pending_age_warning' => $pending_age_warning, + 'dead_letter_warning' => $dead_letter_warning, + 'tls_ca_file' => $tls_ca_file, + 'tls_client_cert' => $tls_client_cert, + 'tls_client_key' => $tls_client_key, + 'poller_id' => defined('POLLER_ID') ? (string) POLLER_ID : '', + 'tls_verify_peer' => true, 'tls_verify_peer_name' => true, - 'errors' => array_values(array_unique($errors)) - ); - $config['valid'] = empty($config['errors']); + 'errors' => array_values(array_unique($errors)) + ]; + $config['valid'] = empty($config['errors']); $config['fingerprint'] = audit_syslog_destination_fingerprint($config); return $config; } -function audit_syslog_destination_fingerprint($config) { - $identity = array( - 'receiver' => $config['receiver'], - 'port' => (int) $config['port'], - 'transport' => $config['transport'], - 'format' => $config['format'], - 'facility' => $config['facility'], - 'application' => $config['application'], - 'node_id' => $config['node_id'], - 'tls_ca_file' => $config['tls_ca_file'], +/** + * @param array $config + */ +function audit_syslog_destination_fingerprint(array $config): string { + $identity = [ + 'receiver' => $config['receiver'], + 'port' => (int) $config['port'], + 'transport' => $config['transport'], + 'format' => $config['format'], + 'facility' => $config['facility'], + 'application' => $config['application'], + 'node_id' => $config['node_id'], + 'tls_ca_file' => $config['tls_ca_file'], 'tls_client_cert' => $config['tls_client_cert'] - ); + ]; return hash('sha256', audit_json_encode($identity, JSON_UNESCAPED_SLASHES)); } -function audit_syslog_facilities() { - return array( - 'kern' => 0, 'user' => 1, 'mail' => 2, 'daemon' => 3, - 'auth' => 4, 'syslog' => 5, 'lpr' => 6, 'news' => 7, - 'uucp' => 8, 'cron' => 9, 'authpriv' => 10, 'ftp' => 11, - 'ntp' => 12, 'audit' => 13, 'alert' => 14, 'clock' => 15, +/** + * @return array + */ +function audit_syslog_facilities(): array { + return [ + 'kern' => 0, 'user' => 1, 'mail' => 2, 'daemon' => 3, + 'auth' => 4, 'syslog' => 5, 'lpr' => 6, 'news' => 7, + 'uucp' => 8, 'cron' => 9, 'authpriv' => 10, 'ftp' => 11, + 'ntp' => 12, 'audit' => 13, 'alert' => 14, 'clock' => 15, 'local0' => 16, 'local1' => 17, 'local2' => 18, 'local3' => 19, 'local4' => 20, 'local5' => 21, 'local6' => 22, 'local7' => 23 - ); + ]; } -function audit_syslog_severity_code($severity) { - $map = array( +function audit_syslog_severity_code(mixed $severity): int { + $map = [ 'emergency' => 0, 'emerg' => 0, 'alert' => 1, 'critical' => 2, - 'crit' => 2, 'error' => 3, 'err' => 3, 'warning' => 4, - 'warn' => 4, 'notice' => 5, 'info' => 6, 'debug' => 7 - ); + 'crit' => 2, 'error' => 3, 'err' => 3, 'warning' => 4, + 'warn' => 4, 'notice' => 5, 'info' => 6, 'debug' => 7 + ]; $severity = strtolower((string) $severity); return isset($map[$severity]) ? $map[$severity] : 6; } -function audit_syslog_header_token($value, $maximum, $fallback) { +function audit_syslog_header_token(mixed $value, int $maximum, string $fallback): string { $value = preg_replace('/[^\\x21-\\x3c\\x3e-\\x5a\\x5e-\\x7e]/', '_', (string) $value); - $value = substr($value, 0, $maximum); + $value = substr($value ?? '', 0, $maximum); return $value === '' ? $fallback : $value; } -function audit_syslog_structured_value($value) { +function audit_syslog_structured_value(mixed $value): string { $value = preg_replace('/[\\x00-\\x1f\\x7f]/', ' ', (string) $value); - return str_replace(array('\\', '"', ']'), array('\\\\', '\\"', '\\]'), $value); + return str_replace(['\\', '"', ']'], ['\\\\', '\\"', '\\]'], $value ?? ''); } -function audit_syslog_timestamp($value) { +function audit_syslog_timestamp(mixed $value): string { $value = (string) $value; + if (preg_match('/^([0-9]{4}-[0-9]{2}-[0-9]{2})[ T]([0-9]{2}:[0-9]{2}:[0-9]{2})(\\.[0-9]{1,6})?/', $value, $matches)) { return $matches[1] . 'T' . $matches[2] . (isset($matches[3]) ? $matches[3] : '') . 'Z'; } @@ -275,41 +302,47 @@ function audit_syslog_timestamp($value) { return gmdate('Y-m-d\\TH:i:s\\Z'); } -function audit_syslog_normalized_data($event, $config) { - $data = audit_external_event_data($event); - $data['node_id'] = $config['node_id']; +/** + * @param array $event + * @param array $config + * @return array + */ +function audit_syslog_normalized_data(array $event, array $config): array { + $data = audit_external_event_data($event); + $data['node_id'] = $config['node_id']; $data['poller_id'] = $config['poller_id'] !== '' ? $config['poller_id'] : null; return $data; } -function audit_syslog_cef_escape_header($value) { - return str_replace(array('\\', '|', "\r", "\n"), array('\\\\', '\\|', ' ', ' '), (string) $value); +function audit_syslog_cef_escape_header(mixed $value): string { + return str_replace(['\\', '|', "\r", "\n"], ['\\\\', '\\|', ' ', ' '], (string) $value); } -function audit_syslog_cef_escape_extension($value) { +function audit_syslog_cef_escape_extension(mixed $value): string { return str_replace( - array('\\', '=', "\r", "\n"), - array('\\\\', '\\=', '\\r', '\\n'), + ['\\', '=', "\r", "\n"], + ['\\\\', '\\=', '\\r', '\\n'], (string) $value ); } -function audit_syslog_cef_severity($severity) { - $map = array( +function audit_syslog_cef_severity(mixed $severity): int { + $map = [ 'emergency' => 10, 'emerg' => 10, 'alert' => 10, - 'critical' => 9, 'crit' => 9, 'error' => 8, 'err' => 8, - 'warning' => 6, 'warn' => 6, 'notice' => 5, - 'info' => 3, 'debug' => 1 - ); + 'critical' => 9, 'crit' => 9, 'error' => 8, 'err' => 8, + 'warning' => 6, 'warn' => 6, 'notice' => 5, + 'info' => 3, 'debug' => 1 + ]; $severity = strtolower((string) $severity); return isset($map[$severity]) ? $map[$severity] : 3; } -function audit_syslog_cef_event_field($value) { +function audit_syslog_cef_event_field(mixed $value): string { if (is_string($value) && $value !== '') { $decoded = audit_json_decode($value, $error); + if ($error === null) { $value = $decoded; } @@ -333,9 +366,13 @@ function audit_syslog_cef_event_field($value) { return audit_redact_sensitive_value((string) $value); } -function audit_syslog_cef_payload($event, $config) { +/** + * @param array $event + * @param array $config + */ +function audit_syslog_cef_payload(array $event, array $config): string { $severity = audit_syslog_cef_severity($event['severity'] ?? 'info'); - $header = array( + $header = [ 'CEF:0', 'Cacti', 'Audit Plugin', @@ -343,30 +380,30 @@ function audit_syslog_cef_payload($event, $config) { $event['event_type'] ?? 'cacti.audit', $event['action'] ?? 'audit', $severity - ); - $extension = array( + ]; + $extension = [ 'externalId' => $event['event_uuid'] ?? '', - 'suser' => $event['user_id'] ?? '', - 'src' => $event['ip_address'] ?? '', - 'act' => $event['action'] ?? '', - 'outcome' => $event['operation_outcome'] ?? '', - 'cs1Label' => 'Correlation ID', - 'cs1' => $event['correlation_id'] ?? '', - 'cs2Label' => 'Target', - 'cs2' => trim(($event['target_type'] ?? '') . ':' . ($event['target_id'] ?? ''), ':'), - 'cs3Label' => 'Node ID', - 'cs3' => $config['node_id'], - 'cn1Label' => 'Poller ID', - 'cn1' => $config['poller_id'], - 'cs4Label' => 'Submitted Data', - 'cs4' => audit_syslog_cef_event_field($event['post'] ?? ''), - 'cs5Label' => 'Object Data', - 'cs5' => audit_syslog_cef_event_field($event['object_data'] ?? ''), - 'cs6Label' => 'Details', - 'cs6' => audit_syslog_cef_event_field($event['details'] ?? '') - ); - $encoded_header = array_map('audit_syslog_cef_escape_header', $header); - $encoded_extension = array(); + 'suser' => $event['user_id'] ?? '', + 'src' => $event['ip_address'] ?? '', + 'act' => $event['action'] ?? '', + 'outcome' => $event['operation_outcome'] ?? '', + 'cs1Label' => 'Correlation ID', + 'cs1' => $event['correlation_id'] ?? '', + 'cs2Label' => 'Target', + 'cs2' => trim(($event['target_type'] ?? '') . ':' . ($event['target_id'] ?? ''), ':'), + 'cs3Label' => 'Node ID', + 'cs3' => $config['node_id'], + 'cn1Label' => 'Poller ID', + 'cn1' => $config['poller_id'], + 'cs4Label' => 'Submitted Data', + 'cs4' => audit_syslog_cef_event_field($event['post'] ?? ''), + 'cs5Label' => 'Object Data', + 'cs5' => audit_syslog_cef_event_field($event['object_data'] ?? ''), + 'cs6Label' => 'Details', + 'cs6' => audit_syslog_cef_event_field($event['details'] ?? '') + ]; + $encoded_header = array_map('audit_syslog_cef_escape_header', $header); + $encoded_extension = []; foreach ($extension as $name => $value) { $encoded_extension[] = $name . '=' . audit_syslog_cef_escape_extension($value); @@ -375,7 +412,11 @@ function audit_syslog_cef_payload($event, $config) { return implode('|', $encoded_header) . '|' . implode(' ', $encoded_extension); } -function audit_syslog_message_payload($event, $config) { +/** + * @param array $event + * @param array $config + */ +function audit_syslog_message_payload(array $event, array $config): string { if ($config['format'] === 'cef') { return audit_syslog_cef_payload($event, $config); } @@ -387,37 +428,42 @@ function audit_syslog_message_payload($event, $config) { return 'Audit event ' . (string) ($event['event_uuid'] ?? ''); } -function audit_syslog_record($event, $config) { +/** + * @param array $event + * @param array $config + * @return array + */ +function audit_syslog_record(array $event, array $config): array { if (empty($config['valid'])) { - return array( - 'status' => 'failed', - 'permanent' => true, + return [ + 'status' => 'failed', + 'permanent' => true, 'error_code' => 'configuration_invalid', - 'error' => implode(',', $config['errors']), - 'record' => '' - ); + 'error' => implode(',', $config['errors']), + 'record' => '' + ]; } $facilities = audit_syslog_facilities(); - $priority = ($facilities[$config['facility']] * 8) + + $priority = ($facilities[$config['facility']] * 8) + audit_syslog_severity_code($event['severity'] ?? 'info'); - $timestamp = audit_syslog_timestamp($event['event_time'] ?? ''); - $hostname = audit_syslog_header_token($config['node_id'], 255, 'cacti'); - $application = audit_syslog_header_token($config['application'], 48, 'cacti-audit'); - $poller_id = $config['poller_id'] !== '' ? $config['poller_id'] : '-'; - $process = audit_syslog_header_token($poller_id, 128, '-'); - $message_id = audit_syslog_header_token($event['event_type'] ?? 'cacti.audit', 32, 'cacti.audit'); - $structured_values = array( - 'eventUuid' => $event['event_uuid'] ?? '', + $timestamp = audit_syslog_timestamp($event['event_time'] ?? ''); + $hostname = audit_syslog_header_token($config['node_id'], 255, 'cacti'); + $application = audit_syslog_header_token($config['application'], 48, 'cacti-audit'); + $poller_id = $config['poller_id'] !== '' ? $config['poller_id'] : '-'; + $process = audit_syslog_header_token($poller_id, 128, '-'); + $message_id = audit_syslog_header_token($event['event_type'] ?? 'cacti.audit', 32, 'cacti.audit'); + $structured_values = [ + 'eventUuid' => $event['event_uuid'] ?? '', 'correlationId' => $event['correlation_id'] ?? '', - 'nodeId' => $config['node_id'], - 'pollerId' => $config['poller_id'], - 'outcome' => $event['operation_outcome'] ?? '', - 'targetType' => $event['target_type'] ?? '', - 'targetId' => $event['target_id'] ?? '', + 'nodeId' => $config['node_id'], + 'pollerId' => $config['poller_id'], + 'outcome' => $event['operation_outcome'] ?? '', + 'targetType' => $event['target_type'] ?? '', + 'targetId' => $event['target_id'] ?? '', 'integrityHash' => $event['integrity_hash'] ?? '' - ); - $structured = array(); + ]; + $structured = []; foreach ($structured_values as $name => $value) { $structured[] = $name . '="' . audit_syslog_structured_value($value) . '"'; @@ -429,35 +475,35 @@ function audit_syslog_record($event, $config) { audit_syslog_message_payload($event, $config); if (strlen($record) > 262144) { - return array( - 'status' => 'failed', - 'permanent' => true, + return [ + 'status' => 'failed', + 'permanent' => true, 'error_code' => 'message_too_large', - 'error' => 'The formatted Syslog record exceeds 262144 bytes.', - 'record' => '' - ); + 'error' => 'The formatted Syslog record exceeds 262144 bytes.', + 'record' => '' + ]; } if ($config['transport'] === 'udp' && strlen($record) > $config['udp_max_size']) { - return array( - 'status' => 'failed', - 'permanent' => true, + return [ + 'status' => 'failed', + 'permanent' => true, 'error_code' => 'udp_message_too_large', - 'error' => 'The formatted Syslog record exceeds the configured UDP maximum.', - 'record' => '' - ); + 'error' => 'The formatted Syslog record exceeds the configured UDP maximum.', + 'record' => '' + ]; } - return array( - 'status' => 'ready', - 'permanent' => false, + return [ + 'status' => 'ready', + 'permanent' => false, 'error_code' => '', - 'error' => '', - 'record' => $record - ); + 'error' => '', + 'record' => $record + ]; } -function audit_syslog_frame($record, $transport) { +function audit_syslog_frame(string $record, string $transport): string { if ($transport === 'tcp' || $transport === 'tls') { return strlen($record) . ' ' . $record; } @@ -465,8 +511,12 @@ function audit_syslog_frame($record, $transport) { return $record; } -function audit_syslog_socket_target($config) { +/** + * @param array $config + */ +function audit_syslog_socket_target(array $config): string { $receiver = $config['receiver']; + if (filter_var($receiver, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { $receiver = '[' . $receiver . ']'; } @@ -476,10 +526,11 @@ function audit_syslog_socket_target($config) { return $scheme . '://' . $receiver . ':' . $config['port']; } -function audit_syslog_stream_operation($operation, &$warning = null) { +function audit_syslog_stream_operation(callable $operation, string &$warning = ''): mixed { $warning = ''; - $handler = function($severity, $message) use (&$warning) { + $handler = function ($severity, $message) use (&$warning) { $warning = audit_syslog_bounded_error($message); + return true; }; @@ -489,24 +540,29 @@ function audit_syslog_stream_operation($operation, &$warning = null) { return call_user_func($operation); } catch (Throwable $exception) { $warning = audit_syslog_bounded_error($exception->getMessage()); + return false; } finally { restore_error_handler(); } } -function audit_syslog_open_socket($config) { - $context_options = array(); +/** + * @param array $config + * @return array + */ +function audit_syslog_open_socket(array $config): array { + $context_options = []; if ($config['transport'] === 'tls') { - $context_options['ssl'] = array( - 'verify_peer' => true, - 'verify_peer_name' => true, - 'allow_self_signed' => false, - 'peer_name' => $config['receiver'], - 'SNI_enabled' => true, + $context_options['ssl'] = [ + 'verify_peer' => true, + 'verify_peer_name' => true, + 'allow_self_signed' => false, + 'peer_name' => $config['receiver'], + 'SNI_enabled' => true, 'disable_compression' => true - ); + ]; if ($config['tls_ca_file'] !== '') { $context_options['ssl']['cafile'] = $config['tls_ca_file']; @@ -514,16 +570,16 @@ function audit_syslog_open_socket($config) { if ($config['tls_client_cert'] !== '') { $context_options['ssl']['local_cert'] = $config['tls_client_cert']; - $context_options['ssl']['local_pk'] = $config['tls_client_key']; + $context_options['ssl']['local_pk'] = $config['tls_client_key']; } } - $context = stream_context_create($context_options); - $error_number = 0; - $error_message = ''; - $flags = STREAM_CLIENT_CONNECT; + $context = stream_context_create($context_options); + $error_number = 0; + $error_message = ''; + $flags = STREAM_CLIENT_CONNECT; $stream_warning = ''; - $socket = audit_syslog_stream_operation(function() use ( + $socket = audit_syslog_stream_operation(function () use ( $config, &$error_number, &$error_message, @@ -545,45 +601,49 @@ function audit_syslog_open_socket($config) { ? $error_message : ($stream_warning !== '' ? $stream_warning : 'Unable to connect to Syslog receiver.'); - return array( - 'socket' => null, + return [ + 'socket' => null, 'error_code' => 'connection_failed', - 'error' => audit_syslog_bounded_error($error) - ); + 'error' => audit_syslog_bounded_error($error) + ]; } stream_set_timeout($socket, $config['timeout']); stream_set_blocking($socket, true); - return array('socket' => $socket, 'error_code' => '', 'error' => ''); + return ['socket' => $socket, 'error_code' => '', 'error' => '']; } -function audit_syslog_bounded_error($error) { +function audit_syslog_bounded_error(mixed $error): string { $error = preg_replace('/[\\x00-\\x1f\\x7f]+/', ' ', (string) $error); - return substr(trim($error), 0, 1024); + return substr(trim($error ?? ''), 0, 1024); } -function audit_syslog_fwrite($socket, $message, &$warning = null) { - return audit_syslog_stream_operation(function() use ($socket, $message) { +function audit_syslog_fwrite(mixed $socket, string $message, string &$warning = ''): int|false { + return audit_syslog_stream_operation(function () use ($socket, $message) { return fwrite($socket, $message); }, $warning); } -function audit_syslog_write($socket, $message, $transport) { +/** + * @return array + */ +function audit_syslog_write(mixed $socket, string $message, string $transport): array { if (!is_resource($socket)) { - return array('status' => 'failed', 'error_code' => 'socket_unavailable', 'error' => 'Syslog socket is unavailable.'); + return ['status' => 'failed', 'error_code' => 'socket_unavailable', 'error' => 'Syslog socket is unavailable.']; } if ($transport === 'udp') { $stream_warning = ''; - $written = audit_syslog_fwrite($socket, $message, $stream_warning); + $written = audit_syslog_fwrite($socket, $message, $stream_warning); + if ($written !== strlen($message)) { $error = $stream_warning !== '' ? $stream_warning : 'Unable to write the complete Syslog datagram.'; - return array('status' => 'failed', 'error_code' => 'write_failed', 'error' => $error); + return ['status' => 'failed', 'error_code' => 'write_failed', 'error' => $error]; } } else { $length = strlen($message); @@ -591,45 +651,53 @@ function audit_syslog_write($socket, $message, $transport) { while ($offset < $length) { $stream_warning = ''; - $written = audit_syslog_fwrite($socket, substr($message, $offset), $stream_warning); + $written = audit_syslog_fwrite($socket, substr($message, $offset), $stream_warning); + if ($written === false || $written === 0) { $metadata = stream_get_meta_data($socket); - $error = !empty($metadata['timed_out']) + $error = !empty($metadata['timed_out']) ? 'Syslog write timed out.' : ($stream_warning !== '' ? $stream_warning : 'Unable to write the complete Syslog record.'); - return array('status' => 'failed', 'error_code' => 'write_failed', 'error' => $error); + return ['status' => 'failed', 'error_code' => 'write_failed', 'error' => $error]; } $offset += $written; } } - return array('status' => 'sent_unconfirmed', 'error_code' => '', 'error' => ''); + return ['status' => 'sent_unconfirmed', 'error_code' => '', 'error' => '']; } -function audit_syslog_send_event($event, $config, &$socket = null) { +/** + * @param array $event + * @param array $config + * @return array + */ +function audit_syslog_send_event(array $event, array $config, mixed &$socket = null): array { $formatted = audit_syslog_record($event, $config); + if ($formatted['status'] !== 'ready') { return $formatted; } if (!is_resource($socket)) { $opened = audit_syslog_open_socket($config); + if (!is_resource($opened['socket'])) { - return array( - 'status' => 'failed', - 'permanent' => false, + return [ + 'status' => 'failed', + 'permanent' => false, 'error_code' => $opened['error_code'], - 'error' => $opened['error'] - ); + 'error' => $opened['error'] + ]; } $socket = $opened['socket']; } - $message = audit_syslog_frame($formatted['record'], $config['transport']); - $result = audit_syslog_write($socket, $message, $config['transport']); + $message = audit_syslog_frame($formatted['record'], $config['transport']); + $result = audit_syslog_write($socket, $message, $config['transport']); $result['permanent'] = false; if ($result['status'] !== 'sent_unconfirmed' && is_resource($socket)) { @@ -640,7 +708,7 @@ function audit_syslog_send_event($event, $config, &$socket = null) { return $result; } -function audit_enqueue_syslog_event($audit_id) { +function audit_enqueue_syslog_event(int $audit_id): void { if (!audit_syslog_enabled() || !db_table_exists('audit_syslog_delivery')) { return; } @@ -648,27 +716,32 @@ function audit_enqueue_syslog_event($audit_id) { $event = db_fetch_row_prepared('SELECT id, event_uuid, request_status FROM audit_log WHERE id = ?', - array($audit_id)); + [$audit_id]); - if (!cacti_sizeof($event) || $event['request_status'] === 'started' || $event['event_uuid'] === '') { + if (!is_array($event) || $event['request_status'] === 'started' || $event['event_uuid'] === '') { return; } $config = audit_syslog_config(); - $state = $config['valid'] ? 'pending' : 'dead_letter'; - $error = $config['valid'] ? null : audit_syslog_bounded_error('configuration_invalid: ' . implode(',', $config['errors'])); + $state = $config['valid'] ? 'pending' : 'dead_letter'; + $error = $config['valid'] ? null : audit_syslog_bounded_error('configuration_invalid: ' . implode(',', $config['errors'])); db_execute_prepared('INSERT IGNORE INTO audit_syslog_delivery ( audit_id, event_uuid, destination_fingerprint, node_id, poller_id, state, attempts, next_attempt, last_error, created_time, updated_time ) VALUES (?, ?, ?, ?, ?, ?, 0, UTC_TIMESTAMP(6), ?, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6))', - array( + [ $event['id'], $event['event_uuid'], $config['fingerprint'], $config['node_id'], $config['poller_id'], $state, $error - )); + ]); } -function audit_syslog_delivery_config($config, $delivery) { +/** + * @param array $config + * @param array $delivery + * @return array + */ +function audit_syslog_delivery_config(array $config, array $delivery): array { if (isset($delivery['delivery_node_id']) && $delivery['delivery_node_id'] !== '') { $config['node_id'] = $delivery['delivery_node_id']; } @@ -682,16 +755,24 @@ function audit_syslog_delivery_config($config, $delivery) { return $config; } -function audit_syslog_retry_delay($attempt, $config) { +/** + * @param array $config + */ +function audit_syslog_retry_delay(mixed $attempt, array $config): int { $exponent = min(max(0, (int) $attempt - 1), 30); - $delay = $config['retry_base'] * pow(2, $exponent); + $delay = $config['retry_base'] * pow(2, $exponent); return (int) min($config['retry_max'], $delay); } -function audit_syslog_update_delivery($delivery, $result, $config) { +/** + * @param array $delivery + * @param array $result + * @param array $config + */ +function audit_syslog_update_delivery(array $delivery, array $result, array $config): void { $attempts = (int) $delivery['attempts'] + 1; - $error = isset($result['error']) ? audit_syslog_bounded_error($result['error']) : ''; + $error = isset($result['error']) ? audit_syslog_bounded_error($result['error']) : ''; if ($result['status'] === 'sent_unconfirmed') { db_execute_prepared("UPDATE audit_syslog_delivery @@ -703,14 +784,15 @@ function audit_syslog_update_delivery($delivery, $result, $config) { last_error = NULL, updated_time = UTC_TIMESTAMP(6) WHERE id = ?", - array($config['fingerprint'], $attempts, $delivery['delivery_id'])); + [$config['fingerprint'], $attempts, $delivery['delivery_id']]); + return; } - $permanent = !empty($result['permanent']) || $attempts >= $config['max_attempts']; - $state = $permanent ? 'dead_letter' : 'retry'; - $delay = $permanent ? 0 : audit_syslog_retry_delay($attempts, $config); - $error_code = isset($result['error_code']) ? $result['error_code'] : 'delivery_failed'; + $permanent = !empty($result['permanent']) || $attempts >= $config['max_attempts']; + $state = $permanent ? 'dead_letter' : 'retry'; + $delay = $permanent ? 0 : audit_syslog_retry_delay($attempts, $config); + $error_code = isset($result['error_code']) ? $result['error_code'] : 'delivery_failed'; $stored_error = audit_syslog_bounded_error($error_code . ': ' . $error); db_execute_prepared('UPDATE audit_syslog_delivery @@ -722,17 +804,19 @@ function audit_syslog_update_delivery($delivery, $result, $config) { last_error = ?, updated_time = UTC_TIMESTAMP(6) WHERE id = ?', - array($state, $config['fingerprint'], $attempts, $delay, $delay, $stored_error, $delivery['delivery_id'])); + [$state, $config['fingerprint'], $attempts, $delay, $delay, $stored_error, $delivery['delivery_id']]); } -function audit_process_syslog_queue() { +function audit_process_syslog_queue(): void { if (!audit_syslog_enabled() || !db_table_exists('audit_syslog_delivery')) { return; } $config = audit_syslog_config(); + if (!$config['valid']) { audit_syslog_check_health($config); + return; } @@ -749,16 +833,18 @@ function audit_process_syslog_queue() { AND a.request_status <> 'started' ORDER BY d.next_attempt, d.id LIMIT " . $batch_size, - array()); + []); $socket = null; - foreach ($deliveries as $delivery) { - $delivery_config = audit_syslog_delivery_config($config, $delivery); - $result = audit_syslog_send_event($delivery, $delivery_config, $socket); - audit_syslog_update_delivery($delivery, $result, $delivery_config); + if (is_array($deliveries)) { + foreach ($deliveries as $delivery) { + $delivery_config = audit_syslog_delivery_config($config, $delivery); + $result = audit_syslog_send_event($delivery, $delivery_config, $socket); + audit_syslog_update_delivery($delivery, $result, $delivery_config); - if ($result['status'] !== 'sent_unconfirmed' && empty($result['permanent'])) { - break; + if ($result['status'] !== 'sent_unconfirmed' && empty($result['permanent'])) { + break; + } } } @@ -769,13 +855,16 @@ function audit_process_syslog_queue() { audit_syslog_check_health($config); } -function audit_syslog_health() { +/** + * @return array + */ +function audit_syslog_health(): array { if (!db_table_exists('audit_syslog_delivery')) { - return array( - 'pending' => 0, 'retry' => 0, 'sent_unconfirmed' => 0, - 'dead_letter' => 0, 'oldest_pending_seconds' => 0, + return [ + 'pending' => 0, 'retry' => 0, 'sent_unconfirmed' => 0, + 'dead_letter' => 0, 'oldest_pending_seconds' => 0, 'last_attempt' => null, 'last_sent' => null, 'last_error' => null - ); + ]; } $row = db_fetch_row("SELECT @@ -795,29 +884,32 @@ function audit_syslog_health() { ORDER BY last_attempt DESC, id DESC LIMIT 1"); - return array( - 'pending' => (int) ($row['pending'] ?? 0), - 'retry' => (int) ($row['retry'] ?? 0), - 'sent_unconfirmed' => (int) ($row['sent_unconfirmed'] ?? 0), - 'dead_letter' => (int) ($row['dead_letter'] ?? 0), + return [ + 'pending' => (int) ($row['pending'] ?? 0), + 'retry' => (int) ($row['retry'] ?? 0), + 'sent_unconfirmed' => (int) ($row['sent_unconfirmed'] ?? 0), + 'dead_letter' => (int) ($row['dead_letter'] ?? 0), 'oldest_pending_seconds' => (int) ($row['oldest_pending_seconds'] ?? 0), - 'last_attempt' => $row['last_attempt'] ?? null, - 'last_sent' => $row['last_sent'] ?? null, - 'last_error' => $last_error !== false && $last_error !== '' ? $last_error : null - ); + 'last_attempt' => $row['last_attempt'] ?? null, + 'last_sent' => $row['last_sent'] ?? null, + 'last_error' => $last_error !== false && $last_error !== '' ? $last_error : null + ]; } -function audit_syslog_check_health($config = null) { +/** + * @param array|null $config + */ +function audit_syslog_check_health(?array $config = null): void { if (!audit_syslog_enabled()) { return; } - $config = $config === null ? audit_syslog_config() : $config; - $health = audit_syslog_health(); + $config = $config === null ? audit_syslog_config() : $config; + $health = audit_syslog_health(); $unhealthy = !$config['valid'] || $health['dead_letter'] >= $config['dead_letter_warning'] || $health['oldest_pending_seconds'] >= $config['pending_age_warning']; - $state = $unhealthy ? 'unhealthy' : 'healthy'; + $state = $unhealthy ? 'unhealthy' : 'healthy'; $previous = read_config_option('audit_syslog_health_state'); if ($previous !== $state) { @@ -829,19 +921,25 @@ function audit_syslog_check_health($config = null) { } } -function audit_syslog_retry_dead_letters($delivery_ids = array()) { +/** + * @param array $delivery_ids + */ +function audit_syslog_retry_dead_letters(array $delivery_ids = []): int { if (!db_table_exists('audit_syslog_delivery')) { return 0; } - $normalized_ids = array(); + $normalized_ids = []; + foreach ($delivery_ids as $delivery_id) { $delivery_id = (int) $delivery_id; + if ($delivery_id > 0) { $normalized_ids[] = $delivery_id; } } $delivery_ids = array_values(array_unique($normalized_ids)); + if (cacti_sizeof($delivery_ids) > 1000) { $delivery_ids = array_slice($delivery_ids, 0, 1000); } @@ -870,24 +968,27 @@ function audit_syslog_retry_dead_letters($delivery_ids = array()) { return db_affected_rows(); } -function audit_syslog_test_delivery() { +/** + * @return array + */ +function audit_syslog_test_delivery(): array { $config = audit_syslog_config(); - $event = array( - 'event_uuid' => audit_uuid_v4(), - 'correlation_id' => audit_request_correlation_id(), - 'event_type' => 'audit.syslog.test', - 'event_category' => 'audit', - 'severity' => 'notice', - 'user_id' => $_SESSION['sess_user_id'] ?? 0, - 'action' => 'test', - 'request_status' => 'completed', + $event = [ + 'event_uuid' => audit_uuid_v4(), + 'correlation_id' => audit_request_correlation_id(), + 'event_type' => 'audit.syslog.test', + 'event_category' => 'audit', + 'severity' => 'notice', + 'user_id' => $_SESSION['sess_user_id'] ?? 0, + 'action' => 'test', + 'request_status' => 'completed', 'operation_outcome' => 'success', - 'target_type' => 'syslog_receiver', - 'target_id' => $config['receiver'], - 'ip_address' => function_exists('get_client_addr') ? get_client_addr() : '', - 'event_time' => audit_utc_time(), - 'details' => audit_json_encode(array('test' => true)) - ); + 'target_type' => 'syslog_receiver', + 'target_id' => $config['receiver'], + 'ip_address' => function_exists('get_client_addr') ? get_client_addr() : '', + 'event_time' => audit_utc_time(), + 'details' => audit_json_encode(['test' => true]) + ]; $socket = null; $result = audit_syslog_send_event($event, $config, $socket); diff --git a/index.php b/index.php index 2e22b8c..828ecbe 100644 --- a/index.php +++ b/index.php @@ -22,4 +22,4 @@ +-------------------------------------------------------------------------+ */ -header("Location:../index.php"); +header('Location:../index.php'); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..782fd76 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,955 @@ +parameters: + ignoreErrors: + - + message: '#^Call to function cacti_log\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 1 + path: audit.php + + - + message: '#^Call to function cacti_log\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 4 + path: audit_functions.php + + - + message: '#^Call to function set_config_option\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 1 + path: audit_functions.php + + - + message: '#^Call to function cacti_log\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 1 + path: audit_syslog.php + + - + message: '#^Call to function set_config_option\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 1 + path: audit_syslog.php + + - + message: '#^Call to function cacti_log\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 4 + path: setup.php + + - + message: '#^Call to function set_config_option\(\) on a separate line has no effect\.$#' + identifier: function.resultUnused + count: 1 + path: setup.php + + - + message: '#^Parameter \#1 \$filename of function file_get_contents expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/Php81SyntaxTest.php + + - + message: '#^Parameter \#1 \$filename of function is_readable expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/Php81SyntaxTest.php + + - + message: '#^Parameter \#2 \$subject of function preg_match expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/Php81SyntaxTest.php + + - + message: '#^Parameter \#1 \$filename of function file_get_contents expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/PreparedStatementConsistencyTest.php + + - + message: '#^Parameter \#2 \$subject of function preg_match expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/PreparedStatementConsistencyTest.php + + - + message: '#^Cannot access offset ''info'' on array\|false\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: tests/Security/SetupStructureTest.php + + - + message: '#^Parameter \#1 \$filename of function file_get_contents expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/SetupStructureTest.php + + - + message: '#^Parameter \#1 \$filename of function parse_ini_file expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/Security/SetupStructureTest.php + + - + message: '#^Function __\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function __\(\) has parameter \$domain with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function __\(\) has parameter \$text with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function __esc\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function __esc\(\) has parameter \$domain with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function __esc\(\) has parameter \$text with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_add_column\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_add_column\(\) has parameter \$data with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_add_column\(\) has parameter \$plugin with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_add_column\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_table_create\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_table_create\(\) has parameter \$data with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_table_create\(\) has parameter \$plugin with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function api_plugin_db_table_create\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_log\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_log\(\) has parameter \$also_print with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_log\(\) has parameter \$level with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_log\(\) has parameter \$log_type with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_log\(\) has parameter \$message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_sizeof\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function cacti_sizeof\(\) has parameter \$array with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_affected_rows\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_column_exists\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_column_exists\(\) has parameter \$column with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_column_exists\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_execute\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_execute\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_execute_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_execute_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_execute_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_assoc\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_assoc\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_cell\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_cell\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_row\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_row\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_row_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_row_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_fetch_row_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_index_exists\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_index_exists\(\) has parameter \$index with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_index_exists\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_table_exists\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function db_table_exists\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has parameter \$error with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has parameter \$optional with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has parameter \$regex with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function form_input_validate\(\) has parameter \$value with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_filter_request_var\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_filter_request_var\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_nfilter_request_var\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_nfilter_request_var\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_request_var\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function get_request_var\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function html_escape\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function html_escape\(\) has parameter \$string with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function is_error_message\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function is_realm_allowed\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function is_realm_allowed\(\) has parameter \$realm with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function raise_message\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function raise_message\(\) has parameter \$id with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function raise_message\(\) has parameter \$level with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function raise_message\(\) has parameter \$text with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function read_config_option\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function read_config_option\(\) has parameter \$force with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function read_config_option\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function set_config_option\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function set_config_option\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function set_config_option\(\) has parameter \$value with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function sql_save\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function sql_save\(\) has parameter \$array with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function sql_save\(\) has parameter \$key with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Function sql_save\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/bootstrap.php + + - + message: '#^Parameter \#1 \$haystack of function strpos expects string, string\|false given\.$#' + identifier: argument.type + count: 11 + path: tests/controller_security_test.php + + - + message: '#^Parameter \#1 \$haystack of function substr_count expects string, string\|false given\.$#' + identifier: argument.type + count: 2 + path: tests/controller_security_test.php + + - + message: '#^Function api_plugin_user_realm_auth\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function api_plugin_user_realm_auth\(\) has parameter \$filename with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function audit_test_assert_same\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function audit_test_assert_same\(\) has parameter \$actual with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function audit_test_assert_same\(\) has parameter \$expected with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function audit_test_assert_same\(\) has parameter \$message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_assoc_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Function db_fetch_cell_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Offset ''type'' might not exist on array\\|null\.$#' + identifier: offsetAccess.notFound + count: 1 + path: tests/security_functions_test.php + + - + message: '#^Cannot access offset 0 on array\\|false\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 9 + path: tests/syslog_functions_test.php + + - + message: '#^Cannot access offset 1 on array\\|false\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 4 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_assert\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_assert\(\) has parameter \$condition with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_assert\(\) has parameter \$message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_config\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_config\(\) has parameter \$overrides with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_event\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_remove_tls_material\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_remove_tls_material\(\) has parameter \$material with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_tls_material\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_tls_server\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_tls_server\(\) has parameter \$material with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_tls_server\(\) has parameter \$port with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_udp_server\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_udp_server\(\) has parameter \$error with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_udp_server\(\) has parameter \$error_message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_syslog_test_udp_server\(\) has parameter \$port with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$csr of function openssl_csr_sign expects OpenSSLCertificateSigningRequest\|string, OpenSSLCertificateSigningRequest\|true given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$haystack of function strrpos expects string, string\|false given\.$#' + identifier: argument.type + count: 3 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$socket of function socket_set_option expects Socket, Socket\|false given\.$#' + identifier: argument.type + count: 2 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$socket of function stream_socket_accept expects resource, resource\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$socket of function stream_socket_get_name expects resource, resource\|false given\.$#' + identifier: argument.type + count: 2 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$stream of function fclose expects resource, resource\|false given\.$#' + identifier: argument.type + count: 3 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$stream of function fread expects resource, resource\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$stream of function stream_set_timeout expects resource, resource\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_functions_test.php + + - + message: '#^Parameter \#1 \$string of function substr expects string, string\|false given\.$#' + identifier: argument.type + count: 3 + path: tests/syslog_functions_test.php + + - + message: '#^Function audit_queue_assert\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function audit_queue_assert\(\) has parameter \$condition with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function audit_queue_assert\(\) has parameter \$message with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function cacti_sizeof\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function cacti_sizeof\(\) has parameter \$value with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_affected_rows\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_execute\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_execute\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_execute_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_execute_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_execute_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_fetch_row_prepared\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_fetch_row_prepared\(\) has parameter \$params with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_fetch_row_prepared\(\) has parameter \$sql with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_table_exists\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function db_table_exists\(\) has parameter \$table with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function read_config_option\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Function read_config_option\(\) has parameter \$name with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Offset 0 does not exist on array\{\}\.$#' + identifier: offsetAccess.notFound + count: 15 + path: tests/syslog_queue_test.php + + - + message: '#^Parameter \#1 \$delivery_ids of function audit_syslog_retry_dead_letters expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_queue_test.php + + - + message: '#^Parameter \#1 \$haystack of function strpos expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: tests/syslog_queue_test.php \ No newline at end of file diff --git a/phpstan/stubs/cacti.stubs.php b/phpstan/stubs/cacti.stubs.php new file mode 100644 index 0000000..cc7c8bd --- /dev/null +++ b/phpstan/stubs/cacti.stubs.php @@ -0,0 +1,328 @@ + */ +global $config; + +/** @var string */ +global $database_default; + +/** @var string */ +global $action; + +/** @var array */ +global $utilities; + +/** @var array */ +global $menu; + +/** @var array */ +global $messages; + +/** @var array */ +global $audit_retentions; + +/** @var array */ +global $tabs; + +/** @var array */ +global $settings; + +/** @var array */ +global $item_rows; + +// ----- Plugin / Hook API --------------------------------------------- + +function api_plugin_register_hook(string $plugin, string $hook, string $function, string $file, int $install = 0): void { +} + +function api_plugin_register_realm(string $plugin, array|string $file, array|string $display, int $install = 0): void { +} + +function api_plugin_is_enabled(string $plugin): bool { + return false; +} + +function api_plugin_enable_hooks(string $plugin): void { +} + +function api_plugin_replicate_config(): void { +} + +function api_plugin_user_realm_auth(string $file = ''): bool { + return false; +} + +function auth_augment_roles(string $label, array $files): void { +} + +// ----- Database ------------------------------------------------------- + +function db_execute(string $sql, bool $log = true, mixed $db_conn = false): bool { + return false; +} + +function db_execute_prepared(string $sql, array $params = [], bool $log = true, mixed $db_conn = false, bool $execute_prepared = true): bool { + return false; +} + +/** + * @return array>|false + */ +function db_fetch_assoc(string $sql, bool $log = true, mixed $db_conn = false): array|false { + return false; +} + +/** + * @return array>|false + */ +function db_fetch_assoc_prepared(string $sql, array $params = [], bool $log = true, mixed $db_conn = false): array|false { + return false; +} + +/** + * @return array|false + */ +function db_fetch_row(string $sql, bool $log = true, mixed $db_conn = false): array|false { + return false; +} + +/** + * @return array|false + */ +function db_fetch_row_prepared(string $sql, array $params = [], bool $log = true, mixed $db_conn = false): array|false { + return false; +} + +function db_fetch_cell(string $sql, string $col_name = '', bool $log = true, mixed $db_conn = false): mixed { + return false; +} + +function db_fetch_cell_prepared(string $sql, array $params = [], string $col_name = '', bool $log = true, mixed $db_conn = false): mixed { + return false; +} + +function db_fetch_insert_id(string $table = ''): int { + return 0; +} + +function db_affected_rows(mixed $db_conn = false): int { + return 0; +} + +function db_column_exists(string $table, string $column, bool $log = true, mixed $db_conn = false): bool { + return false; +} + +function db_table_exists(string $table, bool $log = true, mixed $db_conn = false): bool { + return false; +} + +function db_index_exists(string $table, string $index, bool $log = true, mixed $db_conn = false): bool { + return false; +} + +function db_add_index(string $table, string $type, string $name, mixed $definition, bool $log = true, mixed $db_conn = false): void { +} + +// ----- Config / Options --------------------------------------------- + +function read_config_option(string $name, bool $global = false): mixed { + return false; +} + +function set_config_option(string $name, string $value): bool { + return false; +} + +// ----- Localization -------------------------------------------------- + +/** + * @param mixed ...$args sprintf arguments or domain + */ +function __(string $text, mixed ...$args): string { + return $text; +} + +/** + * @param mixed ...$args sprintf arguments or domain + */ +function __esc(string $text, mixed ...$args): string { + return $text; +} + +// ----- Input Handling ------------------------------------------------ + +function get_request_var(string $name, mixed $default = '', string $filter = ''): mixed { + return $default; +} + +function get_filter_request_var(string $name, int $filter = FILTER_DEFAULT, array $options = []): mixed { + return false; +} + +function get_nfilter_request_var(string $name, mixed $default = '', string $filter = ''): mixed { + return $default; +} + +function isset_request_var(string $name): bool { + return false; +} + +function isempty_request_var(string $name): bool { + return false; +} + +function set_default_action(string $default = ''): void { +} + +/** + * @param array $filters + */ +function validate_store_request_vars(array $filters, string $sess_prefix = ''): void { +} + +function sanitize_search_string(mixed $string): string { + return ''; +} + +function html_escape_request_var(string $name): string { +} + +// ----- Logging / Misc ------------------------------------------------ + +function cacti_log(string $string, bool $output = false, string $environment = '', int $level = 1, bool $force = false): bool { + return false; +} + +function cacti_sizeof(mixed $array): int { + return is_array($array) ? count($array) : 0; +} + +function get_client_addr(): string { + return ''; +} + +function get_username(int $user_id): string { + return ''; +} + +function raise_message(string $id, string $message = '', int $level = 0): void { +} + +function csrf_check(bool $force = false): bool { + return false; +} + +// ----- UI / HTML Helpers --------------------------------------------- + +function top_header(): void { +} + +function bottom_footer(): void { +} + +function html_start_box(string $title, string $width, string $align = '', string $add = '', string $collapsible = '', string $url = ''): void { +} + +function html_end_box(bool $trailing_br = true): void { +} + +/** + * @param array ...$args + */ +function html_nav_bar(string $base_url, int $max_pages, int $current_page, int $rows_per_page, int $total_rows, int $pages_to_display, string $title, string $page_var = 'page', string $class = 'main'): string { + return ''; +} + +/** + * @param array> $display_text + */ +function html_header_sort(array $display_text, string $sort_column, string $sort_direction, bool $last_column = false): void { +} + +function html_escape(mixed $string): string { + return ''; +} + +function form_alternate_row(string $id, bool $light = false): void { +} + +function form_selectable_cell(string $text, int $id, string $class = '', string $title = ''): void { +} + +function form_selectable_ecell(string $text, int $id, string $class = '', string $title = ''): void { +} + +function form_end_row(): void { +} + +function filter_value(string $text, string $filter): string { + return ''; +} + +function get_order_string(): string { + return ''; +} + +/** + * @param array>|false $array + */ +function array_rekey(array|false $array, string $key, string $key_value): array { + return []; +} diff --git a/setup.php b/setup.php index 7d74976..41dadc8 100644 --- a/setup.php +++ b/setup.php @@ -24,7 +24,7 @@ include_once('audit_functions.php'); -function plugin_audit_install() { +function plugin_audit_install(): void { api_plugin_register_hook('audit', 'config_arrays', 'audit_config_arrays', 'setup.php'); api_plugin_register_hook('audit', 'config_settings', 'audit_config_settings', 'setup.php'); api_plugin_register_hook('audit', 'config_insert', 'audit_config_insert', 'setup.php'); @@ -34,7 +34,7 @@ function plugin_audit_install() { api_plugin_register_hook('audit', 'is_console_page', 'audit_is_console_page', 'setup.php'); api_plugin_register_hook('audit', 'logout_pre_session_destroy', 'audit_logout_pre_session_destroy', 'setup.php'); - /* hook for table replication */ + // hook for table replication api_plugin_register_hook('audit', 'replicate_out', 'audit_replicate_out', 'setup.php'); audit_setup_realms(true); @@ -42,11 +42,11 @@ function plugin_audit_install() { audit_setup_table(); } -function audit_setup_realms($grant_installing_user = false) { - $realms = array( +function audit_setup_realms(bool $grant_installing_user = false): void { + $realms = [ 'audit.php' => __('Audit Log User', 'audit'), 'audit_manage.php' => __('Audit Log Admin', 'audit') - ); + ]; foreach ($realms as $file => $display) { api_plugin_register_realm('audit', $file, $display, $grant_installing_user ? 1 : 0); @@ -60,39 +60,43 @@ function audit_setup_realms($grant_installing_user = false) { FROM plugin_realms WHERE plugin = ? AND file IN (?, ?)', - array('audit', 'audit.php', 'audit_manage.php')); + ['audit', 'audit.php', 'audit_manage.php']); - foreach ($realm_ids as $realm) { - db_execute_prepared('REPLACE INTO user_auth_realm + if (is_array($realm_ids)) { + foreach ($realm_ids as $realm) { + db_execute_prepared('REPLACE INTO user_auth_realm (user_id, realm_id) VALUES (?, ?)', - array($admin_user, $realm['realm_id'])); + [$admin_user, $realm['realm_id']]); + } } } } } -function audit_remove_deprecated_realms() { +function audit_remove_deprecated_realms(): void { $realms = db_fetch_assoc_prepared('SELECT id FROM plugin_realms WHERE plugin = ? AND file = ?', - array('audit', 'audit_purge.php')); + ['audit', 'audit_purge.php']); - foreach ($realms as $realm) { - $realm_id = $realm['id'] + 100; + if (is_array($realms)) { + foreach ($realms as $realm) { + $realm_id = $realm['id'] + 100; - db_execute_prepared('DELETE FROM user_auth_realm - WHERE realm_id = ?', - array($realm_id)); + db_execute_prepared('DELETE FROM user_auth_realm + WHERE realm_id = ?', + [$realm_id]); - db_execute_prepared('DELETE FROM user_auth_group_realm - WHERE realm_id = ?', - array($realm_id)); + db_execute_prepared('DELETE FROM user_auth_group_realm + WHERE realm_id = ?', + [$realm_id]); - db_execute_prepared('DELETE FROM plugin_realms - WHERE id = ?', - array($realm['id'])); + db_execute_prepared('DELETE FROM plugin_realms + WHERE id = ?', + [$realm['id']]); + } } if (cacti_sizeof($realms)) { @@ -100,13 +104,14 @@ function audit_remove_deprecated_realms() { } } -function plugin_audit_uninstall() { +function plugin_audit_uninstall(): bool { db_execute('DROP TABLE IF EXISTS audit_syslog_delivery'); db_execute('DROP TABLE IF EXISTS audit_log'); + return true; } -function audit_is_console_page($url) { +function audit_is_console_page(string $url): bool { if (strpos($url, 'audit.php') !== false) { return true; } @@ -114,34 +119,37 @@ function audit_is_console_page($url) { return false; } -function plugin_audit_check_config() { +function plugin_audit_check_config(): bool { return true; } -function plugin_audit_upgrade() { +function plugin_audit_upgrade(): bool { return true; } -function audit_check_upgrade() { +function audit_check_upgrade(): void { global $config, $database_default; include_once($config['library_path'] . '/database.php'); include_once($config['library_path'] . '/functions.php'); - $files = array('plugins.php', 'audit.php'); - if (isset($_SERVER['PHP_SELF']) && !in_array(basename($_SERVER['PHP_SELF']), $files)) { + $files = ['plugins.php', 'audit.php']; + + if (isset($_SERVER['PHP_SELF']) && !in_array(basename($_SERVER['PHP_SELF']), $files, true)) { return; } $info = plugin_audit_version(); $current = $info['version']; - $old = db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', array('audit')); + $old = db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', ['audit']); + if ($current != $old) { if (api_plugin_is_enabled('audit')) { - # may sound ridiculous, but enables new hooks + // may sound ridiculous, but enables new hooks api_plugin_enable_hooks('audit'); } db_execute('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS object_data LONGBLOB'); + if (db_column_exists('audit_log', 'outcome')) { if (!db_column_exists('audit_log', 'request_status')) { db_execute("ALTER TABLE audit_log CHANGE COLUMN outcome request_status varchar(20) NOT NULL DEFAULT 'unknown'"); @@ -168,7 +176,7 @@ function audit_check_upgrade() { db_execute_prepared('UPDATE plugin_config SET version = ? WHERE directory = ?', - array($current, 'audit')); + [$current, 'audit']); db_execute_prepared('UPDATE plugin_config SET version = ?, @@ -176,16 +184,20 @@ function audit_check_upgrade() { author = ?, webpage = ? WHERE directory = ?', - array($info['version'], $info['longname'], $info['author'], $info['homepage'], $info['name'])); + [$info['version'], $info['longname'], $info['author'], $info['homepage'], $info['name']]); - /* hook for table replication */ - api_plugin_register_hook('audit', 'replicate_out', 'audit_replicate_out', 'setup.php', '1'); + // hook for table replication + api_plugin_register_hook('audit', 'replicate_out', 'audit_replicate_out', 'setup.php', 1); api_plugin_register_hook('audit', 'is_console_page', 'audit_is_console_page', 'setup.php', 1); api_plugin_register_hook('audit', 'logout_pre_session_destroy', 'audit_logout_pre_session_destroy', 'setup.php', 1); } } -function audit_replicate_out($data) { +/** + * @param array $data + * @return array + */ +function audit_replicate_out(array $data): array { $rcnn_id = $data['rcnn_id']; $class = $data['class']; @@ -210,6 +222,7 @@ function audit_replicate_out($data) { } db_execute('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS object_data LONGBLOB', true, $rcnn_id); + if (db_column_exists('audit_log', 'outcome', false, $rcnn_id)) { if (!db_column_exists('audit_log', 'request_status', false, $rcnn_id)) { db_execute("ALTER TABLE audit_log CHANGE COLUMN outcome request_status varchar(20) NOT NULL DEFAULT 'unknown'", true, $rcnn_id); @@ -234,7 +247,7 @@ function audit_replicate_out($data) { return $data; } -function audit_poller_bottom() { +function audit_poller_bottom(): void { audit_retry_external_logs(); audit_process_syslog_queue(); @@ -255,7 +268,7 @@ function audit_poller_bottom() { WHERE audit_syslog_delivery.audit_id = audit_log.id AND audit_syslog_delivery.state IN ('pending', 'retry', 'dead_letter') )", - array($cutoff->format('Y-m-d H:i:s'))); + [$cutoff->format('Y-m-d H:i:s')]); $rows = db_affected_rows(); cacti_log('NOTE: Purged ' . $rows . ' Audit Log Records from Cacti', false, 'POLLER'); } @@ -264,7 +277,7 @@ function audit_poller_bottom() { set_config_option('audit_last_check', $now); } -function audit_setup_table() { +function audit_setup_table(): bool { global $config, $database_default; include_once($config['library_path'] . '/database.php'); @@ -320,7 +333,7 @@ function audit_setup_table() { return true; } -function audit_setup_syslog_table() { +function audit_setup_syslog_table(): void { db_execute("CREATE TABLE IF NOT EXISTS `audit_syslog_delivery` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `audit_id` bigint(20) unsigned NOT NULL, @@ -350,51 +363,51 @@ function audit_setup_syslog_table() { db_execute("ALTER TABLE audit_syslog_delivery ADD COLUMN IF NOT EXISTS node_id varchar(255) NOT NULL DEFAULT 'cacti' AFTER destination_fingerprint"); - db_execute("ALTER TABLE audit_syslog_delivery + db_execute('ALTER TABLE audit_syslog_delivery ADD COLUMN IF NOT EXISTS poller_id varchar(64) DEFAULT NULL - AFTER node_id"); + AFTER node_id'); } -function audit_upgrade_event_schema($rcnn_id = false) { - $remote = $rcnn_id !== false; - $args = $remote ? array(true, $rcnn_id) : array(); - $columns = array( - "event_uuid char(36) DEFAULT NULL", - "correlation_id char(36) DEFAULT NULL", +function audit_upgrade_event_schema(mixed $rcnn_id = false): void { + $remote = $rcnn_id !== false; + $args = $remote ? [true, $rcnn_id] : []; + $columns = [ + 'event_uuid char(36) DEFAULT NULL', + 'correlation_id char(36) DEFAULT NULL', "event_type varchar(100) NOT NULL DEFAULT 'cacti.request'", "event_category varchar(40) NOT NULL DEFAULT 'configuration'", "severity varchar(12) NOT NULL DEFAULT 'info'", "actor_type varchar(20) NOT NULL DEFAULT 'user'", - "target_type varchar(64) DEFAULT NULL", - "target_id varchar(128) DEFAULT NULL", + 'target_type varchar(64) DEFAULT NULL', + 'target_id varchar(128) DEFAULT NULL', "operation_outcome varchar(20) NOT NULL DEFAULT 'unknown'", - "outcome_reason varchar(255) DEFAULT NULL", - "http_method varchar(10) DEFAULT NULL", - "http_status smallint unsigned DEFAULT NULL", - "completed_time datetime(6) DEFAULT NULL", - "duration_ms bigint unsigned DEFAULT NULL", - "details longblob", - "previous_hash char(64) DEFAULT NULL", - "integrity_hash char(64) DEFAULT NULL", - "external_attempts int unsigned NOT NULL DEFAULT 0", - "external_last_attempt datetime(6) DEFAULT NULL", - "external_delivered_time datetime(6) DEFAULT NULL" - ); + 'outcome_reason varchar(255) DEFAULT NULL', + 'http_method varchar(10) DEFAULT NULL', + 'http_status smallint unsigned DEFAULT NULL', + 'completed_time datetime(6) DEFAULT NULL', + 'duration_ms bigint unsigned DEFAULT NULL', + 'details longblob', + 'previous_hash char(64) DEFAULT NULL', + 'integrity_hash char(64) DEFAULT NULL', + 'external_attempts int unsigned NOT NULL DEFAULT 0', + 'external_last_attempt datetime(6) DEFAULT NULL', + 'external_delivered_time datetime(6) DEFAULT NULL' + ]; foreach ($columns as $definition) { call_user_func_array('db_execute', array_merge( - array('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS ' . $definition), + ['ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS ' . $definition], $args )); } - $indexes = array( - 'event_uuid' => array('UNIQUE INDEX', array('event_uuid')), - 'correlation_id' => array('INDEX', array('correlation_id')), - 'event_type' => array('INDEX', array('event_type')), - 'operation_outcome' => array('INDEX', array('operation_outcome')), - 'external_status' => array('INDEX', array('external_status')) - ); + $indexes = [ + 'event_uuid' => ['UNIQUE INDEX', ['event_uuid']], + 'correlation_id' => ['INDEX', ['correlation_id']], + 'event_type' => ['INDEX', ['event_type']], + 'operation_outcome' => ['INDEX', ['operation_outcome']], + 'external_status' => ['INDEX', ['external_status']] + ]; foreach ($indexes as $name => $definition) { if (!db_index_exists('audit_log', $name, false, $remote ? $rcnn_id : false)) { @@ -403,13 +416,18 @@ function audit_upgrade_event_schema($rcnn_id = false) { } } -function plugin_audit_version() { +/** + * @return array + */ +function plugin_audit_version(): array { global $config; - $info = parse_ini_file($config['base_path'] . '/plugins/audit/INFO', true); - return $info['info']; + $info = parse_ini_file($config['base_path'] . '/plugins/audit/INFO', true); + $plugin_info = is_array($info) ? ($info['info'] ?? null) : null; + + return is_array($plugin_info) ? $plugin_info : []; } -function audit_log_valid_event() { +function audit_log_valid_event(): bool { global $action; $valid = false; @@ -443,32 +461,32 @@ function audit_log_valid_event() { return $valid; } -function audit_utilities_array() { +function audit_utilities_array(): void { global $utilities; if (version_compare(CACTI_VERSION, '1.3.0', '<')) { if (api_plugin_user_realm_auth('audit.php')) { $utilities[__('Technical Support', 'audit')] = array_merge( $utilities[__('Technical Support', 'audit')], - array( - __('View Audit Log', 'audit') => array( - 'link' => 'plugins/audit/audit.php', + [ + __('View Audit Log', 'audit') => [ + 'link' => 'plugins/audit/audit.php', 'description' => __('Allows Administrators to view change activity on the Cacti server. Administrators can also export the audit log for analysis purposes.', 'audit') - ) - ) + ] + ] ); } } } -function audit_config_arrays() { +function audit_config_arrays(): void { global $menu, $messages, $audit_retentions, $utilities; if (isset($_SESSION['audit_message']) && $_SESSION['audit_message'] != '') { - $messages['audit_message'] = array('message' => $_SESSION['audit_message'], 'type' => 'info'); + $messages['audit_message'] = ['message' => $_SESSION['audit_message'], 'type' => 'info']; } - $audit_retentions = array( + $audit_retentions = [ -1 => __('Indefinitely', 'audit'), 14 => __('%d Weeks', 2, 'audit'), 30 => __('%d Month', 1, 'audit'), @@ -479,235 +497,236 @@ function audit_config_arrays() { 365 => __('%d Year', 1, 'audit'), 730 => __('%d Years', 2, 'audit'), 1095 => __('%d Years', 3, 'audit') - ); + ]; $menu[__('Utilities')]['plugins/audit/audit.php'] = __('Audit Log', 'audit'); if (function_exists('auth_augment_roles')) { - auth_augment_roles(__('Audit Plugin', 'audit'), array('audit.php', 'audit_manage.php')); + auth_augment_roles(__('Audit Plugin', 'audit'), ['audit.php', 'audit_manage.php']); } audit_check_upgrade(); } -function audit_config_settings() { +function audit_config_settings(): void { global $tabs, $settings, $item_rows, $audit_retentions; - $temp = array( - 'audit_header' => array( + $temp = [ + 'audit_header' => [ 'friendly_name' => __('Audit Log Settings', 'audit'), - 'method' => 'spacer', - ), - 'audit_enabled' => array( + 'method' => 'spacer', + ], + 'audit_enabled' => [ 'friendly_name' => __('Enable Audit Log', 'audit'), - 'description' => __('Check this box, if you want the Audit Log to track GUI activities.', 'audit'), - 'method' => 'checkbox', - 'default' => 'on' - ), - 'audit_retention' => array( + 'description' => __('Check this box, if you want the Audit Log to track GUI activities.', 'audit'), + 'method' => 'checkbox', + 'default' => 'on' + ], + 'audit_retention' => [ 'friendly_name' => __('Audit Log Retention', 'audit'), - 'description' => __('How long do you wish Audit Log entries to be retained?', 'audit'), - 'method' => 'drop_array', - 'default' => '90', - 'array' => $audit_retentions - ), - 'audit_log_external' => array( + 'description' => __('How long do you wish Audit Log entries to be retained?', 'audit'), + 'method' => 'drop_array', + 'default' => '90', + 'array' => $audit_retentions + ], + 'audit_log_external' => [ 'friendly_name' => __('External Audit Log', 'audit'), - 'description' => __('Check this box, if you want the Audit Log to be written to an external file.', 'audit'), - 'method' => 'checkbox', - 'default' => 'off' - ), - 'audit_log_external_format' => array( + 'description' => __('Check this box, if you want the Audit Log to be written to an external file.', 'audit'), + 'method' => 'checkbox', + 'default' => 'off' + ], + 'audit_log_external_format' => [ 'friendly_name' => __('External Audit Log Format', 'audit'), - 'description' => __('Select the output format for external audit log records.', 'audit'), - 'method' => 'drop_array', - 'default' => 'json', - 'array' => array( + 'description' => __('Select the output format for external audit log records.', 'audit'), + 'method' => 'drop_array', + 'default' => 'json', + 'array' => [ 'text' => __('Text', 'audit'), 'json' => __('JSON', 'audit') - ) - ), - 'audit_log_external_path' => array( + ] + ], + 'audit_log_external_path' => [ 'friendly_name' => __('External Audit Log Log file Path', 'audit'), - 'description' => __('Enter the path to the external audit log file.', 'audit'), - 'method' => 'filepath', - 'default' => '/var/www/html/cacti/log/audit.log', - 'max_length' => '255' - ), - ); + 'description' => __('Enter the path to the external audit log file.', 'audit'), + 'method' => 'filepath', + 'default' => '/var/www/html/cacti/log/audit.log', + 'max_length' => '255' + ], + ]; if (php_sapi_name() === 'cli' || audit_user_is_admin()) { - $facility_options = array(); + $facility_options = []; + foreach (audit_syslog_facilities() as $facility => $code) { $facility_options[$facility] = strtoupper($facility) . ' (' . $code . ')'; } - $syslog = array( - 'audit_syslog_header' => array( + $syslog = [ + 'audit_syslog_header' => [ 'friendly_name' => __('Remote Syslog', 'audit'), - 'method' => 'spacer' - ), - 'audit_syslog_enabled' => array( + 'method' => 'spacer' + ], + 'audit_syslog_enabled' => [ 'friendly_name' => __('Enable Remote Syslog', 'audit'), - 'description' => __('Queue finalized audit events for remote Syslog delivery. The existing external file output remains independent.', 'audit'), - 'method' => 'checkbox', - 'default' => 'off' - ), - 'audit_syslog_receiver' => array( + 'description' => __('Queue finalized audit events for remote Syslog delivery. The existing external file output remains independent.', 'audit'), + 'method' => 'checkbox', + 'default' => 'off' + ], + 'audit_syslog_receiver' => [ 'friendly_name' => __('Syslog Receiver', 'audit'), - 'description' => __('Enter a receiver hostname or IP address without a URI scheme, path, or credentials.', 'audit'), - 'method' => 'textbox', - 'default' => '', - 'max_length' => '253', - 'size' => '60' - ), - 'audit_syslog_port' => array( + 'description' => __('Enter a receiver hostname or IP address without a URI scheme, path, or credentials.', 'audit'), + 'method' => 'textbox', + 'default' => '', + 'max_length' => '253', + 'size' => '60' + ], + 'audit_syslog_port' => [ 'friendly_name' => __('Syslog Port', 'audit'), - 'description' => __('Enter 1-65535, or leave blank to use 514 for UDP/TCP and 6514 for TLS.', 'audit'), - 'method' => 'textbox', - 'default' => '', - 'max_length' => '5', - 'size' => '8' - ), - 'audit_syslog_transport' => array( + 'description' => __('Enter 1-65535, or leave blank to use 514 for UDP/TCP and 6514 for TLS.', 'audit'), + 'method' => 'textbox', + 'default' => '', + 'max_length' => '5', + 'size' => '8' + ], + 'audit_syslog_transport' => [ 'friendly_name' => __('Syslog Transport', 'audit'), - 'description' => __('UDP sends one datagram without acknowledgement. TCP and TLS use RFC 6587 octet-count framing.', 'audit'), - 'method' => 'drop_array', - 'default' => 'udp', - 'array' => array( + 'description' => __('UDP sends one datagram without acknowledgement. TCP and TLS use RFC 6587 octet-count framing.', 'audit'), + 'method' => 'drop_array', + 'default' => 'udp', + 'array' => [ 'udp' => __('UDP', 'audit'), 'tcp' => __('TCP', 'audit'), 'tls' => __('TLS', 'audit') - ) - ), - 'audit_syslog_format' => array( + ] + ], + 'audit_syslog_format' => [ 'friendly_name' => __('Syslog Payload Format', 'audit'), - 'description' => __('All formats use an RFC 5424 header. Select RFC 5424 structured data, CEF, or compact JSON for the message.', 'audit'), - 'method' => 'drop_array', - 'default' => 'json', - 'array' => array( + 'description' => __('All formats use an RFC 5424 header. Select RFC 5424 structured data, CEF, or compact JSON for the message.', 'audit'), + 'method' => 'drop_array', + 'default' => 'json', + 'array' => [ 'rfc5424' => __('RFC 5424', 'audit'), - 'cef' => __('CEF', 'audit'), - 'json' => __('JSON', 'audit') - ) - ), - 'audit_syslog_facility' => array( + 'cef' => __('CEF', 'audit'), + 'json' => __('JSON', 'audit') + ] + ], + 'audit_syslog_facility' => [ 'friendly_name' => __('Syslog Facility', 'audit'), - 'description' => __('Select the facility used to calculate the RFC 5424 priority.', 'audit'), - 'method' => 'drop_array', - 'default' => 'local0', - 'array' => $facility_options - ), - 'audit_syslog_application' => array( + 'description' => __('Select the facility used to calculate the RFC 5424 priority.', 'audit'), + 'method' => 'drop_array', + 'default' => 'local0', + 'array' => $facility_options + ], + 'audit_syslog_application' => [ 'friendly_name' => __('Syslog Application Name', 'audit'), - 'description' => __('RFC 5424 APP-NAME. Printable non-space ASCII, up to 48 characters.', 'audit'), - 'method' => 'textbox', - 'default' => 'cacti-audit', - 'max_length' => '48', - 'size' => '30' - ), - 'audit_syslog_node_id' => array( + 'description' => __('RFC 5424 APP-NAME. Printable non-space ASCII, up to 48 characters.', 'audit'), + 'method' => 'textbox', + 'default' => 'cacti-audit', + 'max_length' => '48', + 'size' => '30' + ], + 'audit_syslog_node_id' => [ 'friendly_name' => __('Audit Node Identity', 'audit'), - 'description' => __('Stable RFC 5424 hostname identity for this Cacti node. Do not use a value that changes on restart.', 'audit'), - 'method' => 'textbox', - 'default' => php_uname('n'), - 'max_length' => '255', - 'size' => '60' - ), - 'audit_syslog_timeout' => array( + 'description' => __('Stable RFC 5424 hostname identity for this Cacti node. Do not use a value that changes on restart.', 'audit'), + 'method' => 'textbox', + 'default' => php_uname('n'), + 'max_length' => '255', + 'size' => '60' + ], + 'audit_syslog_timeout' => [ 'friendly_name' => __('Connection and Write Timeout', 'audit'), - 'description' => __('Timeout in seconds, from 1 through 30.', 'audit'), - 'method' => 'textbox', - 'default' => '5', - 'max_length' => '2', - 'size' => '8' - ), - 'audit_syslog_udp_max_size' => array( + 'description' => __('Timeout in seconds, from 1 through 30.', 'audit'), + 'method' => 'textbox', + 'default' => '5', + 'max_length' => '2', + 'size' => '8' + ], + 'audit_syslog_udp_max_size' => [ 'friendly_name' => __('Maximum UDP Record Size', 'audit'), - 'description' => __('Records larger than this byte limit are dead-lettered and are never truncated or split. TCP or TLS is recommended for large events.', 'audit'), - 'method' => 'textbox', - 'default' => '8192', - 'max_length' => '5', - 'size' => '10' - ), - 'audit_syslog_tls_header' => array( + 'description' => __('Records larger than this byte limit are dead-lettered and are never truncated or split. TCP or TLS is recommended for large events.', 'audit'), + 'method' => 'textbox', + 'default' => '8192', + 'max_length' => '5', + 'size' => '10' + ], + 'audit_syslog_tls_header' => [ 'friendly_name' => __('Syslog TLS', 'audit'), - 'method' => 'spacer' - ), - 'audit_syslog_tls_ca_file' => array( + 'method' => 'spacer' + ], + 'audit_syslog_tls_ca_file' => [ 'friendly_name' => __('TLS CA File', 'audit'), - 'description' => __('Optional PEM CA bundle. Peer and hostname verification are always enabled.', 'audit'), - 'method' => 'filepath', - 'default' => '', - 'max_length' => '255' - ), - 'audit_syslog_tls_client_cert' => array( + 'description' => __('Optional PEM CA bundle. Peer and hostname verification are always enabled.', 'audit'), + 'method' => 'filepath', + 'default' => '', + 'max_length' => '255' + ], + 'audit_syslog_tls_client_cert' => [ 'friendly_name' => __('TLS Client Certificate', 'audit'), - 'description' => __('Optional PEM client certificate. A client key must also be configured.', 'audit'), - 'method' => 'filepath', - 'default' => '', - 'max_length' => '255' - ), - 'audit_syslog_tls_client_key' => array( + 'description' => __('Optional PEM client certificate. A client key must also be configured.', 'audit'), + 'method' => 'filepath', + 'default' => '', + 'max_length' => '255' + ], + 'audit_syslog_tls_client_key' => [ 'friendly_name' => __('TLS Client Private Key', 'audit'), - 'description' => __('Optional readable PEM private-key path. The key contents are never stored in audit events.', 'audit'), - 'method' => 'filepath', - 'default' => '', - 'max_length' => '255' - ), - 'audit_syslog_delivery_header' => array( + 'description' => __('Optional readable PEM private-key path. The key contents are never stored in audit events.', 'audit'), + 'method' => 'filepath', + 'default' => '', + 'max_length' => '255' + ], + 'audit_syslog_delivery_header' => [ 'friendly_name' => __('Syslog Delivery Queue', 'audit'), - 'method' => 'spacer' - ), - 'audit_syslog_retry_base' => array( + 'method' => 'spacer' + ], + 'audit_syslog_retry_base' => [ 'friendly_name' => __('Retry Base Delay', 'audit'), - 'description' => __('Initial retry delay in seconds, from 1 through 3600.', 'audit'), - 'method' => 'textbox', - 'default' => '30', - 'max_length' => '4', - 'size' => '8' - ), - 'audit_syslog_retry_max' => array( + 'description' => __('Initial retry delay in seconds, from 1 through 3600.', 'audit'), + 'method' => 'textbox', + 'default' => '30', + 'max_length' => '4', + 'size' => '8' + ], + 'audit_syslog_retry_max' => [ 'friendly_name' => __('Maximum Retry Delay', 'audit'), - 'description' => __('Maximum retry delay in seconds, from the base delay through 86400.', 'audit'), - 'method' => 'textbox', - 'default' => '3600', - 'max_length' => '5', - 'size' => '8' - ), - 'audit_syslog_max_attempts' => array( + 'description' => __('Maximum retry delay in seconds, from the base delay through 86400.', 'audit'), + 'method' => 'textbox', + 'default' => '3600', + 'max_length' => '5', + 'size' => '8' + ], + 'audit_syslog_max_attempts' => [ 'friendly_name' => __('Maximum Delivery Attempts', 'audit'), - 'description' => __('Move a record to dead-letter after this many attempts, from 1 through 100.', 'audit'), - 'method' => 'textbox', - 'default' => '10', - 'max_length' => '3', - 'size' => '8' - ), - 'audit_syslog_batch_size' => array( + 'description' => __('Move a record to dead-letter after this many attempts, from 1 through 100.', 'audit'), + 'method' => 'textbox', + 'default' => '10', + 'max_length' => '3', + 'size' => '8' + ], + 'audit_syslog_batch_size' => [ 'friendly_name' => __('Poller Batch Size', 'audit'), - 'description' => __('Maximum due records processed per poller cycle, from 1 through 1000.', 'audit'), - 'method' => 'textbox', - 'default' => '100', - 'max_length' => '4', - 'size' => '8' - ), - 'audit_syslog_pending_age_warning' => array( + 'description' => __('Maximum due records processed per poller cycle, from 1 through 1000.', 'audit'), + 'method' => 'textbox', + 'default' => '100', + 'max_length' => '4', + 'size' => '8' + ], + 'audit_syslog_pending_age_warning' => [ 'friendly_name' => __('Pending Age Warning', 'audit'), - 'description' => __('Show an unhealthy warning when the oldest queued record reaches this age in seconds.', 'audit'), - 'method' => 'textbox', - 'default' => '900', - 'max_length' => '6', - 'size' => '10' - ), - 'audit_syslog_dead_letter_warning' => array( + 'description' => __('Show an unhealthy warning when the oldest queued record reaches this age in seconds.', 'audit'), + 'method' => 'textbox', + 'default' => '900', + 'max_length' => '6', + 'size' => '10' + ], + 'audit_syslog_dead_letter_warning' => [ 'friendly_name' => __('Dead-letter Warning Count', 'audit'), - 'description' => __('Show an unhealthy warning when this many records are dead-lettered.', 'audit'), - 'method' => 'textbox', - 'default' => '1', - 'max_length' => '7', - 'size' => '10' - ) - ); + 'description' => __('Show an unhealthy warning when this many records are dead-lettered.', 'audit'), + 'method' => 'textbox', + 'default' => '1', + 'max_length' => '7', + 'size' => '10' + ] + ]; $temp = array_merge($temp, $syslog); } @@ -721,13 +740,17 @@ function audit_config_settings() { } } -function audit_draw_navigation_text($nav) { - $nav['audit.php:'] = array( +/** + * @param array $nav + * @return array + */ +function audit_draw_navigation_text(array $nav): array { + $nav['audit.php:'] = [ 'title' => __('Audit Event Log', 'audit'), 'mapping' => 'index.php:', 'url' => 'audit.php', 'level' => '1' - ); + ]; return $nav; } diff --git a/tests/Pest.php b/tests/Pest.php index e2132a3..675e214 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -7,8 +7,6 @@ +-------------------------------------------------------------------------+ */ -/* - * Pest configuration file. - */ +// Pest configuration file. require_once __DIR__ . '/bootstrap.php'; diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php deleted file mode 100644 index bd8d5e1..0000000 --- a/tests/Security/Php74CompatibilityTest.php +++ /dev/null @@ -1,82 +0,0 @@ -not->toBeFalse("Required plugin file is missing: {$relativeFile}"); - expect(is_readable($path))->toBeTrue("Required plugin file is unreadable: {$relativeFile}"); - } - }); - - it('does not use str_contains (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - $contents = file_get_contents($path); - expect($contents)->not->toBeFalse("Unable to read {$relativeFile}"); - - expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0, - "{$relativeFile} uses str_contains() which requires PHP 8.0" - ); - } - }); - - it('does not use str_starts_with (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - $contents = file_get_contents($path); - expect($contents)->not->toBeFalse("Unable to read {$relativeFile}"); - - expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, - "{$relativeFile} uses str_starts_with() which requires PHP 8.0" - ); - } - }); - - it('does not use str_ends_with (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - $contents = file_get_contents($path); - expect($contents)->not->toBeFalse("Unable to read {$relativeFile}"); - - expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, - "{$relativeFile} uses str_ends_with() which requires PHP 8.0" - ); - } - }); - - it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { - foreach ($files as $relativeFile) { - $path = realpath(__DIR__ . '/../../' . $relativeFile); - - $contents = file_get_contents($path); - expect($contents)->not->toBeFalse("Unable to read {$relativeFile}"); - - expect(preg_match('/\?->/', $contents))->toBe(0, - "{$relativeFile} uses nullsafe operator which requires PHP 8.0" - ); - } - }); -}); diff --git a/tests/Security/Php81SyntaxTest.php b/tests/Security/Php81SyntaxTest.php new file mode 100644 index 0000000..f0daa1f --- /dev/null +++ b/tests/Security/Php81SyntaxTest.php @@ -0,0 +1,43 @@ +not->toBeFalse("Required plugin file is missing: {$relativeFile}"); + expect(is_readable($path))->toBeTrue("Required plugin file is unreadable: {$relativeFile}"); + } + }); + + it('uses short array syntax', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + $contents = file_get_contents($path); + + expect(preg_match('/\barray\s*\(/', $contents))->toBe(0, + "{$relativeFile} still uses long array() syntax" + ); + } + }); +}); diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php index fbdc78d..9a0c8cc 100644 --- a/tests/Security/PreparedStatementConsistencyTest.php +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -14,13 +14,12 @@ describe('prepared statement consistency in audit', function () { it('documents database helper usage in all plugin files', function () { - $targetFiles = array( + $targetFiles = [ 'audit.php', 'audit_functions.php', 'audit_syslog.php', 'setup.php', - ); - + ]; foreach ($targetFiles as $relativeFile) { $path = realpath(__DIR__ . '/../../' . $relativeFile); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php index 7f4a129..e3799d5 100644 --- a/tests/Security/SetupStructureTest.php +++ b/tests/Security/SetupStructureTest.php @@ -7,9 +7,7 @@ +-------------------------------------------------------------------------+ */ -/* - * Verify setup.php defines required plugin hooks and info function. - */ +// Verify setup.php defines required plugin hooks and info function. describe('audit setup.php structure', function () { $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); @@ -26,11 +24,24 @@ expect($source)->toContain('function plugin_audit_uninstall'); }); - it('returns version array with name key', function () use ($source) { - expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); + it('reads plugin info from INFO file', function () use ($source) { + expect($source)->toContain('parse_ini_file'); + expect($source)->toContain("'info'"); + }); + + it('returns an array when plugin info is missing or malformed', function () use ($source) { + expect($source)->toContain("\$info['info'] ?? null"); + expect($source)->toContain('is_array($plugin_info) ? $plugin_info : []'); }); - it('returns version array with version key', function () use ($source) { - expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); + it('INFO file defines name and version keys', function () { + $info_file = realpath(__DIR__ . '/../../INFO'); + expect($info_file)->not->toBeFalse('INFO file must exist'); + + $info = parse_ini_file($info_file, true); + expect($info)->not->toBeFalse('INFO file must be valid INI'); + expect($info)->toHaveKey('info'); + expect($info['info'])->toHaveKey('name'); + expect($info['info'])->toHaveKey('version'); }); }); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b701e11..e268ffd 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,43 +12,45 @@ * can be loaded in isolation without the full Cacti application. */ -$GLOBALS['__test_db_calls'] = array(); +$GLOBALS['__test_db_calls'] = []; if (!function_exists('db_execute')) { function db_execute($sql) { - $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute', 'sql' => $sql, 'params' => array()); + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute', 'sql' => $sql, 'params' => []]; + return true; } } if (!function_exists('db_execute_prepared')) { - function db_execute_prepared($sql, $params = array()) { - $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params); + function db_execute_prepared($sql, $params = []) { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params]; + return true; } } if (!function_exists('db_fetch_assoc')) { function db_fetch_assoc($sql) { - return array(); + return []; } } if (!function_exists('db_fetch_assoc_prepared')) { - function db_fetch_assoc_prepared($sql, $params = array()) { - return array(); + function db_fetch_assoc_prepared($sql, $params = []) { + return []; } } if (!function_exists('db_fetch_row')) { function db_fetch_row($sql) { - return array(); + return []; } } if (!function_exists('db_fetch_row_prepared')) { - function db_fetch_row_prepared($sql, $params = array()) { - return array(); + function db_fetch_row_prepared($sql, $params = []) { + return []; } } @@ -59,7 +61,7 @@ function db_fetch_cell($sql) { } if (!function_exists('db_fetch_cell_prepared')) { - function db_fetch_cell_prepared($sql, $params = array()) { + function db_fetch_cell_prepared($sql, $params = []) { return ''; } } diff --git a/tests/controller_security_test.php b/tests/controller_security_test.php index 2f445ae..80ba0c0 100644 --- a/tests/controller_security_test.php +++ b/tests/controller_security_test.php @@ -5,19 +5,19 @@ $javascript = file_get_contents(dirname(__DIR__) . '/js/functions.js'); $setup = file_get_contents(dirname(__DIR__) . '/setup.php'); -$required_controller_guards = array( +$required_controller_guards = [ "\$_SERVER['REQUEST_METHOD'] !== 'POST'", 'audit_user_is_admin()', 'csrf_check(false)', 'html_escape($data', "__('Outcome Reason:', 'audit')", "header('Content-Type: text/csv; charset=UTF-8')", - "fputcsv(", + 'fputcsv(', "case 'syslog_test':", "case 'syslog_retry':", 'audit_syslog_test_delivery()', 'audit_syslog_retry_dead_letters($delivery_ids)' -); +]; foreach ($required_controller_guards as $guard) { if (strpos($controller, $guard) === false) { @@ -26,13 +26,13 @@ } } -$required_schema_fragments = array( +$required_schema_fragments = [ "'audit.php' => __('Audit Log User'", "'audit_manage.php' => __('Audit Log Admin'", 'audit_setup_realms(true)', 'audit_setup_realms()', 'audit_remove_deprecated_realms()', - "auth_augment_roles(__('Audit Plugin', 'audit'), array('audit.php', 'audit_manage.php'))", + "auth_augment_roles(__('Audit Plugin', 'audit'), ['audit.php', 'audit_manage.php'])", 'api_plugin_register_hook(\'audit\', \'replicate_out\'', 'request_status', 'ADD COLUMN IF NOT EXISTS external_status', @@ -46,8 +46,8 @@ 'external_attempts', 'CREATE TABLE IF NOT EXISTS `audit_syslog_delivery`', 'DROP TABLE IF EXISTS audit_syslog_delivery', - "auth_augment_roles(__('Audit Plugin', 'audit'), array('audit.php', 'audit_manage.php'))" -); + "auth_augment_roles(__('Audit Plugin', 'audit'), ['audit.php', 'audit_manage.php'])" +]; foreach ($required_schema_fragments as $fragment) { if (strpos($setup, $fragment) === false) { @@ -56,12 +56,12 @@ } } -$required_verifier_fragments = array( +$required_verifier_fragments = [ 'audit_operation_verifier_for_request', "'user_realm_permissions'", "'realm_permissions_verified'", "register_shutdown_function('audit_finalize_request', \$audit_id, \$started_at, \$verifier)" -); +]; foreach ($required_verifier_fragments as $fragment) { if (strpos($functions, $fragment) === false) { @@ -91,7 +91,7 @@ } if (strpos($functions, 'audit_enforce_syslog_settings_request()') === false || - strpos($functions, "'audit.syslog.configuration.denied'") === false) { + strpos($functions, "'audit.syslog.configuration.denied'") === false) { fwrite(STDERR, 'Remote Syslog settings must enforce Audit Log Admin on save.' . PHP_EOL); exit(1); } @@ -107,7 +107,7 @@ } if (strpos($javascript, "loadPageUsingPost('audit.php?action=syslog_test") === false || - strpos($javascript, "loadPageUsingPost('audit.php?action=syslog_retry") === false) { + strpos($javascript, "loadPageUsingPost('audit.php?action=syslog_retry") === false) { fwrite(STDERR, 'Syslog administration actions must use POST request paths.' . PHP_EOL); exit(1); } diff --git a/tests/security_functions_test.php b/tests/security_functions_test.php index 4a736a5..f5bcee4 100644 --- a/tests/security_functions_test.php +++ b/tests/security_functions_test.php @@ -2,11 +2,15 @@ require_once dirname(__DIR__) . '/audit_functions.php'; -$audit_test_realms = array(); -$audit_test_existing_users = array(); -$audit_test_user_realms = array(); -$audit_test_user_query_failure = false; -$audit_test_realm_query_failure = false; +$audit_test_realms = []; +$audit_test_existing_users = []; +$audit_test_user_realms = []; +$audit_test_user_query_failure = false; +$audit_test_realm_query_failure = false; +$audit_test_object_query_failure = false; +$audit_test_external_event = false; +$audit_test_external_updates = []; +$audit_test_config_options = []; function api_plugin_user_realm_auth($filename = '') { global $audit_test_realms; @@ -14,7 +18,7 @@ function api_plugin_user_realm_auth($filename = '') { return !empty($audit_test_realms[$filename]); } -function db_fetch_cell_prepared($sql, $params = array()) { +function db_fetch_cell_prepared($sql, $params = []) { global $audit_test_existing_users, $audit_test_user_query_failure; if ($audit_test_user_query_failure) { @@ -26,21 +30,41 @@ function db_fetch_cell_prepared($sql, $params = array()) { return in_array($user_id, $audit_test_existing_users, true) ? 1 : 0; } -function db_fetch_assoc_prepared($sql, $params = array()) { - global $audit_test_user_realms, $audit_test_realm_query_failure; +function db_fetch_assoc_prepared($sql, $params = []) { + global $audit_test_user_realms, $audit_test_realm_query_failure, $audit_test_object_query_failure; - if ($audit_test_realm_query_failure) { + if ($audit_test_realm_query_failure || $audit_test_object_query_failure) { return false; } - $user_id = (int) ($params[0] ?? 0); - $realm_ids = $audit_test_user_realms[$user_id] ?? array(); + $user_id = (int) ($params[0] ?? 0); + $realm_ids = $audit_test_user_realms[$user_id] ?? []; - return array_map(function($realm_id) { - return array('realm_id' => $realm_id); + return array_map(function ($realm_id) { + return ['realm_id' => $realm_id]; }, $realm_ids); } +function db_fetch_row_prepared(string $sql, array $params = []): array|false { + global $audit_test_external_event; + + return $audit_test_external_event; +} + +function db_execute_prepared(string $sql, array $params = []): bool { + global $audit_test_external_updates; + + $audit_test_external_updates[] = ['sql' => $sql, 'params' => $params]; + + return true; +} + +function read_config_option(string $name): mixed { + global $audit_test_config_options; + + return $audit_test_config_options[$name] ?? ''; +} + function audit_test_assert_same($expected, $actual, $message) { if ($expected !== $actual) { fwrite(STDERR, $message . PHP_EOL); @@ -53,90 +77,98 @@ function audit_test_assert_same($expected, $actual, $message) { audit_test_assert_same(false, audit_user_is_admin(), 'Audit users must not be treated as audit administrators.'); $audit_test_realms['audit_manage.php'] = true; audit_test_assert_same(true, audit_user_is_admin(), 'Audit plugin administrators must be able to purge.'); -$audit_test_realms = array(); +$audit_test_realms = []; -$verifier = audit_operation_verifier_for_request('user_admin.php', array( - 'action' => 'save', - 'id' => '4', +$verifier = audit_operation_verifier_for_request('user_admin.php', [ + 'action' => 'save', + 'id' => '4', 'save_component_realm_perms' => '1', - 'section110' => 'on', - 'section106' => 'on' -)); + 'section110' => 'on', + 'section106' => 'on' +]); audit_test_assert_same( - array( - 'type' => 'user_realm_permissions', - 'target_user_id' => 4, - 'expected_realm_ids' => array(106, 110) - ), + [ + 'type' => 'user_realm_permissions', + 'target_user_id' => 4, + 'expected_realm_ids' => [106, 110] + ], $verifier, 'User realm permission saves must capture a normalized post-condition verifier.' ); audit_test_assert_same( null, - audit_operation_verifier_for_request('host.php', array('id' => '4')), + audit_operation_verifier_for_request('host.php', ['id' => '4']), 'Unrelated requests must not receive a user realm verifier.' ); -$invalid_verifier = audit_operation_verifier_for_request('user_admin.php', array( - 'id' => 'invalid', +$invalid_verifier = audit_operation_verifier_for_request('user_admin.php', [ + 'id' => 'invalid', 'save_component_realm_perms' => '1' -)); +]); audit_test_assert_same( 'invalid', $invalid_verifier['type'], 'Invalid realm permission requests must not be verified as successful.' ); -$audit_test_existing_users = array(4); -$audit_test_user_realms = array(4 => array(110, 106)); +$audit_test_existing_users = [4]; +$audit_test_user_realms = [4 => [110, 106]]; audit_test_assert_same( - array('outcome' => 'success', 'reason' => 'realm_permissions_verified'), + ['outcome' => 'success', 'reason' => 'realm_permissions_verified'], audit_verify_operation($verifier), 'Matching stored realm permissions must produce a verified success outcome.' ); -$audit_test_user_realms = array(4 => array(106)); +$audit_test_user_realms = [4 => [106]]; audit_test_assert_same( - array('outcome' => 'failure', 'reason' => 'realm_permissions_mismatch'), + ['outcome' => 'failure', 'reason' => 'realm_permissions_mismatch'], audit_verify_operation($verifier), 'Mismatched stored realm permissions must produce a failure outcome.' ); -$audit_test_existing_users = array(); +$audit_test_existing_users = []; audit_test_assert_same( - array('outcome' => 'failure', 'reason' => 'target_user_not_found'), + ['outcome' => 'failure', 'reason' => 'target_user_not_found'], audit_verify_operation($verifier), 'A missing target user must produce a failure outcome.' ); -$audit_test_existing_users = array(4); +$audit_test_existing_users = [4]; $audit_test_user_query_failure = true; audit_test_assert_same( - array('outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'), + ['outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'], audit_verify_operation($verifier), 'A failed target-user query must preserve an unknown outcome.' ); $audit_test_user_query_failure = false; -$audit_test_existing_users = array(4); +$audit_test_existing_users = [4]; $audit_test_realm_query_failure = true; audit_test_assert_same( - array('outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'), + ['outcome' => 'unknown', 'reason' => 'realm_permissions_verification_failed'], audit_verify_operation($verifier), 'A failed verification query must preserve an unknown outcome.' ); $audit_test_realm_query_failure = false; -$request = array( +$audit_test_object_query_failure = true; +audit_test_assert_same( + [], + json_decode(audit_process_page_data('automation_devices.php', '1', ['42']), true), + 'Failed automation-device queries must not add false entries to object data.' +); +$audit_test_object_query_failure = false; + +$request = [ 'username' => 'operator', 'password' => 'top-secret', - 'nested' => array( - 'api_token' => 'nested-secret', - 'description' => '', + 'nested' => [ + 'api_token' => 'nested-secret', + 'description' => '', 'opaque_value' => 'Bearer abcdefghijklmnopqrstuvwxyz' - ) -); + ] +]; $redacted = audit_redact_sensitive_data($request); @@ -149,14 +181,14 @@ function audit_test_assert_same($expected, $actual, $message) { 'Non-secret data must remain available for later context-aware output escaping.' ); -$arguments = audit_redact_cli_arguments(array( +$arguments = audit_redact_cli_arguments([ 'cli/example.php', '--password=secret-one', '--api-token', 'secret-two', 'https://user:secret-three@example.com/path', '--description=test' -)); +]); audit_test_assert_same('--password=[REDACTED]', $arguments[1], 'Inline CLI passwords must be redacted.'); audit_test_assert_same('[REDACTED]', $arguments[3], 'Separate CLI secret values must be redacted.'); @@ -168,15 +200,17 @@ function audit_test_assert_same($expected, $actual, $message) { audit_test_assert_same("'=1+1", audit_csv_safe_cell('=1+1'), 'Spreadsheet formulas must be neutralized.'); -$deep = array(); +$deep = []; $cursor = &$deep; + for ($i = 0; $i < 15; $i++) { - $cursor['nested'] = array(); - $cursor = &$cursor['nested']; + $cursor['nested'] = []; + $cursor = &$cursor['nested']; } unset($cursor); $bounded_json = audit_json_encode($deep); + if (strpos($bounded_json, 'MAXIMUM DEPTH REACHED') === false) { fwrite(STDERR, 'Deep request structures must be bounded.' . PHP_EOL); exit(1); @@ -184,6 +218,7 @@ function audit_test_assert_same($expected, $actual, $message) { $invalid_json = audit_json_decode('{invalid', $json_error); audit_test_assert_same(null, $invalid_json, 'Malformed JSON must return a controlled null result.'); + if ($json_error === null || $json_error === '') { fwrite(STDERR, 'Malformed JSON must return a useful error.' . PHP_EOL); exit(1); @@ -204,11 +239,12 @@ function audit_test_assert_same($expected, $actual, $message) { ); audit_test_assert_same( 'failed', - audit_request_status(array('type' => E_ERROR), 200), + audit_request_status(['type' => E_ERROR], 200), 'Fatal errors must finalize as failed requests.' ); $uuid = audit_uuid_v4(); + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid)) { fwrite(STDERR, 'Event identifiers must be RFC 4122 version 4 UUIDs.' . PHP_EOL); exit(1); @@ -225,38 +261,39 @@ function audit_test_assert_same($expected, $actual, $message) { 'Requests without a specific action must use the submitted event verb.' ); -$hash_event = array( - 'event_uuid' => $uuid, - 'correlation_id' => audit_uuid_v4(), - 'event_type' => 'cacti.test.completed', - 'user_id' => 1, - 'action' => 'test', - 'event_time' => '2026-07-24 10:00:00', +$hash_event = [ + 'event_uuid' => $uuid, + 'correlation_id' => audit_uuid_v4(), + 'event_type' => 'cacti.test.completed', + 'user_id' => 1, + 'action' => 'test', + 'event_time' => '2026-07-24 10:00:00', 'operation_outcome' => 'success', - 'details' => '{}' -); -$first_hash = audit_event_integrity_hash($hash_event); + 'details' => '{}' +]; +$first_hash = audit_event_integrity_hash($hash_event); $hash_event['operation_outcome'] = 'failure'; + if ($first_hash === audit_event_integrity_hash($hash_event)) { fwrite(STDERR, 'Integrity hashes must change when protected event fields change.' . PHP_EOL); exit(1); } -$external_record = array( - 'event_time' => '2026-07-24 10:00:00', - 'action' => "Update\nDevice", - 'post' => '{"id":42}', +$external_record = [ + 'event_time' => '2026-07-24 10:00:00', + 'action' => "Update\nDevice", + 'post' => '{"id":42}', 'object_data' => '[]' -); +]; $json_record = audit_external_log_format($external_record, 'json'); audit_test_assert_same("\n", substr($json_record, -1), 'JSON external records must end with a newline.'); audit_test_assert_same( - array( - 'event_time' => '2026-07-24 10:00:00', - 'action' => "Update\nDevice", - 'post' => array('id' => 42), - 'object_data' => array() - ), + [ + 'event_time' => '2026-07-24 10:00:00', + 'action' => "Update\nDevice", + 'post' => ['id' => 42], + 'object_data' => [] + ], json_decode(trim($json_record), true), 'JSON external records must expose stored JSON fields as native structures.' ); @@ -267,7 +304,7 @@ function audit_test_assert_same($expected, $actual, $message) { ); audit_test_assert_same( '{"post":"not-json"}' . "\n", - audit_external_log_format(array('post' => 'not-json'), 'json'), + audit_external_log_format(['post' => 'not-json'], 'json'), 'Malformed stored JSON fields must remain available as strings.' ); audit_test_assert_same( @@ -280,6 +317,24 @@ function audit_test_assert_same($expected, $actual, $message) { $delivery = audit_append_external_log($temporary_log, "test-record\n"); audit_test_assert_same('delivered', $delivery['status'], 'A complete external log write must report delivery.'); audit_test_assert_same("test-record\n", file_get_contents($temporary_log), 'External log content must be complete.'); + +$audit_test_config_options = [ + 'audit_log_external' => 'on', + 'audit_log_external_path' => $temporary_log, + 'audit_log_external_format' => 'json' +]; +$audit_test_external_event = false; +$audit_test_external_updates = []; +file_put_contents($temporary_log, ''); +audit_deliver_external_event(999); +audit_test_assert_same('', file_get_contents($temporary_log), 'Missing audit events must not create external records.'); +audit_test_assert_same([], $audit_test_external_updates, 'Missing audit events must not update delivery status.'); + +$audit_test_external_event = []; +audit_deliver_external_event(999); +audit_test_assert_same('', file_get_contents($temporary_log), 'Empty audit events must not create external records.'); +audit_test_assert_same([], $audit_test_external_updates, 'Empty audit events must not update delivery status.'); + unlink($temporary_log); print "Security helper tests passed.\n"; diff --git a/tests/syslog_functions_test.php b/tests/syslog_functions_test.php index 0adf595..5dae55e 100644 --- a/tests/syslog_functions_test.php +++ b/tests/syslog_functions_test.php @@ -13,59 +13,59 @@ function audit_syslog_test_assert($condition, $message) { } } -function audit_syslog_test_config($overrides = array()) { - $values = array( - 'receiver' => '127.0.0.1', - 'port' => '514', - 'transport' => 'udp', - 'format' => 'json', - 'facility' => 'local0', - 'application' => 'cacti-audit', - 'node_id' => 'cacti-node-1', - 'timeout' => '1', - 'udp_max_size' => '8192', - 'retry_base' => '5', - 'retry_max' => '60', - 'max_attempts' => '5', - 'batch_size' => '10', +function audit_syslog_test_config($overrides = []) { + $values = [ + 'receiver' => '127.0.0.1', + 'port' => '514', + 'transport' => 'udp', + 'format' => 'json', + 'facility' => 'local0', + 'application' => 'cacti-audit', + 'node_id' => 'cacti-node-1', + 'timeout' => '1', + 'udp_max_size' => '8192', + 'retry_base' => '5', + 'retry_max' => '60', + 'max_attempts' => '5', + 'batch_size' => '10', 'pending_age_warning' => '900', 'dead_letter_warning' => '1', - 'tls_ca_file' => '', - 'tls_client_cert' => '', - 'tls_client_key' => '' - ); + 'tls_ca_file' => '', + 'tls_client_cert' => '', + 'tls_client_key' => '' + ]; return audit_syslog_config(array_merge($values, $overrides)); } function audit_syslog_test_event() { - return array( - 'id' => 42, - 'event_uuid' => '32e0a97d-d9e8-4abc-8f41-2bbbc50793ca', - 'correlation_id' => '48f486c3-90d0-45a2-a50d-3b31e571aa91', - 'event_type' => 'cacti.user_admin.save', - 'event_category' => 'identity_access', - 'severity' => 'info', - 'actor_type' => 'user', - 'page' => 'user_admin.php', - 'user_id' => 1, - 'action' => 'save', - 'request_status' => 'completed', + return [ + 'id' => 42, + 'event_uuid' => '32e0a97d-d9e8-4abc-8f41-2bbbc50793ca', + 'correlation_id' => '48f486c3-90d0-45a2-a50d-3b31e571aa91', + 'event_type' => 'cacti.user_admin.save', + 'event_category' => 'identity_access', + 'severity' => 'info', + 'actor_type' => 'user', + 'page' => 'user_admin.php', + 'user_id' => 1, + 'action' => 'save', + 'request_status' => 'completed', 'operation_outcome' => 'success', - 'target_type' => 'user', - 'target_id' => '4', - 'ip_address' => '192.0.2.10', - 'event_time' => '2026-07-24 15:16:17.123456', - 'post' => '{"id":4,"description":"new value","password":"must-not-leak"}', - 'object_data' => '[{"id":4,"description":"old value"}]', - 'details' => '{"test":true}', - 'integrity_hash' => str_repeat('a', 64) - ); + 'target_type' => 'user', + 'target_id' => '4', + 'ip_address' => '192.0.2.10', + 'event_time' => '2026-07-24 15:16:17.123456', + 'post' => '{"id":4,"description":"new value","password":"must-not-leak"}', + 'object_data' => '[{"id":4,"description":"old value"}]', + 'details' => '{"test":true}', + 'integrity_hash' => str_repeat('a', 64) + ]; } function audit_syslog_test_udp_server(&$port, &$error, &$error_message) { for ($attempt = 0; $attempt < 20; $attempt++) { - $port = random_int(20000, 50000); + $port = random_int(20000, 50000); $server = @stream_socket_server( 'udp://127.0.0.1:' . $port, $error, @@ -83,6 +83,7 @@ function audit_syslog_test_udp_server(&$port, &$error, &$error_message) { function audit_syslog_test_tls_material() { $directory = sys_get_temp_dir() . '/audit-syslog-' . bin2hex(random_bytes(8)); + if (!mkdir($directory, 0700)) { return false; } @@ -100,16 +101,16 @@ function audit_syslog_test_tls_material() { "extendedKeyUsage=serverAuth\n"; file_put_contents($config_path, $config_text); - $options = array( - 'config' => $config_path, - 'digest_alg' => 'sha256', + $options = [ + 'config' => $config_path, + 'digest_alg' => 'sha256', 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, - 'req_extensions' => 'v3_req', - 'x509_extensions' => 'v3_req' - ); - $key = openssl_pkey_new($options); - $csr = $key === false ? false : openssl_csr_new(array('commonName' => '127.0.0.1'), $key, $options); + 'req_extensions' => 'v3_req', + 'x509_extensions' => 'v3_req' + ]; + $key = openssl_pkey_new($options); + $csr = $key === false ? false : openssl_csr_new(['commonName' => '127.0.0.1'], $key, $options); $certificate = $csr === false ? false : openssl_csr_sign($csr, null, $key, 1, $options); if ($certificate === false || @@ -118,33 +119,33 @@ function audit_syslog_test_tls_material() { return false; } - $ca_path = $directory . '/ca.pem'; + $ca_path = $directory . '/ca.pem'; $server_path = $directory . '/server.pem'; $result_path = $directory . '/received.log'; file_put_contents($ca_path, $certificate_pem); file_put_contents($server_path, $certificate_pem . $key_pem); chmod($server_path, 0600); - return array( + return [ 'directory' => $directory, - 'config' => $config_path, - 'ca' => $ca_path, - 'server' => $server_path, - 'result' => $result_path - ); + 'config' => $config_path, + 'ca' => $ca_path, + 'server' => $server_path, + 'result' => $result_path + ]; } function audit_syslog_test_tls_server($material, &$port) { - $context = stream_context_create(array( - 'ssl' => array( - 'local_cert' => $material['server'], - 'verify_peer' => false, + $context = stream_context_create([ + 'ssl' => [ + 'local_cert' => $material['server'], + 'verify_peer' => false, 'allow_self_signed' => true - ) - )); - $error = 0; + ] + ]); + $error = 0; $error_message = ''; - $server = stream_socket_server( + $server = stream_socket_server( 'tls://127.0.0.1:0', $error, $error_message, @@ -163,7 +164,7 @@ function audit_syslog_test_tls_server($material, &$port) { } function audit_syslog_test_remove_tls_material($material) { - foreach (array('result', 'server', 'ca', 'config') as $name) { + foreach (['result', 'server', 'ca', 'config'] as $name) { if (is_file($material[$name])) { unlink($material[$name]); } @@ -177,22 +178,24 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert($config['tls_verify_peer'] === true, 'TLS peer verification must always be enabled.'); audit_syslog_test_assert($config['tls_verify_peer_name'] === true, 'TLS hostname verification must always be enabled.'); audit_syslog_test_assert( - audit_syslog_test_config(array('transport' => 'udp', 'port' => ''))['port'] === 514, + audit_syslog_test_config(['transport' => 'udp', 'port' => ''])['port'] === 514, 'A blank UDP port must use the standard default of 514.' ); audit_syslog_test_assert( - audit_syslog_test_config(array('transport' => 'tls', 'port' => ''))['port'] === 6514, + audit_syslog_test_config(['transport' => 'tls', 'port' => ''])['port'] === 6514, 'A blank TLS port must use the standard default of 6514.' ); $outer_warning_called = false; -$captured_warning = ''; -set_error_handler(function() use (&$outer_warning_called) { +$captured_warning = ''; +set_error_handler(function () use (&$outer_warning_called) { $outer_warning_called = true; + return true; }); -$warning_result = audit_syslog_stream_operation(function() { +$warning_result = audit_syslog_stream_operation(function () { trigger_error("expected stream warning\nwith control text", E_USER_WARNING); + return false; }, $captured_warning); restore_error_handler(); @@ -201,10 +204,10 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert($captured_warning === 'expected stream warning with control text', 'Captured stream warnings must be normalized into a bounded one-line delivery error.'); -$invalid = audit_syslog_test_config(array('receiver' => 'udp://user:secret@example.test')); +$invalid = audit_syslog_test_config(['receiver' => 'udp://user:secret@example.test']); audit_syslog_test_assert(!$invalid['valid'], 'Receiver URIs and embedded credentials must be rejected.'); -$event = audit_syslog_test_event(); +$event = audit_syslog_test_event(); $formatted = audit_syslog_record($event, $config); audit_syslog_test_assert($formatted['status'] === 'ready', 'A valid event must produce a Syslog record.'); audit_syslog_test_assert(strpos($formatted['record'], '<134>1 2026-07-24T15:16:17.123456Z cacti-node-1 cacti-audit 7 cacti.user_admin.save ') === 0, @@ -222,8 +225,8 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert(audit_syslog_frame($formatted['record'], 'udp') === $formatted['record'], 'UDP must send one unframed RFC 5424 record per datagram.'); -$cef_config = audit_syslog_test_config(array('format' => 'cef')); -$cef = audit_syslog_record($event, $cef_config); +$cef_config = audit_syslog_test_config(['format' => 'cef']); +$cef = audit_syslog_record($event, $cef_config); audit_syslog_test_assert(strpos($cef['record'], 'CEF:0|Cacti|Audit Plugin|1.5|cacti.user_admin.save|save|3|') !== false, 'CEF payloads must contain normalized vendor, product, event, action, and severity fields.'); audit_syslog_test_assert(strpos($cef['record'], 'externalId=32e0a97d-d9e8-4abc-8f41-2bbbc50793ca') !== false, @@ -237,21 +240,21 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert(strpos($cef['record'], 'must-not-leak') === false, 'CEF payloads must defensively redact sensitive submitted fields.'); -$rfc_config = audit_syslog_test_config(array('format' => 'rfc5424')); -$rfc = audit_syslog_record($event, $rfc_config); +$rfc_config = audit_syslog_test_config(['format' => 'rfc5424']); +$rfc = audit_syslog_record($event, $rfc_config); audit_syslog_test_assert(strpos($rfc['record'], 'Audit event 32e0a97d-d9e8-4abc-8f41-2bbbc50793ca') !== false, 'RFC 5424-only payloads must remain identifiable.'); -$escaped_event = $event; +$escaped_event = $event; $escaped_event['target_id'] = "value\\\"]\nnext"; -$escaped = audit_syslog_record($escaped_event, $config); +$escaped = audit_syslog_record($escaped_event, $config); audit_syslog_test_assert(strpos($escaped['record'], 'targetId="value\\\\\\"\\] next"') !== false, 'RFC 5424 structured data must escape backslashes, quotes, brackets, and control characters.'); -$small_udp = audit_syslog_test_config(array('udp_max_size' => '512')); -$large_event = $event; -$large_event['details'] = audit_json_encode(array('value' => str_repeat('x', 1024))); -$oversized = audit_syslog_record($large_event, $small_udp); +$small_udp = audit_syslog_test_config(['udp_max_size' => '512']); +$large_event = $event; +$large_event['details'] = audit_json_encode(['value' => str_repeat('x', 1024)]); +$oversized = audit_syslog_record($large_event, $small_udp); audit_syslog_test_assert($oversized['error_code'] === 'udp_message_too_large' && $oversized['permanent'], 'Oversized UDP events must fail permanently without truncation or splitting.'); @@ -259,12 +262,12 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert($formatted['record'] === $again['record'], 'Repeated formatting of one event must preserve receiver deduplication identity.'); -$udp_error = 0; +$udp_error = 0; $udp_error_message = ''; -$udp_port = 0; -$udp_server = audit_syslog_test_udp_server($udp_port, $udp_error, $udp_error_message); +$udp_port = 0; +$udp_server = audit_syslog_test_udp_server($udp_port, $udp_error, $udp_error_message); audit_syslog_test_assert(is_resource($udp_server), 'The UDP integration-test receiver must start.'); -$udp_config = audit_syslog_test_config(array('port' => (string) $udp_port)); +$udp_config = audit_syslog_test_config(['port' => (string) $udp_port]); $udp_socket = null; $udp_result = audit_syslog_send_event($event, $udp_config, $udp_socket); audit_syslog_test_assert($udp_result['status'] === 'sent_unconfirmed', @@ -273,18 +276,19 @@ function audit_syslog_test_remove_tls_material($material) { $udp_received = fread($udp_server, 65535); audit_syslog_test_assert($udp_received === audit_syslog_record($event, $udp_config)['record'], 'UDP transport must deliver exactly one complete untruncated record.'); + if (is_resource($udp_socket)) { fclose($udp_socket); } fclose($udp_server); -$tcp_error = 0; +$tcp_error = 0; $tcp_error_message = ''; -$tcp_server = stream_socket_server('tcp://127.0.0.1:0', $tcp_error, $tcp_error_message); +$tcp_server = stream_socket_server('tcp://127.0.0.1:0', $tcp_error, $tcp_error_message); audit_syslog_test_assert(is_resource($tcp_server), 'The TCP integration-test receiver must start.'); -$tcp_name = stream_socket_get_name($tcp_server, false); -$tcp_port = (int) substr($tcp_name, strrpos($tcp_name, ':') + 1); -$tcp_config = audit_syslog_test_config(array('transport' => 'tcp', 'port' => (string) $tcp_port)); +$tcp_name = stream_socket_get_name($tcp_server, false); +$tcp_port = (int) substr($tcp_name, strrpos($tcp_name, ':') + 1); +$tcp_config = audit_syslog_test_config(['transport' => 'tcp', 'port' => (string) $tcp_port]); $tcp_socket = null; $tcp_result = audit_syslog_send_event($event, $tcp_config, $tcp_socket); audit_syslog_test_assert($tcp_result['status'] === 'sent_unconfirmed', @@ -297,6 +301,7 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert($tcp_received === $expected_tcp, 'TCP transport must deliver one complete RFC 6587 octet-counted record.'); fclose($tcp_peer); + if (is_resource($tcp_socket)) { fclose($tcp_socket); } @@ -310,14 +315,16 @@ function audit_syslog_test_remove_tls_material($material) { socket_set_option($native_socket, SOL_SOCKET, SO_SNDBUF, 1024); stream_set_timeout($pair[0], 2); $partial_payload = str_repeat('p', 262144); - $partial_pid = pcntl_fork(); + $partial_pid = pcntl_fork(); audit_syslog_test_assert($partial_pid >= 0, 'The partial-write test must fork a local reader.'); if ($partial_pid === 0) { fclose($pair[0]); $received_length = 0; + while ($received_length < strlen($partial_payload)) { $chunk = fread($pair[1], 8192); + if ($chunk === false || $chunk === '') { break; } @@ -332,16 +339,16 @@ function audit_syslog_test_remove_tls_material($material) { fclose($pair[0]); pcntl_waitpid($partial_pid, $partial_status); audit_syslog_test_assert($partial_result['status'] === 'sent_unconfirmed' && - pcntl_wexitstatus($partial_status) === 0, + pcntl_wexitstatus($partial_status) === 0, 'TCP writes must loop until a record is complete when the socket accepts partial chunks.'); - $timeout_pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + $timeout_pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); $timeout_socket = socket_import_stream($timeout_pair[0]); socket_set_option($timeout_socket, SOL_SOCKET, SO_SNDBUF, 1024); stream_set_timeout($timeout_pair[0], 0, 100000); $timeout_result = audit_syslog_write($timeout_pair[0], str_repeat('t', 4194304), 'tcp'); audit_syslog_test_assert($timeout_result['status'] === 'failed' && - $timeout_result['error_code'] === 'write_failed', + $timeout_result['error_code'] === 'write_failed', 'A blocked stream must fail with a bounded write timeout instead of hanging indefinitely.'); fclose($timeout_pair[0]); fclose($timeout_pair[1]); @@ -350,7 +357,7 @@ function audit_syslog_test_remove_tls_material($material) { if (extension_loaded('openssl') && function_exists('pcntl_fork')) { $tls_material = audit_syslog_test_tls_material(); audit_syslog_test_assert(is_array($tls_material), 'The TLS integration test must create temporary certificate material.'); - $tls_port = 0; + $tls_port = 0; $tls_server = audit_syslog_test_tls_server($tls_material, $tls_port); audit_syslog_test_assert(is_resource($tls_server), 'The TLS integration-test receiver must start.'); $tls_pid = pcntl_fork(); @@ -358,6 +365,7 @@ function audit_syslog_test_remove_tls_material($material) { if ($tls_pid === 0) { $tls_peer = @stream_socket_accept($tls_server, 5); + if (is_resource($tls_peer)) { stream_set_timeout($tls_peer, 2); $tls_received = fread($tls_peer, 262144); @@ -370,13 +378,14 @@ function audit_syslog_test_remove_tls_material($material) { } fclose($tls_server); - $tls_config = audit_syslog_test_config(array( - 'transport' => 'tls', - 'port' => (string) $tls_port, + $tls_config = audit_syslog_test_config([ + 'transport' => 'tls', + 'port' => (string) $tls_port, 'tls_ca_file' => $tls_material['ca'] - )); + ]); $tls_socket = null; $tls_result = audit_syslog_send_event($event, $tls_config, $tls_socket); + if (is_resource($tls_socket)) { fclose($tls_socket); } @@ -390,7 +399,7 @@ function audit_syslog_test_remove_tls_material($material) { audit_syslog_test_assert($tls_received === $expected_tls, 'TLS transport must deliver one complete RFC 6587 octet-counted record.'); - $untrusted_port = 0; + $untrusted_port = 0; $untrusted_server = audit_syslog_test_tls_server($tls_material, $untrusted_port); audit_syslog_test_assert(is_resource($untrusted_server), 'The untrusted TLS test receiver must start.'); $untrusted_pid = pcntl_fork(); @@ -398,6 +407,7 @@ function audit_syslog_test_remove_tls_material($material) { if ($untrusted_pid === 0) { $untrusted_peer = @stream_socket_accept($untrusted_server, 5); + if (is_resource($untrusted_peer)) { fclose($untrusted_peer); } @@ -405,41 +415,42 @@ function audit_syslog_test_remove_tls_material($material) { } fclose($untrusted_server); - $untrusted_config = audit_syslog_test_config(array( - 'transport' => 'tls', - 'port' => (string) $untrusted_port, + $untrusted_config = audit_syslog_test_config([ + 'transport' => 'tls', + 'port' => (string) $untrusted_port, 'tls_ca_file' => '' - )); + ]); $untrusted_socket = null; $untrusted_result = audit_syslog_send_event($event, $untrusted_config, $untrusted_socket); + if (is_resource($untrusted_socket)) { fclose($untrusted_socket); } pcntl_waitpid($untrusted_pid, $untrusted_status); audit_syslog_test_assert($untrusted_result['status'] === 'failed' && - $untrusted_result['error_code'] === 'connection_failed', + $untrusted_result['error_code'] === 'connection_failed', 'TLS delivery must fail when the receiver certificate is not trusted.'); audit_syslog_test_remove_tls_material($tls_material); } -$outage_error = 0; +$outage_error = 0; $outage_message = ''; -$outage_server = stream_socket_server('tcp://127.0.0.1:0', $outage_error, $outage_message); -$outage_name = stream_socket_get_name($outage_server, false); -$outage_port = (int) substr($outage_name, strrpos($outage_name, ':') + 1); +$outage_server = stream_socket_server('tcp://127.0.0.1:0', $outage_error, $outage_message); +$outage_name = stream_socket_get_name($outage_server, false); +$outage_port = (int) substr($outage_name, strrpos($outage_name, ':') + 1); fclose($outage_server); -$outage_config = audit_syslog_test_config(array('transport' => 'tcp', 'port' => (string) $outage_port)); +$outage_config = audit_syslog_test_config(['transport' => 'tcp', 'port' => (string) $outage_port]); $outage_socket = null; -$outage = audit_syslog_send_event($event, $outage_config, $outage_socket); +$outage = audit_syslog_send_event($event, $outage_config, $outage_socket); audit_syslog_test_assert($outage['status'] === 'failed' && $outage['error_code'] === 'connection_failed', 'An unavailable receiver must return a bounded transient connection failure.'); -$invalid_tls = audit_syslog_test_config(array( - 'transport' => 'tls', - 'port' => '6514', +$invalid_tls = audit_syslog_test_config([ + 'transport' => 'tls', + 'port' => '6514', 'tls_ca_file' => '/does/not/exist' -)); +]); audit_syslog_test_assert(!$invalid_tls['valid'] && in_array('tls_ca_file_invalid', $invalid_tls['errors'], true), 'An invalid TLS trust path must be rejected before a connection is attempted.'); diff --git a/tests/syslog_queue_test.php b/tests/syslog_queue_test.php index 775f161..b88913c 100644 --- a/tests/syslog_queue_test.php +++ b/tests/syslog_queue_test.php @@ -1,27 +1,27 @@ 'on', - 'audit_syslog_receiver' => '127.0.0.1', - 'audit_syslog_port' => '514', - 'audit_syslog_transport' => 'udp', - 'audit_syslog_format' => 'json', - 'audit_syslog_facility' => 'local0', - 'audit_syslog_application' => 'cacti-audit', - 'audit_syslog_node_id' => 'queue-test-node', - 'audit_syslog_timeout' => '5', - 'audit_syslog_udp_max_size' => '8192', - 'audit_syslog_retry_base' => '5', - 'audit_syslog_retry_max' => '60', - 'audit_syslog_max_attempts' => '5', - 'audit_syslog_batch_size' => '10', +$audit_queue_settings = [ + 'audit_syslog_enabled' => 'on', + 'audit_syslog_receiver' => '127.0.0.1', + 'audit_syslog_port' => '514', + 'audit_syslog_transport' => 'udp', + 'audit_syslog_format' => 'json', + 'audit_syslog_facility' => 'local0', + 'audit_syslog_application' => 'cacti-audit', + 'audit_syslog_node_id' => 'queue-test-node', + 'audit_syslog_timeout' => '5', + 'audit_syslog_udp_max_size' => '8192', + 'audit_syslog_retry_base' => '5', + 'audit_syslog_retry_max' => '60', + 'audit_syslog_max_attempts' => '5', + 'audit_syslog_batch_size' => '10', 'audit_syslog_pending_age_warning' => '900', 'audit_syslog_dead_letter_warning' => '1', - 'audit_syslog_tls_ca_file' => '', - 'audit_syslog_tls_client_cert' => '', - 'audit_syslog_tls_client_key' => '' -); -$audit_queue_calls = array(); + 'audit_syslog_tls_ca_file' => '', + 'audit_syslog_tls_client_cert' => '', + 'audit_syslog_tls_client_key' => '' +]; +$audit_queue_calls = []; $audit_queue_affected_rows = 0; function read_config_option($name) { @@ -34,25 +34,27 @@ function db_table_exists($table) { return $table === 'audit_syslog_delivery'; } -function db_fetch_row_prepared($sql, $params = array()) { - return array( - 'id' => (int) $params[0], - 'event_uuid' => '32e0a97d-d9e8-4abc-8f41-2bbbc50793ca', +function db_fetch_row_prepared($sql, $params = []) { + return [ + 'id' => (int) $params[0], + 'event_uuid' => '32e0a97d-d9e8-4abc-8f41-2bbbc50793ca', 'request_status' => 'completed' - ); + ]; } -function db_execute_prepared($sql, $params = array()) { +function db_execute_prepared($sql, $params = []) { global $audit_queue_calls; - $audit_queue_calls[] = array('sql' => $sql, 'params' => $params); + $audit_queue_calls[] = ['sql' => $sql, 'params' => $params]; + return true; } function db_execute($sql) { global $audit_queue_calls; - $audit_queue_calls[] = array('sql' => $sql, 'params' => array()); + $audit_queue_calls[] = ['sql' => $sql, 'params' => []]; + return true; } @@ -87,11 +89,11 @@ function audit_queue_assert($condition, $message) { audit_queue_assert($audit_queue_calls[0]['params'][5] === 'pending', 'A valid enabled destination must enqueue in pending state.'); -$config = audit_syslog_config(); -$retry_identity = audit_syslog_delivery_config($config, array( - 'delivery_node_id' => 'original-node', +$config = audit_syslog_config(); +$retry_identity = audit_syslog_delivery_config($config, [ + 'delivery_node_id' => 'original-node', 'delivery_poller_id' => '3' -)); +]); audit_queue_assert($retry_identity['node_id'] === 'original-node' && $retry_identity['poller_id'] === '3', 'Retries must use the node and poller identity captured when the event was queued.'); @@ -99,14 +101,14 @@ function audit_queue_assert($condition, $message) { audit_queue_assert(audit_syslog_retry_delay(2, $config) === 10, 'Retry delay must increase exponentially.'); audit_queue_assert(audit_syslog_retry_delay(10, $config) === 60, 'Retry delay must be capped by the configured maximum.'); -$audit_queue_calls = array(); -$delivery = array('delivery_id' => 9, 'attempts' => 0); -$failure = array( - 'status' => 'failed', - 'permanent' => false, +$audit_queue_calls = []; +$delivery = ['delivery_id' => 9, 'attempts' => 0]; +$failure = [ + 'status' => 'failed', + 'permanent' => false, 'error_code' => 'connection_failed', - 'error' => "receiver unavailable\nwith control text" -); + 'error' => "receiver unavailable\nwith control text" +]; audit_syslog_update_delivery($delivery, $failure, $config); audit_queue_assert($audit_queue_calls[0]['params'][0] === 'retry', 'A transient failure before maximum attempts must enter retry state.'); @@ -116,32 +118,32 @@ function audit_queue_assert($condition, $message) { audit_queue_assert(strpos($audit_queue_calls[0]['params'][5], "\n") === false, 'Stored delivery errors must be bounded to one safe line.'); -$audit_queue_calls = array(); +$audit_queue_calls = []; $delivery['attempts'] = 4; audit_syslog_update_delivery($delivery, $failure, $config); audit_queue_assert($audit_queue_calls[0]['params'][0] === 'dead_letter', 'The configured final failed attempt must enter dead-letter state.'); -$audit_queue_calls = array(); -$permanent = $failure; -$permanent['permanent'] = true; +$audit_queue_calls = []; +$permanent = $failure; +$permanent['permanent'] = true; $permanent['error_code'] = 'udp_message_too_large'; -$delivery['attempts'] = 0; +$delivery['attempts'] = 0; audit_syslog_update_delivery($delivery, $permanent, $config); audit_queue_assert($audit_queue_calls[0]['params'][0] === 'dead_letter', 'A permanent formatting failure must enter dead-letter immediately.'); -$audit_queue_calls = array(); -$success = array('status' => 'sent_unconfirmed', 'error_code' => '', 'error' => '', 'permanent' => false); +$audit_queue_calls = []; +$success = ['status' => 'sent_unconfirmed', 'error_code' => '', 'error' => '', 'permanent' => false]; audit_syslog_update_delivery($delivery, $success, $config); audit_queue_assert(strpos($audit_queue_calls[0]['sql'], "state = 'sent_unconfirmed'") !== false, 'A complete socket write must enter sent_unconfirmed state.'); -$audit_queue_calls = array(); +$audit_queue_calls = []; $audit_queue_affected_rows = 2; -$retried = audit_syslog_retry_dead_letters(array('2', 2, 0, -1, '7')); +$retried = audit_syslog_retry_dead_letters(['2', 2, 0, -1, '7']); audit_queue_assert($retried === 2, 'Manual retry must report the affected row count.'); -audit_queue_assert($audit_queue_calls[0]['params'] === array(2, 7), +audit_queue_assert($audit_queue_calls[0]['params'] === [2, 7], 'Manual retry must accept only unique positive selected delivery IDs.'); audit_queue_assert(strpos($audit_queue_calls[0]['sql'], "WHERE state = 'dead_letter'") !== false, 'Manual retry must never reset a non-dead-letter delivery.');