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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

--- develop ---

* security: Replace the confirmation pages' inline `onClick='cactiReturnTo()'` Cancel buttons with the CSP-safe `cactiReturnTo` class so the pages no longer trip Cacti's Content-Security-Policy script-src-attr directive
* security: Move the WMI Accounts and Queries filter rows selects' inline onChange handlers into the ready block so the pages no longer trip Cacti's Content-Security-Policy script-src-attr directive
* dev: Measure CI coverage with xdebug instead of pcov so the plugin's own sources are instrumented (pcov auto-scopes to the Composer root and skipped cacti/plugins/, leaving the patch-coverage gate with nothing to measure)
* dev: Enforce patch coverage of changed lines in CI and remove the inert COMPOSER_ROOT_VERSION env from the Pest step
* security: Add a version-safe CSP nonce (`plugin_wmi_csp_nonce()`) to every inline `<script>` tag so pages stay compatible with Cacti's Content-Security-Policy nonce enforcement, while falling back cleanly on older Cacti releases that lack the `CactiSecureHeaders` class
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ system (see Transports below).
This plugin is NOT fully functional. It should not be used on production
systems. Use at your own risk!

## Cacti compatibility

If you are running a version of Cacti below 1.2.31, please add the function
below to the `applySkin()` function in `include/layout.js` to enable the Cancel
buttons on forms to work:

```js
$(document).off('click.cactiReturnTo', '.cactiReturnTo')
.on('click.cactiReturnTo', '.cactiReturnTo', function(event) {
event.preventDefault();
cactiReturnTo($(this).attr('data-url'));
});
```

## Installation

To install the plugin, simply copy the plugin_wmi directory to Cacti's plugins
Expand Down
1 change: 1 addition & 0 deletions locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Cacti \n"
"Report-Msgid-Bugs-To: developers@cacti.net\n"
"POT-Creation-Date: 2026-10-07 16:15-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers <developers@cacti.net>>\n"
"Language-Team: Cacti Developers <developers@cacti.net>\n"
Expand Down
37 changes: 37 additions & 0 deletions tests/Security/CspFilterHandlersTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Regression coverage for the CSP migration of the WMI Accounts and Queries
* filter "rows" selects. Both entry points are excluded from the patch-
* coverage gate, so without this source-level assertion CI would not catch a
* reintroduced inline `onChange='applyFilter()'` handler (which trips Cacti's
* Content-Security-Policy script-src-attr directive) or a lost ready-block
* binding.
*/

$filter_files = array('wmi_accounts.php', 'wmi_queries.php');

foreach ($filter_files as $file) {
describe("csp filter handler migration in {$file}", function () use ($file) {
$source = plugin_test_read_source($file);

it('has no inline onChange handler on the rows select', function () use ($source, $file) {
expect($source)->not->toMatch('/<select\s+id=[\'"]rows[\'"][^>]*\sonChange\s*=/i', "{$file} still has an inline onChange handler on the rows select");
});

it('has no inline applyFilter onChange handler anywhere', function () use ($source, $file) {
expect($source)->not->toMatch('/onChange\s*=\s*[\'"]applyFilter\(/i', "{$file} still contains an inline applyFilter onChange handler");
});

it('binds the rows select change event from the ready block', function () use ($source, $file) {
expect($source)->toMatch('/\$\(\s*[\'"]#rows[\'"]\s*\)\s*\.change\s*\(/', "{$file} does not bind #rows change() from the ready block");
});
});
}
8 changes: 6 additions & 2 deletions wmi_accounts.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function actions_accounts() {
<input type='hidden' name='action' value='actions'>
<input type='hidden' name='selected_items' value='" . (isset($account_array) ? serialize($account_array) : '') . "'>
<input type='hidden' name='drp_action' value='" . get_request_var('drp_action') . "'>
<input type='button' value='" . __('Cancel', 'wmi') . "' onClick='cactiReturnTo()'>
<input class='cactiReturnTo' type='button' value='" . __('Cancel', 'wmi') . "'>
$save_html
</td>
</tr>";
Expand Down Expand Up @@ -316,7 +316,7 @@ function account_filter() {
<?php print __('Accounts', 'wmi'); ?>
</td>
<td>
<select id='rows' onChange='applyFilter()'>
<select id='rows'>
<option value='-1'<?php print (get_request_var('rows') == '-1' ? ' selected>' : '>') . __('Default', 'wmi'); ?></option>
<?php
if (sizeof($item_rows)) {
Expand Down Expand Up @@ -357,6 +357,10 @@ function clearFilter() {
}

$(function() {
$('#rows').change(function() {
applyFilter();
});
Comment thread
TheWitness marked this conversation as resolved.

$('#refresh').click(function() {
applyFilter();
});
Expand Down
10 changes: 7 additions & 3 deletions wmi_queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ function actions_queries() {

if (!isset($query_array)) {
print "<tr><td class='odd'><span class='textError'>" . __('You must select at least one WMI Query.', 'wmi') . '</span></td></tr>';
$save_html = "<input type='button' value='" . __('Return', 'wmi') . "' onClick='cactiReturnTo()'>";
$save_html = "<input class='cactiReturnTo' type='button' value='" . __('Return', 'wmi') . "'>";
} else {
$save_html = "<input type='button' value='" . __('Cancel', 'wmi') . "' onClick='cactiReturnTo()'>&nbsp;<input type='submit' value='" . __('Continue', 'wmi') . "' title='" . __('Delete WMI Query', 'wmi') . "'>";
$save_html = "<input class='cactiReturnTo' type='button' value='" . __('Cancel', 'wmi') . "'>&nbsp;<input type='submit' value='" . __('Continue', 'wmi') . "' title='" . __('Delete WMI Query', 'wmi') . "'>";
}

print "<tr>
Expand Down Expand Up @@ -339,7 +339,7 @@ function query_filter() {
<?php print __('Queries', 'wmi'); ?>
</td>
<td>
<select id='rows' onChange='applyFilter()'>
<select id='rows'>
<option value='-1'<?php print (get_request_var('rows') == '-1' ? ' selected>' : '>') . __('Default', 'wmi'); ?></option>
<?php
if (sizeof($item_rows)) {
Expand Down Expand Up @@ -383,6 +383,10 @@ function clearFilter() {
}

$(function() {
$('#rows').change(function() {
applyFilter();
});
Comment thread
TheWitness marked this conversation as resolved.

$('#refresh').click(function() {
applyFilter();
});
Expand Down
Loading