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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# +-------------------------------------------------------------------------+
# | Copyright (C) 2004-2026 The Cacti Group |
# +-------------------------------------------------------------------------+
# | Cacti: The Complete RRDtool-based Graphing Solution |
# +-------------------------------------------------------------------------+
# | http://www.cacti.net/ |
# +-------------------------------------------------------------------------+
#
# CodeQL has no PHP analysis, so this covers the plugin JavaScript only. Nothing in
# CI performs taint analysis on the PHP: plugin-ci-workflow.yml runs a parser check
# and an install smoke test, neither of which tracks a value from request to sink.

name: "CodeQL"

on:
push:
branches: [main, develop]
paths-ignore:
- "**/*.md"
pull_request:
branches: [main, develop]
paths-ignore:
- "**/*.md"
schedule:
- cron: "30 1 * * 1"
workflow_dispatch:

concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["javascript-typescript"]
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Initialize CodeQL
uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3
with:
category: "/language:${{ matrix.language }}"
44 changes: 44 additions & 0 deletions tests/Integration/OutputEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Source rules, not behavioural tests. They match the text of the web |
| entry points, so they catch an escaping call being deleted and nothing |
| else: a new unescaped sink written with different markup passes. The |
| pages chdir() to the Cacti root and include auth.php, so asserting on |
| rendered output needs a live install, which tests/e2e would cover. |
| |
| Behavioural coverage lives in tests/Unit, which loads plugin code. |
+-------------------------------------------------------------------------+
*/

$root = dirname(__DIR__, 2);

dataset('escaped_outputs', [
['monitor_controller.php', "html_escape(get_request_var('downhosts'))"],
['monitor_controller.php', "html_escape(get_request_var('mute'))"],
['monitor_controller.php', "html_escape(get_request_var('tree'))"],
['monitor_controller.php', "html_escape(get_request_var('site'))"],
Comment on lines +18 to +22
['monitor_controller.php', "html_escape(get_request_var('template'))"],
['monitor_controller.php', "html_escape(get_request_var('size'))"],
['monitor_controller.php', "html_escape(get_request_var('trim'))"],
['monitor_render.php', "rawurlencode(get_request_var('rfilter'))"],
]);

it('escapes request values before printing them', function (string $file, string $pattern) use ($root) {
expect(file_get_contents($root . '/' . $file))->toContain($pattern);
})->with('escaped_outputs');

dataset('raw_reuse', [
['monitor_controller.php', "get_request_var('tree') . '\"></td>'"],
['monitor_controller.php', "get_request_var('site') . '\"></td>'"],
['monitor_controller.php', "get_request_var('template') . '\"></td>'"],
['monitor_controller.php', "get_request_var('size') . '\"></td>'"],
['monitor_controller.php', "get_request_var('trim') . '\"></td>'"],
['monitor_render.php', "monitor.php?rfilter=' . get_request_var('rfilter')"],
]);

it('never concatenates a raw request value into markup', function (string $file, string $pattern) use ($root) {
expect(file_get_contents($root . '/' . $file))->not->toContain($pattern);
})->with('raw_reuse');
5 changes: 5 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@
* bootstrap attribute (tests/bootstrap-unit.php), which requires Cacti's
* own Composer-managed vendor tree checked out by the CI workflow.
*/

uses()->beforeEach(function () {
$GLOBALS['__test_db_calls'] = array();
test_set_request(array());
})->in(__DIR__);
59 changes: 59 additions & 0 deletions tests/Unit/DbFunctionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Exercises db_functions.php through the Cacti stubs in bootstrap-unit.php.|
| These load and run plugin code, unlike the source-matching rules in |
| tests/Integration. |
+-------------------------------------------------------------------------+
*/

require_once dirname(__DIR__, 2) . '/db_functions.php';

it('selects the breached threshold clause for status 2', function () {
test_set_request(['status' => '2']);

expect(getTholdWhere())->toContain('td.thold_alert != 0 OR td.bl_alert > 0');
});

it('selects the triggered clause for any other status', function () {
test_set_request(['status' => '0']);

expect(getTholdWhere())->toContain('thold_fail_count >= td.thold_fail_trigger');
});

it('builds an IN clause from a concatenated id list', function () {
$where = '';
renderGroupConcat($where, ' AND ', 'h.id', '4,9,17');

expect($where)->toBe('(h.id IN(4,9,17) )');
});

it('joins onto an existing clause rather than replacing it', function () {
$where = 'h.disabled = ""';
renderGroupConcat($where, ' AND ', 'h.id', '4');

expect($where)->toStartWith('h.disabled = ""')->and($where)->toContain(' AND ');
});

it('adds nothing when the id list is empty', function () {
$where = '';
renderGroupConcat($where, ' AND ', 'h.id', '');

expect($where)->toBe('');
});

it('collapses the doubled commas GROUP_CONCAT can produce', function () {
$where = '';
renderGroupConcat($where, ' AND ', 'h.id', ',,4,,9,,');

expect($where)->toBe('(h.id IN(4,9) )');
});

it('appends the optional suffix', function () {
$where = '';
renderGroupConcat($where, ' AND ', 'h.id', '4', 'OR h.id IS NULL');

expect($where)->toContain('OR h.id IS NULL');
});
19 changes: 16 additions & 3 deletions tests/bootstrap-unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
);

$GLOBALS['__test_db_calls'] = array();
$GLOBALS['__test_request'] = array();

if (!function_exists('db_execute')) {
function db_execute($sql) {
Expand Down Expand Up @@ -220,27 +221,39 @@ function raise_message($id, $text = '', $level = 0) {

if (!function_exists('get_request_var')) {
function get_request_var($name) {
return '';
return $GLOBALS['__test_request'][$name] ?? '';
}
}

if (!function_exists('get_nfilter_request_var')) {
function get_nfilter_request_var($name) {
return '';
return get_request_var($name);
}
}

if (!function_exists('get_filter_request_var')) {
function get_filter_request_var($name) {
return '';
return get_request_var($name);
}
}

if (!function_exists('set_request_var')) {
function set_request_var($name, $value) {
$GLOBALS['__test_request'][$name] = $value;
}
}

/**
* Set the stubbed request vars a test needs, resetting the rest.
*
* @param array $vars
*
* @return void
*/
function test_set_request(array $vars): void {
$GLOBALS['__test_request'] = $vars;
}

if (!function_exists('validateRequestVars')) {
function validateRequestVars($section, $filter = array()) {
}
Expand Down
Loading