mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-27 23:27:44 +09:00
* chore: manage front-end JavaScript libraries with npm and Dependabot Move all third-party front-end JavaScript to npm so Dependabot tracks it, and drop the committed copies in favour of a build. package.json pins jquery, jquery-ui, jstree (3.3.12 -> 3.3.17), billboard.js, pace-js, tablesorter and jquery-validation; build/sync-js.mjs copies their dist files into include/js/ and runs on npm install via postinstall. pace-js carries a one-line local fix (a stray regex alternation that made it match the empty string) applied through patch-package (patches/pace-js+1.2.4.patch); the fix should be sent upstream so the patch can be dropped. Only Cacti's own themed jquery-ui/jstree CSS stays committed. An npm ecosystem is added to dependabot.yml for develop. The release pipeline and any workflow that serves Cacti pages must run 'npm ci' (which builds via postinstall) before use, since the generated files are no longer in the tree. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * hardening: remove vendored development artifacts * feat(release): attach the built artifact to the release The workflow already builds an attested, checksummed archive carrying the Composer dependencies, then discards it after thirty days. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: fail the publish step when the bundle is empty An unmatched glob would otherwise reach gh as a literal path. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: treat the maintenance lock notice as expected log output The line records the lock working, so a second maintenance run overlapping in CI failed unrelated pull requests. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: consolidate release dependency changelog * ci: build managed JavaScript in release artifacts * test: cover managed JavaScript build end to end * deps: complete phpseclib 4 migration * deps: update patch-package to audited release * ci: satisfy workflow shell policy * chore: source DOMPurify from npm and update it to 3.4.14 Add dompurify to the npm-managed set (was committed at 3.4.7) and bump to 3.4.14. DOMPurify is the client-side XSS sanitizer used on tree labels, device tooltips and plugin messages; keeping it current with upstream bypass fixes matters more than for the UI libraries. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: preserve release changelog ordering * test: preserve DOMPurify source map in npm build * chore: source htmx, d3, big.js, lzjs and screenfull from npm Add the remaining unmodified third-party libraries to the npm-managed build: htmx.org 2.0.6, d3 7.8.2, big.js 6.2.2, lzjs 1.3.1 and screenfull 3.3.3. Each was verified byte-identical (lzjs differs only in a JSDoc comment) to the copies being removed. htmx in particular moves from a hand-tracked SHA pin to npm with lockfile integrity. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: cover phpseclib 4 integration * ci: build managed assets before htmx tests --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
121 lines
4.9 KiB
PHP
121 lines
4.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 any later version. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
use Composer\InstalledVersions;
|
|
use phpseclib4\Crypt\Rijndael;
|
|
use phpseclib4\Crypt\RSA;
|
|
|
|
beforeEach(function () {
|
|
global $config, $database_default, $database_hostname, $database_port, $database_sessions, $encryption;
|
|
|
|
$this->phpseclibGlobals = [
|
|
'config' => $config,
|
|
'database_default' => $database_default,
|
|
'database_hostname' => $database_hostname,
|
|
'database_port' => $database_port,
|
|
'database_sessions' => $database_sessions,
|
|
'encryption' => $encryption ?? false,
|
|
];
|
|
});
|
|
|
|
afterEach(function () {
|
|
global $config, $database_default, $database_hostname, $database_port, $database_sessions, $encryption;
|
|
|
|
$config = $this->phpseclibGlobals['config'];
|
|
$database_default = $this->phpseclibGlobals['database_default'];
|
|
$database_hostname = $this->phpseclibGlobals['database_hostname'];
|
|
$database_port = $this->phpseclibGlobals['database_port'];
|
|
$database_sessions = $this->phpseclibGlobals['database_sessions'];
|
|
$encryption = $this->phpseclibGlobals['encryption'];
|
|
});
|
|
|
|
test('Composer loads phpseclib 4 without the retired version 3 namespace', function () {
|
|
expect(InstalledVersions::getPrettyVersion('phpseclib/phpseclib'))->toStartWith('4.')
|
|
->and(class_exists(RSA::class))->toBeTrue()
|
|
->and(class_exists(Rijndael::class))->toBeTrue()
|
|
->and(class_exists('phpseclib3\\Crypt\\RSA'))->toBeFalse();
|
|
});
|
|
|
|
test('Cacti generates and persists a phpseclib 4 RSA key pair', function () {
|
|
global $config, $database_default, $database_hostname, $database_port, $database_sessions;
|
|
|
|
$database_hostname = 'phpseclib4-integration';
|
|
$database_port = 0;
|
|
$database_default = 'cacti';
|
|
$connection = new PDO('sqlite::memory:');
|
|
$connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$connection->exec('CREATE TABLE settings (name TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
|
$database_sessions["$database_hostname:$database_port:$database_default"] = $connection;
|
|
unset($config[OPTIONS_CLI]['rsa_public_key']);
|
|
|
|
rsa_check_keypair();
|
|
|
|
$settings = $connection->query("SELECT name, value FROM settings WHERE name LIKE 'rsa_%'")
|
|
->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
|
|
expect($settings)->toHaveKeys(['rsa_public_key', 'rsa_private_key', 'rsa_fingerprint']);
|
|
|
|
$private = RSA::loadPrivateKey($settings['rsa_private_key']);
|
|
$public = RSA::loadPublicKey($settings['rsa_public_key']);
|
|
|
|
expect($private->getPublicKey()->getFingerprint())->toBe($settings['rsa_fingerprint'])
|
|
->and($public->getFingerprint())->toBe($settings['rsa_fingerprint']);
|
|
});
|
|
|
|
test('Cacti encrypted proxy packets round trip through phpseclib 4 keys', function () {
|
|
global $encryption;
|
|
|
|
$privateKey = RSA::createKey(2048);
|
|
$publicKey = $privateKey->getPublicKey();
|
|
$encryption = true;
|
|
$plaintext = random_bytes(512);
|
|
$packet = encrypt($plaintext, (string) $publicKey);
|
|
|
|
expect($packet)->not->toBe($plaintext)
|
|
->and(rrdtool_proxy_decrypt($packet, (string) $privateKey))->toBe($plaintext);
|
|
|
|
if (extension_loaded('openssl')) {
|
|
expect(openssl_pkey_get_private((string) $privateKey))->not->toBeFalse()
|
|
->and(openssl_pkey_get_public((string) $publicKey))->not->toBeFalse();
|
|
}
|
|
});
|
|
|
|
test('Cacti accepts the legacy oversized symmetric-key packet contract', function () {
|
|
$privateKey = RSA::createKey(4096);
|
|
$legacyKey = random_bytes(192);
|
|
$plaintext = random_bytes(256);
|
|
$aes = new Rijndael('cbc');
|
|
$aes->setKey(substr($legacyKey, 0, 32));
|
|
$aes->setIV(str_repeat("\0", 16));
|
|
|
|
$encryptedKey = base64_encode($privateKey->getPublicKey()->encrypt($legacyKey));
|
|
$packet = str_pad(dechex(strlen($encryptedKey)), 3, '0', STR_PAD_LEFT)
|
|
. $encryptedKey
|
|
. base64_encode($aes->encrypt($plaintext));
|
|
|
|
expect(rrdtool_proxy_decrypt($packet, (string) $privateKey))->toBe($plaintext);
|
|
});
|
|
|
|
test('Cacti fails closed for invalid and unrelated phpseclib 4 private keys', function () {
|
|
global $encryption;
|
|
|
|
$privateKey = RSA::createKey(2048);
|
|
$otherKey = RSA::createKey(2048);
|
|
$encryption = true;
|
|
$packet = encrypt('protected payload', (string) $privateKey->getPublicKey());
|
|
|
|
expect(rrdtool_proxy_decrypt($packet, (string) $otherKey))->toBeFalse()
|
|
->and(rrdtool_proxy_decrypt($packet, 'not a private key'))->toBeFalse();
|
|
});
|