From 89649494cd04f2093ae6a76f468e4a8bb125c826 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Fri, 11 Sep 2026 02:17:41 -0700 Subject: [PATCH 1/2] refactor: DRY evidence tab page wrapper (#23) * refactor: DRY evidence tab page wrapper * fix: tighten evidence DRY helper and routing tests --- evidence_tab.php | 12 ++----- include/ui_helpers.php | 20 ++++++++++++ tests/test_tab_wrapper.php | 64 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 include/ui_helpers.php create mode 100644 tests/test_tab_wrapper.php diff --git a/evidence_tab.php b/evidence_tab.php index fbf7caf..d0d1ac0 100644 --- a/evidence_tab.php +++ b/evidence_tab.php @@ -28,6 +28,7 @@ include_once('./lib/snmp.php'); include_once('./plugins/evidence/include/functions.php'); include_once('./plugins/evidence/include/arrays.php'); +include_once('./plugins/evidence/include/ui_helpers.php'); set_default_action(); @@ -42,18 +43,12 @@ break; case 'find': - general_header(); - evidence_display_form(); - evidence_find(); - bottom_footer(); + evidence_render_tab_page('evidence_find'); break; default: - general_header(); - evidence_display_form(); - evidence_stats(); - bottom_footer(); + evidence_render_tab_page('evidence_stats'); break; } @@ -260,4 +255,3 @@ function evidence_stats() { print 'Oldest record: ' . $old . '
'; } - diff --git a/include/ui_helpers.php b/include/ui_helpers.php new file mode 100644 index 0000000..e922525 --- /dev/null +++ b/include/ui_helpers.php @@ -0,0 +1,20 @@ + Date: Sat, 12 Sep 2026 16:38:09 -0700 Subject: [PATCH 2/2] hardening: migrate uninstall drop statements to prepared + align test framework with Thold (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * security: migrate uninstall drop statements to prepared * fix: complete uninstall drop coverage and harden tests * fix: harden evidence uninstall drop path with IF EXISTS * test: expand security test coverage for hardening changes Add targeted tests for prepared statement migration, output escaping, auth guard presence, CSRF token validation, redirect safety, and PHP 7.4 compatibility. Tests use source-scan patterns that verify security invariants without requiring the Cacti database. Signed-off-by: Thomas Vincent * fix: escape quotes in Pest regex patterns Signed-off-by: Thomas Vincent * fix(security): escape evidence filter output * test: align Evidence test framework with Thold model Supersedes #25 ("hardening: migrate uninstall drop statements to prepared" by @somethingwithproof) — this branch carries forward that PR's prepared-statement hardening and test coverage, then updates the test/CI plumbing to match the model used by Cacti/plugin_thold, mirroring the same work already applied in PR #29: - Remove composer.json (no composer.lock was present): Pest/PHPUnit no longer come from a vendor tree local to this plugin. - Add tests/.cacti-version, tests/TestCase.php, and tests/bootstrap-unit.php (adapted from thold). bootstrap-unit.php verifies the Cacti checkout in CI matches tests/.cacti-version, requires Cacti's own Composer vendor autoloader, and stubs the Cacti global functions plugin source expects. - Replace tests/bootstrap.php with tests/bootstrap-unit.php and update tests/Pest.php's comment to match. - Add phpunit.xml (none existed before) bootstrapping from tests/bootstrap-unit.php and covering tests/Security, tests/Unit, tests/Integration, and tests/E2E. - Add .github/copilot-instructions.md adapted from thold's for the Evidence plugin. - Add .github/workflows/php-unit-tests.yml modeled on thold's workflow: checks out this plugin plus a pinned Cacti runtime and test toolchain, builds a Docker test image, lints, and runs Pest with coverage. * fix(tests): correct pre-existing test bugs surfaced by new CI run Adding phpunit.xml means these tests actually execute in CI for the first time; two of them had latent bugs that were never caught before: - AuthGuardTest.php checked tests/test_prepared_statements.php (a test helper script) instead of the plugin's real UI entry points (evidence.php, evidence_tab.php), causing a false-positive failure and a risky (zero-assertion) test once no matching line existed. - SetupStructureTest.php regexed setup.php's raw source for `'name' =>` / `'version' =>`, but those keys live in the INFO ini file that setup.php parses at runtime, not in its literal source text. Assert against the parsed INFO file instead, matching PR #29's approach. * chore: normalize line endings to LF and enforce via .gitattributes .github/copilot-instructions.md, .github/workflows/php-unit-tests.yml, tests/.cacti-version, tests/TestCase.php, and tests/bootstrap-unit.php were committed with CRLF line endings. Normalize them to LF to match the rest of the repo, and add .gitattributes (`* text=auto eol=lf`) so this doesn't regress. * fix(tests): address Copilot review feedback on PR #30 - PreparedStatementConsistencyTest.php: drop the non-existent tests/test_prepared_statements.php target and narrow the description/ scope to the setup.php uninstall/drop path it actually verifies, instead of implying (and failing to enforce) coverage of every plugin file's DB calls. - RedirectSafetyTest.php: scan the real redirect entry points (index.php, images/index.php, data/index.php) instead of setup.php, which has no header(Location) call at all. - index.php, images/index.php, data/index.php: follow the header("Location: ...") redirect with exit, so the test above passes for a real reason and the scripts can't fall through if code is later added after the redirect. - Php74CompatibilityTest.php: drop the dangling reference to the removed tests/test_prepared_statements.php. - Convert tests/test_prepared_statements.php (a standalone script never discovered by phpunit.xml or Pest's *Test.php convention, so its assertions never ran in CI) into tests/Security/UninstallPreparedStatementsTest.php, a proper Pest test with the same assertions. * chore: remove php-unit-tests.yml workflow from this PR * docs: merge copilot-instructions.md with main's structure * docs: bring in CHANGELOG.md and updated README.md from main * fix(tests): align phpunit.xml schema path and cacti version check with main * docs: clarify renaming plugin_evidence directory to evidence on install * ci: add plugin-ci-workflow.yml from main and run full test suite; docs: raise PHP floor to 8.2 * ci: run composer install without sudo to avoid root-owned vendor tree breaking Pest cache writes --------- Signed-off-by: Thomas Vincent Co-authored-by: Thomas Vincent --- .gitattributes | 1 + .github/copilot-instructions.md | 448 ++++++++++++++++++ .github/workflows/plugin-ci-workflow.yml | 251 ++++++++++ CHANGELOG.md | 10 + README.md | 24 +- data/index.php | 1 + evidence_tab.php | 8 +- images/index.php | 1 + index.php | 1 + phpunit.xml | 37 ++ setup.php | 26 +- tests/.cacti-version | 1 + tests/E2E/EvidenceFilterXssRegressionTest.php | 17 + tests/Integration/EvidenceTabEscapingTest.php | 23 + tests/Pest.php | 14 + tests/Security/AuthGuardTest.php | 68 +++ tests/Security/OutputEscapingTest.php | 76 +++ tests/Security/Php74CompatibilityTest.php | 112 +++++ .../PreparedStatementConsistencyTest.php | 73 +++ tests/Security/RedirectSafetyTest.php | 51 ++ tests/Security/SetupStructureTest.php | 38 ++ .../UninstallPreparedStatementsTest.php | 44 ++ tests/TestCase.php | 44 ++ tests/Unit/FilterOutputEscapingTest.php | 20 + tests/bootstrap-unit.php | 170 +++++++ 25 files changed, 1524 insertions(+), 35 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/plugin-ci-workflow.yml create mode 100644 CHANGELOG.md create mode 100644 phpunit.xml create mode 100644 tests/.cacti-version create mode 100644 tests/E2E/EvidenceFilterXssRegressionTest.php create mode 100644 tests/Integration/EvidenceTabEscapingTest.php create mode 100644 tests/Pest.php create mode 100644 tests/Security/AuthGuardTest.php create mode 100644 tests/Security/OutputEscapingTest.php create mode 100644 tests/Security/Php74CompatibilityTest.php create mode 100644 tests/Security/PreparedStatementConsistencyTest.php create mode 100644 tests/Security/RedirectSafetyTest.php create mode 100644 tests/Security/SetupStructureTest.php create mode 100644 tests/Security/UninstallPreparedStatementsTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/FilterOutputEscapingTest.php create mode 100644 tests/bootstrap-unit.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..e4c2f6e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,448 @@ +# GitHub Copilot Instructions + +## Project Overview +The `evidence` plugin for Cacti collects and tracks hardware/software evidence for devices: Entity MIB data (serial numbers, part numbers, hardware/firmware/software revisions), MAC addresses, IP addresses, and vendor-specific SNMP data. It stores history of changes and can notify when evidence changes (e.g. a firmware upgrade or serial number swap). + +## Priority Guidelines + +When generating code for this repository: + +1. **Version Compatibility**: This is a Cacti plugin (`evidence`) targeting Cacti 1.2.x +2. **Context Files**: Prioritize patterns and standards defined in this file (`.github/copilot-instructions.md`) +3. **Codebase Patterns**: When context files don't provide specific guidance, scan the codebase for established patterns +4. **Architectural Consistency**: Maintain plugin-based architecture extending Cacti core +5. **Code Quality**: Prioritize security, maintainability, and compatibility in all generated code + +## Technology Stack + +### Core Technologies +- **PHP**: Minimum PHP 8.2; CI verifies compatibility through PHP 8.4. `tests/Security/Php74CompatibilityTest.php` still guards `setup.php` against PHP 8.0+ only syntax (e.g. `str_contains()`, nullsafe operator `?->`, `match`, union types, constructor property promotion) as a legacy regression check. +- **Platform**: Cacti Plugin Architecture (Cacti 1.2.x) +- **Database**: MySQL/MariaDB with InnoDB engine +- **SNMP**: Cacti's SNMP library (`lib/snmp.php`, `cacti_snmp_get()`/`cacti_snmp_walk()`) for device polling + +### Key Dependencies +- Cacti core framework (functions like `api_plugin_*`, `db_*`, `cacti_snmp_*`) +- PHP extensions: `snmp`, `mysqli` +- Optional: `gettext` for internationalization + +## Testing Frameworks +Tests use [Pest](https://pestphp.com/) (which sits on top of PHPUnit). There is no local `composer.json`/`vendor` tree in this plugin repository; the project's CI workflow checks out a pinned Cacti release next to the plugin and runs Pest against Cacti's own Composer-managed vendor tree. +- `tests/bootstrap-unit.php` verifies the checked-out Cacti version matches `tests/.cacti-version`, then stubs the Cacti global functions (`db_*`, `read_config_option`, `__`, `cacti_log`, ...) that plugin source expects to already exist. +- `tests/TestCase.php` is a small `PHPUnit\Framework\TestCase` base class available to any test that prefers a class-based fixture. +- Tests that need a database or a fully running Cacti instance should instead be exercised against a local Cacti installation with the evidence plugin enabled; non-database-dependent logic should be unit tested with Pest. +- Test files are organized by intent under `tests/`: `tests/Security/` (hardening regressions), `tests/Unit/` (isolated helper behavior), `tests/Integration/` (multi-file wiring, still without a live Cacti), and `tests/E2E/` (source-level regression checks against full plugin files). Keep new tests in the directory matching their scope so `phpunit.xml`'s `` picks them up. + +## Project Structure + +``` +evidence/ # Repository root (install to plugins/evidence/ in Cacti) +├── include/ +│ ├── functions.php # Core polling, display and hook logic +│ ├── database.php # Table creation and upgrade logic +│ ├── settings.php # Plugin config_settings hook +│ ├── arrays.php # Configuration arrays (entities, datatypes) +│ └── index.php # Access protection +├── data/ # SQL seed data (enterprise-numbers.sql) and prep scripts +├── images/ # Tab icons and UI images +├── tests/ # Pest/PHPUnit test suite (Security, Unit, Integration, E2E) +├── evidence.php # Main standalone/console page +├── evidence_tab.php # Device tab integration page +├── evidence.js # Client-side JS for device edit page +├── poller_evidence.php # Background poller entry point (CLI) +├── setup.php # Plugin install/uninstall/upgrade hooks +├── INFO # Plugin metadata (name, version, compat) +├── README.md # Feature overview, installation and usage +└── CHANGELOG.md # Version history +``` + +### Plugin Structure +- **Entry Point:** `setup.php` registers all Cacti hooks (`api_plugin_register_hook`) and owns install/uninstall/upgrade (`plugin_evidence_install`, `plugin_evidence_uninstall`, `plugin_evidence_version`, `plugin_evidence_check_config`). +- **Core Logic:** `include/functions.php` contains the hook callbacks (device edit links, header tabs, poller bottom, host edit bottom) and the bulk of the plugin's business logic. +- **Database Schema:** `include/database.php` creates and upgrades the plugin's tables (`plugin_evidence_organization`, `plugin_evidence_snmp_info`, `plugin_evidence_entity`, `plugin_evidence_specific_query`, ...). +- **Settings:** `include/settings.php` registers the "Evidence" settings tab (`plugin_evidence_config_settings`), controlling polling frequency, base time, and history retention. +- **Static Data:** `include/arrays.php` defines the Entity MIB field labels and SNMP data type labels used throughout the UI. +- **Polling Integration:** `poller_evidence.php` is the CLI script invoked from the `poller_bottom` hook (see `include/functions.php`) to gather SNMP evidence data per device. +- **UI:** `evidence.php` and `evidence_tab.php` render the plugin's pages and the per-device Evidence tab. + +### Data Flow +1. **Data Collection:** Cacti's poller triggers `plugin_evidence_poller_bottom` (in `include/functions.php`), which schedules `poller_evidence.php`. +2. **Collection:** `poller_evidence.php` walks the configured SNMP OIDs (Entity MIB, MACs, IPs, vendor-specific data) for each device. +3. **Storage:** Results are written to the `plugin_evidence_*` tables via `include/database.php`; changes are diffed against prior history. +4. **Display:** `evidence.php` / `evidence_tab.php` present current and historical evidence to the user. + +### Installation & Setup +- **Location:** Code resides in `plugins/evidence/` within the Cacti base directory. +- **Activation:** Install and enable via Cacti Plugin Management, then configure under Console -> Settings -> Evidence. + +## Naming Conventions + +### Function Names + +#### Plugin Hook Functions +Functions that integrate with Cacti's plugin system MUST be prefixed with `plugin_evidence_`: + +```php +function plugin_evidence_install() { } +function plugin_evidence_poller_bottom() { } +function plugin_evidence_config_settings() { } +function plugin_evidence_device_remove($device_id) { } +``` + +#### Internal/Display Functions +Other functions MUST be prefixed with `evidence_`: + +```php +function evidence_show_tab() { } +function evidence_show_host_info($data, $host_id) { } +``` + +**IMPORTANT**: Match the existing prefix used by the function you are editing; do not introduce a third naming scheme. + +### Database Tables +All database tables MUST be prefixed with `plugin_evidence_`: + +``` +plugin_evidence_organization +plugin_evidence_snmp_info +plugin_evidence_specific_query +plugin_evidence_entity +plugin_evidence_mac +plugin_evidence_ip +plugin_evidence_vendor_specific +``` + +### Variables and Constants +- Use snake_case for variables: `$host_id`, `$evidence_records`, `$snmp_info` +- Global configuration arrays use descriptive names: `$entities`, `$datatypes` +- Access Cacti configuration via the global `$config` array, e.g. `$config['base_path'] . '/plugins/evidence/...'`; never hardcode `plugins/evidence`'s location + +## Code Style + +### Indentation and Formatting +- **Tabs**: Use tabs (not spaces) for indentation throughout all PHP files +- **Braces**: Opening brace on same line for functions and control structures +- **Spacing**: Space after control structure keywords (`if`, `foreach`, `while`) + +```php +function evidence_example($param) { + if ($param > 0) { + foreach ($items as $item) { + // code here + } + } +} +``` + +### File Headers +ALL PHP files MUST include the standard GPL v2 license header used throughout this repository: + +```php + 'host_id', 'type' => 'int(11)', 'NULL' => false); +$data['columns'][] = array('name' => 'sysdescr', 'type' => 'varchar(255)', 'default' => null); +$data['type'] = 'InnoDB'; +$data['comment'] = 'evidence snmp info'; + +api_plugin_db_table_create('evidence', 'plugin_evidence_snmp_info', $data); +``` + +### Upgrade Handling +Version-gate schema changes in `plugin_evidence_upgrade_database()` (`include/database.php`) using `cacti_version_compare()`, and always update the stored version at the end: + +```php +function plugin_evidence_upgrade_database() { + global $config; + + $info = parse_ini_file($config['base_path'] . '/plugins/evidence/INFO', true); + $info = $info['info']; + $current = $info['version']; + $oldv = db_fetch_cell('SELECT version FROM plugin_config WHERE directory = "evidence"'); + + if (!cacti_version_compare($oldv, $current, '=')) { + if (cacti_version_compare($oldv, '0.3', '<')) { + // create/alter tables here + } + + db_execute_prepared("UPDATE plugin_config + SET version = ?, author = ?, webpage = ? + WHERE directory = 'evidence'", + array($info['version'], $info['author'], $info['homepage'])); + } +} +``` + +### Reference: Cacti Database Functions +Some database functions are provided by the Cacti project itself (see [Cacti DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php)): +- `db_fetch_row($result)` / `db_fetch_assoc($result)`: Fetch a single row from a result set as an associative array. +- `db_query($query)`: Executes a SQL query and returns the result set. +- `db_insert($table, $data)`: Inserts a new record into the specified table. +- `db_update($table, $data, $where)`: Updates records in the specified table based on the given conditions. +- `db_delete($table, $where)`: Deletes records from the specified table based on the given conditions. +- `db_escape_string($string)`: Escapes special characters in a string for use in a SQL query. +- `db_num_rows($result)`: Returns the number of rows in the result set. +- `db_last_insert_id()`: Retrieves the ID of the last inserted record. + +## Internationalization + +### Translation Wrapping +ALL user-facing strings MUST use the `__()` function (or `__esc()` when the value needs to be escaped for output) with the `'evidence'` text domain: + +```php +// CORRECT +print __('Disabled/down device. No actual data', 'evidence') . '
'; +$datatypes = array( + 'info' => __('SNMP info', 'evidence'), + 'entity' => __('Entity MIB', 'evidence'), +); + +// WRONG - Never use plain strings for user-facing text +print 'Disabled/down device'; // Missing translation +``` + +## Plugin Architecture + +### Plugin Hooks +Register all plugin hooks in `plugin_evidence_install()` (`setup.php`): + +```php +function plugin_evidence_install() { + api_plugin_register_hook('evidence', 'device_edit_top_links', 'plugin_evidence_device_edit_top_links', 'include/functions.php'); + api_plugin_register_hook('evidence', 'top_header_tabs', 'evidence_show_tab', 'include/functions.php'); + api_plugin_register_hook('evidence', 'top_graph_header_tabs', 'evidence_show_tab', 'include/functions.php'); + api_plugin_register_hook('evidence', 'host_device_remove', 'plugin_evidence_device_remove', 'include/functions.php'); + api_plugin_register_hook('evidence', 'config_settings', 'plugin_evidence_config_settings', 'include/settings.php'); + api_plugin_register_hook('evidence', 'poller_bottom', 'plugin_evidence_poller_bottom', 'include/functions.php'); + api_plugin_register_hook('evidence', 'host_edit_bottom', 'plugin_evidence_host_edit_bottom', 'include/functions.php'); + + api_plugin_register_realm('evidence', 'evidence.php,evidence_tab.php,', 'Plugin evidence - view', 1); + + plugin_evidence_setup_database(); +} +``` + +### Poller Integration +Background collection runs via `poller_evidence.php`, invoked from `plugin_evidence_poller_bottom()` using `exec_background()` and gated by `plugin_evidence_time_to_run()` (which honors the `evidence_frequency` and `evidence_base_time` settings): + +```php +function plugin_evidence_poller_bottom() { + global $config; + + if (plugin_evidence_time_to_run()) { + include_once($config['library_path'] . '/poller.php'); + $command_string = trim(read_config_option('path_php_binary')); + + if (trim($command_string) == '') { + $command_string = 'php'; + } + + $extra_args = ' -q ' . $config['base_path'] . '/plugins/evidence/poller_evidence.php --id=all'; + + exec_background($command_string, $extra_args); + } +} +``` + +## Configuration Arrays + +Define configuration in `include/arrays.php` and settings in `include/settings.php`: + +```php +// include/arrays.php +$datatypes = array( + 'info' => __('SNMP info', 'evidence'), + 'entity' => __('Entity MIB', 'evidence'), + 'mac' => __('Mac addresses', 'evidence'), + 'ip' => __('IP addresses', 'evidence'), +); + +// include/settings.php - registered via the config_settings hook +$settings['evidence'] = array( + 'evidence_frequency' => array( + 'friendly_name' => 'How often gather data', + 'method' => 'drop_array', + 'array' => array('0' => 'Disabled', '6' => 'Every 6 hours', '24' => 'Every day', '168' => 'Every week'), + 'default' => '24', + ), +); +``` + +## Best Practices + +### 1. Consistency Over Innovation +- Match existing code patterns exactly +- Don't introduce new patterns without documented reason +- Follow established naming conventions without exception + +### 2. Security First +- Always use prepared statements for SQL +- Validate all user input using Cacti's validation functions (`form_input_validate()`, `get_filter_request_var()`, etc.) +- Verify device access via `plugin_evidence_get_allowed_devices()` before displaying host data +- Never trust user input in file operations + +### 3. Cacti Integration +- Use Cacti's API functions (`api_plugin_*`, `db_*`, `cacti_snmp_*`) +- Follow Cacti's plugin architecture requirements +- Respect Cacti's configuration options and settings + +### 4. Internationalization +- Wrap ALL user-facing strings with `__('text', 'evidence')` / `__esc('text', 'evidence')` +- Never use plain strings for labels, messages, or UI text +- Keep text domain consistent (`evidence`) + +### 5. SNMP Resilience +- Suppress and check SNMP calls (`@cacti_snmp_get()`/`@cacti_snmp_walk()`) since not all devices support every OID +- Treat missing/failed SNMP data as "not available" rather than a hard error + +### 6. Performance +- Limit history retention based on the `evidence_records` setting +- Avoid unnecessary polling; respect `evidence_frequency`/`evidence_base_time` + +### 7. Testing & Safety +- Test changes in a safe environment, especially anything touching database interactions or SNMP collection +- Prefer Pest unit tests for non-database-dependent logic; validate database/live-poller behavior against a real Cacti install with the plugin enabled + +## Common Pitfalls to Avoid + +### ❌ NEVER Do This +```php +// Don't concatenate SQL queries +$sql = "SELECT * FROM host WHERE id = $id"; // WRONG + +// Don't use hardcoded strings for UI +print 'Permission issue'; // WRONG + +// Don't use spaces for indentation + if ($condition) { // WRONG (spaces used) + +// Don't skip input validation +$id = $_GET['host_id']; // WRONG +``` + +### ✅ ALWAYS Do This +```php +// Use prepared statements +$host = db_fetch_row_prepared('SELECT * FROM host WHERE id = ?', array($id)); // CORRECT + +// Translate all user-facing strings +print __('Permission issue', 'evidence'); // CORRECT + +// Use tabs for indentation + if ($condition) { // CORRECT (tabs used) + +// Always validate input +$id = get_filter_request_var('host_id'); // CORRECT +``` + +## Version Control + +### Changelog Maintenance +Document all changes in `CHANGELOG.md`: + +```markdown +--- 0.3 --- +* Add generic snmp info +``` + +### Commit Messages +Follow the established pattern from git history: +- Use descriptive commit messages +- Reference issue numbers when applicable +- Group related changes logically + +## References + +- Cacti Plugin Development Guide +- [Cacti API / DB Functions](https://github.com/Cacti/cacti/blob/1.2.x/lib/database.php) +- [Cacti Documentation](https://www.github.com/Cacti/documentation) +- Project README.md for feature descriptions +- CHANGELOG.md for version history diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml new file mode 100644 index 0000000..ea78553 --- /dev/null +++ b/.github/workflows/plugin-ci-workflow.yml @@ -0,0 +1,251 @@ +# +-------------------------------------------------------------------------+ +# | 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. | +# | | +# | This program is distributed in the hope that it will be useful, | +# | but WITHOUT ANY WARRANTY; without even the implied warranty of | +# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +# | GNU General Public License for more details. | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | This code is designed, written, and maintained by the Cacti Group. See | +# | about.php and/or the AUTHORS file for specific developer information. | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ + +name: Plugin Integration Tests + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +env: + CACTI: 1.2.x + +jobs: + integration-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3', '8.4'] + os: [ubuntu-latest] + + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: cactiroot + MYSQL_DATABASE: cacti + MYSQL_USER: cactiuser + MYSQL_PASSWORD: cactiuser + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + name: PHP ${{ matrix.php }} Integration Test on ${{ matrix.os }} + + steps: + - name: Checkout Cacti + uses: actions/checkout@v4 + with: + repository: Cacti/cacti + ref: ${{ env.CACTI }} + path: cacti + + - name: Checkout evidence Plugin + uses: actions/checkout@v4 + with: + path: cacti/plugins/evidence + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp + ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York" + + - name: Check PHP version + run: php -v + + - name: Run apt-get update + run: sudo apt-get update + + - name: Install System Dependencies + run: sudo apt-get install -y snmp snmpd rrdtool fping + + - name: Start SNMPD Agent and Test + run: | + sudo systemctl start snmpd + sudo snmpwalk -c public -v2c -On localhost .1.3.6.1.2.1.1 + + - name: Setup Permissions + run: | + sudo chown -R www-data:runner ${{ github.workspace }}/cacti + sudo find ${{ github.workspace }}/cacti -type d -exec chmod 775 {} \; + sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; + sudo chmod +x ${{ github.workspace }}/cacti/cmd.php + sudo chmod +x ${{ github.workspace }}/cacti/poller.php + + - name: Create MySQL Config + run: | + echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf + cat ~/.my.cnf + + - name: Initialize Cacti Database + env: + MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' + run: | + mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" + mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti + + - name: Validate composer files + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + composer validate --strict || true + fi + + - name: Install Composer Dependencies + env: + COMPOSER_ROOT_VERSION: 1.3.0-dev + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + composer config --no-plugins allow-plugins.pestphp/pest-plugin true + composer require --dev --no-progress --no-interaction "pestphp/pest: ^3" "pestphp/pest-plugin-drift: ^3.0" + rm -f composer.lock + composer install --dev --no-progress + fi + + - name: Create Cacti config.php + run: | + cat ${{ github.workspace }}/cacti/include/config.php.dist | \ + sed -r "s/localhost/127.0.0.1/g" | \ + sed -r "s/'cacti'/'cacti'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php + sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php + + - name: Configure Apache + run: | + cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf + + ServerAdmin webmaster@localhost + DocumentRoot GITHUB_WORKSPACE/cacti + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + EOF + sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf + sudo systemctl restart apache2 + + - name: Install Cacti via CLI + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/install_cacti.php --accept-eula --install --force + + - name: Install evidence Plugin + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/plugin_manage.php --plugin=evidence --install --enable + + - name: Check PHP Syntax for Plugin + run: | + cd ${{ github.workspace }}/cacti/plugins/evidence + if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found!" + exit 1 + fi + + - name: Set expected Cacti version for unit tests + run: echo -n "${{ env.CACTI }}" | sudo tee ${{ github.workspace }}/cacti/plugins/evidence/tests/.cacti-version > /dev/null + + - name: Run Pest Unit Tests + env: + COMPOSER_ROOT_VERSION: 1.3.0-dev + run: | + cd ${{ github.workspace }}/cacti + include/vendor/bin/pest --configuration=plugins/evidence/phpunit.xml \ + --coverage-clover=plugins/evidence/coverage/clover.xml \ + plugins/evidence/tests + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-php${{ matrix.php }} + path: ${{ github.workspace }}/cacti/plugins/evidence/coverage/ + if-no-files-found: warn + + - name: Remove the plugins directory exclusion from the .phpstan.neon + if: ${{ env.CACTI != '1.2.x' }} + run: sed '/plugins/d' -i .phpstan.neon + working-directory: ${{ github.workspace }}/cacti + + - name: Mark composer scripts executable + if: ${{ env.CACTI != '1.2.x' }} + run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* + + - name: Run Linter on base code + if: ${{ env.CACTI != '1.2.x' }} + run: composer run-script lint ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Checking coding standards on base code + if: ${{ env.CACTI != '1.2.x' }} + run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues + if: ${{ env.CACTI != '1.2.x' }} + run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/evidence + working-directory: ${{ github.workspace }}/cacti + + - name: Run Cacti Poller + run: | + cd ${{ github.workspace }}/cacti + sudo php poller.php --poller=1 --force --debug + + if ! grep -q "SYSTEM STATS" log/cacti.log; then + echo "Cacti poller did not finish successfully" + cat log/cacti.log + exit 1 + fi + + - name: View Cacti Logs + if: always() + run: | + if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then + echo "=== Cacti Log ===" + sudo cat ${{ github.workspace }}/cacti/log/cacti.log + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..230ca1b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +--- 0.3 --- +* Add generic snmp info + +--- 0.2 --- +* Better data display + +--- 0.1 --- +* Beginning diff --git a/README.md b/README.md index 2e52648..9c41300 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,28 @@ # plugin_evidence for Cacti -## Try find serial number, version and important information about devices +## Evidence plugin can be useful when you need to find serial number, firmware change, + or problematic firmware. Plugin can collect information about: +- Entity MIB - serial numbers, part numbers, version, firmware, ... +- MAC addresses +- IP addresses +- vendor specific information -A lot of vendors support SNMP Entity MIB (HPE, Synology, Cisco, Mikrotik, Fortinet, ...). -There are information about serial numbers, part numbers, versions, firmware, .. -For few vendors I added vendor specific OIDs (Aruba, Mikrotik, Synology, ..). -It can be useful when you need to find serial number, firmware change, problematic firmware, ... +The plugin also stores history and can notify you when a change occurs. ## Author Petr Macek (petr.macek@kostax.cz) +Based on SNVer plugin 0.6 ## Installation -Copy directory evidence to plugins directory (keep lowercase) +Copy directory plugin_evidence to the plugins directory and rename it to evidence (keep lowercase) Check file permission (Linux/unix - readable for www server) Enable plugin (Console -> Plugin management) Configure plugin (Console -> Settings -> Evidence tab ## How to use? You will see information about serial numbers and version on each supported device -You can use Evidence tab or link on edit device page +You can use the Evidence tab or link on edit device page ## Upgrade Copy and rewrite files @@ -31,9 +34,8 @@ Install and enable new version (Console -> Plugin management) ## Possible Bugs or any ideas? If you find a problem, let me know via github or https://forums.cacti.net - ## Changelog - --- 0.1 - Beginning +See [CHANGELOG.md](CHANGELOG.md) - --- Based on SNVer plugin 0.6 +----------------------------------------------------------------------------- +Copyright (c) 2004-2026 - The Cacti Group, Inc. diff --git a/data/index.php b/data/index.php index b97b997..bfecd3c 100644 --- a/data/index.php +++ b/data/index.php @@ -1,3 +1,4 @@ ' . $scan_date . ''; + '>' . html_escape($scan_date) . ''; } } @@ -137,7 +137,7 @@ function evidence_display_form() { print ''; print ''; - print ''; + print ''; print ''; print ''; print __('Specify data type'); @@ -152,7 +152,7 @@ function evidence_display_form() { print ''; foreach ($entities as $key => $value) { - print ''; + print ''; } print ''; diff --git a/images/index.php b/images/index.php index f6dd345..b34010b 100644 --- a/images/index.php +++ b/images/index.php @@ -1,5 +1,6 @@ diff --git a/index.php b/index.php index b7b31fa..65ffcf5 100644 --- a/index.php +++ b/index.php @@ -1,2 +1,3 @@ + + + + + + + + tests/Security + tests/Unit + tests/Integration + tests/E2E + + + + + + + setup.php + evidence_tab.php + include/database.php + include/functions.php + + + diff --git a/setup.php b/setup.php index c3b671c..4209b1d 100644 --- a/setup.php +++ b/setup.php @@ -42,26 +42,12 @@ function plugin_evidence_install () { function plugin_evidence_uninstall () { - - if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_specific_query'")) > 0 ) { - db_execute("DROP TABLE `plugin_evidence_specific_query`"); - } - - if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_organization'")) > 0 ) { - db_execute("DROP TABLE `plugin_evidence_organization`"); - } - - if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_entity'")) > 0 ) { - db_execute("DROP TABLE `plugin_evidence_entity`"); - } - - if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_mac'")) > 0 ) { - db_execute("DROP TABLE `plugin_evidence_mac`"); - } - - if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_vendor_specific'")) > 0 ) { - db_execute("DROP TABLE `plugin_evidence_vendor_specific`"); - } + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_specific_query`', []); + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_organization`', []); + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_entity`', []); + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_mac`', []); + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_ip`', []); + db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_vendor_specific`', []); } diff --git a/tests/.cacti-version b/tests/.cacti-version new file mode 100644 index 0000000..0848465 --- /dev/null +++ b/tests/.cacti-version @@ -0,0 +1 @@ +1.2.31 diff --git a/tests/E2E/EvidenceFilterXssRegressionTest.php b/tests/E2E/EvidenceFilterXssRegressionTest.php new file mode 100644 index 0000000..d32213d --- /dev/null +++ b/tests/E2E/EvidenceFilterXssRegressionTest.php @@ -0,0 +1,17 @@ +not->toContain('value="' . "' . get_request_var('find_text') . '"); + expect($contents)->toContain('value="' . "' . html_escape(get_request_var('find_text')) . '"); + }); +}); diff --git a/tests/Integration/EvidenceTabEscapingTest.php b/tests/Integration/EvidenceTabEscapingTest.php new file mode 100644 index 0000000..0cc70b9 --- /dev/null +++ b/tests/Integration/EvidenceTabEscapingTest.php @@ -0,0 +1,23 @@ +toContain('html_escape($scan_date)'); + }); + + it('escapes entity keys and values rendered into select options', function () { + $contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php')); + + expect($contents)->toContain('html_escape($key)'); + expect($contents)->toContain('html_escape($value)'); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..c639e23 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,14 @@ +toBeTrue( + "File {$relativeFile} does not include auth.php or global.php" + ); + } + }); + + it('validates numeric IDs from request variables before DB queries', function () { + $uiFiles = array( + 'evidence.php', + 'evidence_tab.php', + ); + + expect($uiFiles)->not->toBeEmpty(); + + foreach ($uiFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + // Check for get_filter_request_var usage for numeric IDs + if (preg_match('/get_request_var\s*\(\s*[\'\"]id[\'\"]/', $contents)) { + // Should use get_filter_request_var for 'id' params + $hasFilter = ( + strpos($contents, 'get_filter_request_var') !== false || + strpos($contents, 'input_validate_input_number') !== false || + strpos($contents, 'form_input_validate') !== false + ); + + expect($hasFilter)->toBeTrue( + "File {$relativeFile} uses get_request_var for IDs without validation" + ); + } + } + }); +}); diff --git a/tests/Security/OutputEscapingTest.php b/tests/Security/OutputEscapingTest.php new file mode 100644 index 0000000..a9b7294 --- /dev/null +++ b/tests/Security/OutputEscapingTest.php @@ -0,0 +1,76 @@ +toBe(0, + "File {$relativeFile} has unescaped variables in HTML attributes" + ); + } + }); + + it('uses html_escape or __esc for user-controlled output', function () { + $uiFiles = array( + 'evidence_tab.php', + ); + + $totalEscapeCalls = 0; + + foreach ($uiFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + $totalEscapeCalls += preg_match_all('/html_escape|__esc\(|htmlspecialchars/', $contents); + } + + // At least some escaping should be present in UI files + expect($totalEscapeCalls)->toBeGreaterThan(0, + 'UI files should contain at least one html_escape/__esc call' + ); + }); + + it('escapes the evidence filter text before rendering it into the HTML value attribute', function () { + $contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php')); + + expect($contents)->toContain( + 'html_escape(get_request_var(\'find_text\'))' + ); + }); +}); diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php new file mode 100644 index 0000000..6b08cc8 --- /dev/null +++ b/tests/Security/Php74CompatibilityTest.php @@ -0,0 +1,112 @@ +toBe(0, "{$f} uses str_contains"); + } + }); + + it('does not use str_starts_with (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\bstr_starts_with\s*\(/', $c))->toBe(0, "{$f} uses str_starts_with"); + } + }); + + it('does not use str_ends_with (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\bstr_ends_with\s*\(/', $c))->toBe(0, "{$f} uses str_ends_with"); + } + }); + + it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\?->/', $c))->toBe(0, "{$f} uses nullsafe operator"); + } + }); + + it('does not use match expression (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + // Avoid false positive on preg_match etc + $c2 = preg_replace('/preg_match|preg_match_all|fnmatch/', '', $c); + expect(preg_match('/\bmatch\s*\(/', $c2))->toBe(0, "{$f} uses match expression"); + } + }); + + it('does not use union type declarations (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + // Match function params/return with union types like string|false + $hits = preg_match_all('/function\s+\w+\s*\([^)]*\w+\s*\|\s*\w+/', $c); + expect($hits)->toBe(0, "{$f} uses union types in function signatures"); + } + }); + + it('does not use constructor property promotion (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/function\s+__construct\s*\([^)]*\b(public|private|protected|readonly)\s/', $c))->toBe(0, + "{$f} uses constructor promotion" + ); + } + }); + + it('uses array() not short syntax for new arrays', function () use ($files) { + // This is a style preference for 1.2.x consistency, not a hard requirement + // Just verify no mixed styles in the same file + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + + $hasArrayFunc = preg_match('/\barray\s*\(/', $c); + $hasShortArray = preg_match('/=\s*\[/', $c); + + // Flag files that mix both styles + if ($hasArrayFunc && $hasShortArray) { + // Allow mixed if the file existed before our changes + // This is informational, not a hard fail + } + } + + expect(true)->toBeTrue(); + }); +}); diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php new file mode 100644 index 0000000..4f89be0 --- /dev/null +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -0,0 +1,73 @@ +toBe(0, "File {$relativeFile} contains raw DB calls"); + } + }); + + it('uses parameterized placeholders not string interpolation in SQL', function () { + $targetFiles = array( + 'setup.php', + ); + + foreach ($targetFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + $lines = explode("\n", $contents); + $interpolatedSql = 0; + + foreach ($lines as $num => $line) { + $trimmed = ltrim($line); + if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0) continue; + + // Detect _prepared calls with $ interpolation instead of ? placeholders + if (preg_match('/_prepared\s*\(/', $line) && preg_match('/\$[a-zA-Z_]/', $line)) { + // Allow array($var) param binding but flag "WHERE id = $var" + if (preg_match('/(?:SELECT|INSERT|UPDATE|DELETE|WHERE|SET|FROM|JOIN).*\$/', $line)) { + $interpolatedSql++; + } + } + } + + // This is a heuristic; some false positives expected for complex queries + expect($interpolatedSql)->toBeLessThanOrEqual(2, + "File {$relativeFile} may have SQL interpolation in prepared calls" + ); + } + }); +}); diff --git a/tests/Security/RedirectSafetyTest.php b/tests/Security/RedirectSafetyTest.php new file mode 100644 index 0000000..787f389 --- /dev/null +++ b/tests/Security/RedirectSafetyTest.php @@ -0,0 +1,51 @@ +toBe(0, + "File {$relativeFile} has header(Location) without exit/die" + ); + } + }); +}); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php new file mode 100644 index 0000000..6079c5f --- /dev/null +++ b/tests/Security/SetupStructureTest.php @@ -0,0 +1,38 @@ +toContain('function plugin_evidence_install'); + }); + + it('defines plugin_evidence_version function', function () use ($source) { + expect($source)->toContain('function plugin_evidence_version'); + }); + + it('defines plugin_evidence_uninstall function', function () use ($source) { + expect($source)->toContain('function plugin_evidence_uninstall'); + }); + + it('declares a plugin name in INFO', function () use ($info) { + expect($info)->toHaveKey('name'); + }); + + it('declares a plugin version in INFO', function () use ($info) { + expect($info)->toHaveKey('version'); + }); + + it('registers hooks in install function', function () use ($source) { + expect($source)->toContain('api_plugin_register_hook'); + }); +}); diff --git a/tests/Security/UninstallPreparedStatementsTest.php b/tests/Security/UninstallPreparedStatementsTest.php new file mode 100644 index 0000000..13e7d6f --- /dev/null +++ b/tests/Security/UninstallPreparedStatementsTest.php @@ -0,0 +1,44 @@ +toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_specific_query`', []);"); + }); + + it('drops plugin_evidence_organization with a prepared statement', function () use ($setup) { + expect($setup)->toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_organization`', []);"); + }); + + it('drops plugin_evidence_entity with a prepared statement', function () use ($setup) { + expect($setup)->toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_entity`', []);"); + }); + + it('drops plugin_evidence_mac with a prepared statement', function () use ($setup) { + expect($setup)->toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_mac`', []);"); + }); + + it('drops plugin_evidence_ip with a prepared statement', function () use ($setup) { + expect($setup)->toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_ip`', []);"); + }); + + it('drops plugin_evidence_vendor_specific with a prepared statement', function () use ($setup) { + expect($setup)->toContain("db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_vendor_specific`', []);"); + }); + + it('does not rely on SHOW TABLES LIKE pre-checks', function () use ($setup) { + expect($setup)->not->toMatch('/SHOW TABLES LIKE\s+[\'"]plugin_evidence_/i'); + }); + + it('does not leave raw db_execute drop statements', function () use ($setup) { + expect($setup)->not->toMatch('/db_execute\s*\(\s*["\']DROP TABLE\s+`plugin_evidence_/i'); + }); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..4395961 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,44 @@ +alert(1)'; + + $escaped = html_escape($payload); + + expect($escaped)->not->toContain('