From 39bcf06a59cde37d9c4cad2d18f207af9320d893 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Tue, 15 Sep 2026 15:34:51 -0400 Subject: [PATCH] ci: add CodeQL workflow and adopt DbFunctionsTest/OutputEscapingTest from #229 Cherry-picked from Cacti/plugin_monitor#229 (somethingwithproof:hardening/testing-and-workflows) and adapted to this repo's existing Pest setup: - .github/workflows/codeql.yml: added as-is (JS/TS CodeQL scanning). - tests/Integration/OutputEscapingTest.php: added as-is, references tests/Unit instead of tests/unit in its docblock. - tests/Unit/DbFunctionsTest.php: added, placed under tests/Unit (PascalCase) instead of tests/unit to match this repo's layout. - tests/bootstrap-unit.php: get_request_var()/get_nfilter_request_var()/get_filter_request_var()/set_request_var() now read/write a stubbed \['__test_request'] store, and a new test_set_request() helper lets tests control request vars, mirroring #229's tests/bootstrap.php. - tests/Pest.php: added a beforeEach() hook resetting __test_db_calls and the request var store between tests. --- .github/workflows/codeql.yml | 60 ++++++++++++++++++++++++ tests/Integration/OutputEscapingTest.php | 44 +++++++++++++++++ tests/Pest.php | 5 ++ tests/Unit/DbFunctionsTest.php | 59 +++++++++++++++++++++++ tests/bootstrap-unit.php | 19 ++++++-- 5 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 tests/Integration/OutputEscapingTest.php create mode 100644 tests/Unit/DbFunctionsTest.php diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8bc3f3a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -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 }}" diff --git a/tests/Integration/OutputEscapingTest.php b/tests/Integration/OutputEscapingTest.php new file mode 100644 index 0000000..2878082 --- /dev/null +++ b/tests/Integration/OutputEscapingTest.php @@ -0,0 +1,44 @@ +toContain($pattern); +})->with('escaped_outputs'); + +dataset('raw_reuse', [ + ['monitor_controller.php', "get_request_var('tree') . '\">'"], + ['monitor_controller.php', "get_request_var('site') . '\">'"], + ['monitor_controller.php', "get_request_var('template') . '\">'"], + ['monitor_controller.php', "get_request_var('size') . '\">'"], + ['monitor_controller.php', "get_request_var('trim') . '\">'"], + ['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'); diff --git a/tests/Pest.php b/tests/Pest.php index 4c958b4..4cadc94 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -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__); diff --git a/tests/Unit/DbFunctionsTest.php b/tests/Unit/DbFunctionsTest.php new file mode 100644 index 0000000..3f69cd0 --- /dev/null +++ b/tests/Unit/DbFunctionsTest.php @@ -0,0 +1,59 @@ + '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'); +}); diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php index 9f440e9..c599982 100644 --- a/tests/bootstrap-unit.php +++ b/tests/bootstrap-unit.php @@ -81,6 +81,7 @@ ); $GLOBALS['__test_db_calls'] = array(); +$GLOBALS['__test_request'] = array(); if (!function_exists('db_execute')) { function db_execute($sql) { @@ -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()) { }