Files
cacti/tests/integration/SqlInterpolationRatchetIntegrationTest.php
Thomas VincentandTheWitness 37fe6eab9d Gate new unparameterised SQL value interpolation (#7260)
* test(security): add SQL interpolation classifier and ratchet baseline

Tokenizes each db_fetch/db_execute call and classifies its SQL argument as
prepared, static, qstr-escaped, opaque, identifier or value. The value bucket
is the migration target: a raw variable interpolated into value position. The
baseline records the current per-file value count so CI can block new ones.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* ci(security): gate new SQL value interpolation in quality workflow

Runs the interpolation audit in --check mode so a pull request that adds an
unparameterised value interpolation fails until it uses db_*_prepared(). Pins
setup-php to a commit SHA.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* refactor(security): split prebuilt SQL fragments from scalar interpolation

Cacti names query fragments consistently (sql_where, sql_join, ...). A variable
with such a name is a SQL snippet, not a bindable scalar, so it now lands in a
separate fragment category instead of inflating the value count. Extracts
scan_source() and a library-only include guard so the classifier can be unit
tested. Baseline value count drops from 373 to 300.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test(security): cover the SQL interpolation classifier

Unit tests exercise each category (prepared, static, qstr, value, identifier,
fragment, opaque) against snippets, including IF EXISTS identifiers and mixed
scalar/fragment statements. An integration test runs the CLI against the tree
and asserts the committed baseline stays in sync.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(tooling): gate chdir to CLI runs and align checkout version

The audit is require'd by the unit tests in library mode; running chdir() at
load time leaked a working-directory change into other tests. Gate it behind
the library sentinel. Also aligns the ratchet job's checkout to the version the
rest of the workflow uses.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* style(tooling): use PHP 8 string helpers and short array syntax in audit

Switches the classifier to str_starts_with/str_ends_with/str_contains and short
array and list syntax, matching the idiom the rest of the codebase uses. Output
is unchanged.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix installer failure handling

* style(lib): align installer assignment per cs-fixer

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
2026-07-12 20:02:16 -04:00

81 lines
2.9 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| 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. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
// End-to-end checks for the SQL interpolation ratchet: run the actual CLI
// against the real tree the way CI does, then confirm the committed baseline
// still matches that tree.
function audit_run(string $args): array {
$root = dirname(__DIR__, 2);
$cmd = escapeshellarg(PHP_BINARY) . ' '
. escapeshellarg($root . '/tests/tools/sql_interpolation_audit.php')
. ' ' . $args . ' 2>&1';
$output = array();
$code = 0;
exec('cd ' . escapeshellarg($root) . ' && ' . $cmd, $output, $code);
return array('code' => $code, 'out' => implode("\n", $output));
}
test('ratchet passes on the current tree', function () {
$result = audit_run('--check');
expect($result['code'])->toBe(0)
->and($result['out'])->toContain('ratchet passed');
});
test('json output is well-formed and totals every category', function () {
$result = audit_run('--json');
expect($result['code'])->toBe(0);
$data = json_decode($result['out'], true);
expect($data)->toBeArray()
->and($data)->toHaveKey('totals')
->and($data)->toHaveKey('files')
->and($data['totals'])->toHaveKeys(
array('prepared', 'static', 'qstr', 'fragment', 'opaque', 'identifier', 'value')
);
});
test('committed baseline matches the current tree', function () {
$root = dirname(__DIR__, 2);
$baseline = json_decode(file_get_contents($root . '/tests/tools/sql_interpolation_baseline.json'), true);
expect($baseline)->toBeArray();
$data = json_decode(audit_run('--json')['out'], true);
$current = array();
foreach ($data['files'] as $path => $counts) {
if (($counts['value'] ?? 0) > 0) {
$current[$path] = $counts['value'];
}
}
ksort($baseline);
ksort($current);
// Drift here means someone changed SQL call sites without refreshing the
// baseline: php tests/tools/sql_interpolation_audit.php --write-baseline
expect($current)->toBe($baseline);
});
test('every baseline path still exists', function () {
$root = dirname(__DIR__, 2);
$baseline = json_decode(file_get_contents($root . '/tests/tools/sql_interpolation_baseline.json'), true);
foreach (array_keys($baseline) as $path) {
expect(file_exists($root . '/' . $path))->toBeTrue("missing baseline path: $path");
}
});