Skip to content

feat: PowerShell/CIM WMI transport (Windows backend) - #17

Draft
somethingwithproof wants to merge 14 commits into
Cacti:developfrom
somethingwithproof:feature/powershell-cim-transport
Draft

feat: PowerShell/CIM WMI transport (Windows backend)#17
somethingwithproof wants to merge 14 commits into
Cacti:developfrom
somethingwithproof:feature/powershell-cim-transport

Conversation

@somethingwithproof

Copy link
Copy Markdown
Member

Draft. Phase 2 of the CIM backend (issue #15), stacked on #16. Merge order: #13, #14, #16, then this. Diff includes the earlier PRs until they land; I will rebase clean.

Adds PowerShellCim_Transport, a second Wmi_Transport backend that runs Get-CimInstance through pwsh/powershell.exe. It works on a Windows Cacti server and, for a remote target, over WinRM/CIM against modern Windows hosts the legacy Linux wmic client can no longer authenticate to (NTLM deprecation, DCOM hardening).

Credential handling (the reason this is its own PR)

  • The password and every device-supplied value (host, namespace, WQL) pass through the child process environment and a fixed script on stdin, run via proc_open with an argv array. No shell is involved, the password never appears on the command line or in ps, and nothing is interpolated into the script, so there is no PowerShell injection.
  • The script builds a PSCredential from $env:WMI_PASS. Env is process-scoped; the caveat versus a plaintext file is documented, and it is strictly better than the legacy argv exposure.

Output contract

The script emits the class name, the separator-joined column header, then one separator-joined row per instance, so Linux_WMI parses it exactly like wmic output. Namespace \ is translated to / for CIM.

Tests

tests/WmiPowerShellTransportTest.php (standalone, injectable runner, no live PowerShell): asserts the password is off argv and out of the script, that it travels via env, the class/header/row parsing, and the empty-result and non-zero-exit error paths. php tests/WmiPowerShellTransportTest.php exits 0.

Not in this PR (Phases 3-4)

Server-OS auto-detection, a per-account transport/auth-level UI, and docs. Tracked in #15.

Automated fixes:
- XSS: escape request variables in HTML value attributes
- SQLi: convert string-concat queries to prepared statements
- Deserialization: add allowed_classes=>false
- Temp files: replace rand() with tempnam()

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
- Change Dependabot ecosystem from npm to composer (PHP-only repo)
- Remove PHP from CodeQL paths-ignore so security PRs get analysis
- Remove committed .omc session artifacts, add .omc/ to .gitignore

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Linux_WMI::clean() escaped the username, password, binary and command but left
the device hostname only trimmed and the query namespace untouched, so
getcommand() interpolated them raw into the wmic command line that exec() runs
on the Cacti server. A device-supplied hostname such as
  127.0.0.1; touch /tmp/pwned #
therefore ran a command on the poller.

Escape the hostname and namespace with cacti_escapeshellarg, and on Windows
strip the cmd.exe metacharacters (" & | ^ < > ( ) %) that cmd.exe interprets
despite quoting. A standalone regression test in tests/ verifies both.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Escape account/query/tool output on render (html_escape/__esc), bind the
remaining WMI queries as prepared statements, and drop wmi_accounts.php and
wmi_tools.php from the Template Editor auth augment so credential management
and the live query tool stay behind the WMI Management realm.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The default separator (|+|) contains pipe characters, so the unquoted
--delimiter=|+| made exec() split the command into a shell pipeline (exit
127, no data). Quote it in getcommand() while keeping the property raw for
the explode() in fetch(). issue#5

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Type the properties and method signatures, replace var/sizeof, and move the
wmic command construction behind a Wmi_Transport interface (Wmic_Shell_Transport
by default, injectable) so a PowerShell/CIM backend can be added without
touching the parser. Behaviour and the shell-escaping guards are unchanged; the
existing injection test still passes.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Short array syntax, single-quoted literals, comment style and indentation to
match the checked-in .php-cs-fixer.php. Mechanical only; no logic change.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Second Wmi_Transport backend: runs Get-CimInstance through pwsh/powershell.exe
so the plugin works on a Windows Cacti server and against modern Windows hosts
the legacy wmic client cannot authenticate to. The credential and every
device-supplied value pass through the child environment and a fixed stdin
script (run via proc_open with an argv array), never the command line, so no
shell or process listing sees the password and nothing is interpolated into the
script. issue#15

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
A Windows Cacti server has no Linux wmic binary, so default to the PowerShell
transport there and to wmic elsewhere; an explicit transport still wins. Update
the injection test to select the wmic transport explicitly for its win32 case.
issue#15

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Analyse linux_wmi.php at level 6 with signature stubs for the two Cacti core
functions it calls. Add the array value-type docblocks the level requires. The
loosely typed page files stay out of scope for now.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Augment the existing README with the two WMI transports (wmic on Linux,
PowerShell/CIM on Windows or for hardened hosts), their requirements, the access
model, and the credential storage posture. Preserve the existing changelog and
authors. issue#15

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are critical runtime blockers (a mis-terminated nowdoc causing a PHP parse error and a wrong include path), plus the Windows execution path still bypasses the new PowerShell transport and likely fails due to encoded password handling.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces a second WMI backend that runs Get-CimInstance via PowerShell/CIM (intended as the default on Windows Cacti servers) while keeping the existing wmic-based transport for Unix servers, along with related security hardening, formatting/modernization, and standalone tests.

Changes:

  • Add PowerShellCim_Transport and a Wmi_Transport seam so Linux_WMI can delegate execution to either wmic or PowerShell/CIM.
  • Update multiple UI/scripts to match PHP 8-era style and improve output escaping / SQL binding (stacked security/modernization work).
  • Add standalone tests for command-injection regression and PowerShell transport behavior, plus PHPStan configuration/stubs.
File summaries
File Description
wmi_tools.php Formatting/escaping updates; retains a Windows COM execution path for the query tool.
wmi_script.php CLI script style/DB query modernizations.
wmi_queries.php UI formatting/escaping and prepared statements.
wmi_accounts.php UI formatting/escaping and prepared statements.
tests/WmiPowerShellTransportTest.php Standalone tests validating PowerShell transport argv/env/script contract and parsing behavior.
tests/WmiCommandInjectionTest.php Regression test ensuring hostname/namespace are neutralized in command building.
tests/cacti-stubs.php PHPStan stubs for Cacti core functions.
setup.php Setup/uninstall and role augmentation adjustments plus formatting.
script/wmi-script.php Script-server entrypoint refactor and prepared statement usage.
README.md Documentation update describing transport selection and security posture.
poller_wmi.php Prepared statements + formatting/modernization in the poller.
phpstan.neon Adds PHPStan config for the plugin code + stubs.
linux_wmi.php Adds transport seam + PowerShell/CIM transport + typed properties and refactors execution path.
functions.php Minor refactors + prepared statements; still contains a Windows COM execution branch in data collection.
CHANGELOG.md Summarizes the stacked security/behavior changes.
.gitignore Ignores .omc/.
Review details
  • Files reviewed: 15/16 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread functions.php
Comment on lines 387 to +392
} else {
// Windows version
$wmi = new COM('WbemScripting.SWwebLocator');
$wmic = $wmi->ConnectServer($host, $namespace, $username, $password);
$wmi = new COM('WbemScripting.SWwebLocator');
$wmic = $wmi->ConnectServer($host, $namespace, $username, $password);
$wmic->Security_->ImpersonationLevel = 3;
$data = $wmic->ExecQuery($command);
$data = $wmic->ExecQuery($command);
Comment thread linux_wmi.php
$vals = foreach ($p in $props) { [string]$item.$p }
Write-Output ($vals -join $sep)
}
PS;
Comment thread script/wmi-script.php
@@ -37,29 +38,31 @@ function wmi_script($hostname, $host_id, $wmiquery, $cmd = '', $arg1 = '', $arg2

include_once($config['base_path'] . '/plugins/wmi/linux-wmi.php');
Comment thread wmi_tools.php
// Windows version
$wmi = new COM('WbemScripting.SWwebLocator');
$wmic = $wmi->ConnectServer($host, $namespace, $username, $password);
$wmi = new COM('WbemScripting.SWwebLocator');
Comment thread setup.php
Comment on lines 44 to 48
function plugin_wmi_uninstall() {
global $config;

return true;

include_once($config['base_path'] . '/lib/api_data_source.php');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants