Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
.phpunit.result.cache
vendor
10 changes: 10 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ permissions:
contents: read

jobs:
runtime-coverage:
name: Docker tests and runtime coverage
runs-on: ubuntu-latest
steps:
- name: Checkout plugin
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Run tests and enforce 100% runtime coverage in PHP 8.3
run: tests/docker/run-tests.sh

lint:
name: PHP Lint (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
Expand Down
40 changes: 30 additions & 10 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ on:
- develop

jobs:
production-path:
name: Production scanner path
runs-on: ubuntu-latest
steps:
- name: Checkout Cacti develop
uses: actions/checkout@v7
with:
repository: Cacti/cacti
ref: develop
path: cacti

- name: Checkout mactrack plugin
uses: actions/checkout@v7
with:
path: cacti/plugins/mactrack

- name: Run installed Cacti, MariaDB, SNMP, scanner, and poller
env:
CACTI_SOURCE: ${{ github.workspace }}/cacti
MACTRACK_SOURCE: ${{ github.workspace }}/cacti/plugins/mactrack
run: cacti/plugins/mactrack/tests/e2e/run-mactrack-e2e.sh

integration-test:
runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -107,16 +129,14 @@ jobs:
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
run: |
mysql --defaults-file="$HOME/.my.cnf" -e 'CREATE DATABASE IF NOT EXISTS cacti;'
mysql --defaults-file="$HOME/.my.cnf" -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';"
mysql --defaults-file="$HOME/.my.cnf" -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';"
mysql --defaults-file="$HOME/.my.cnf" -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';"
mysql --defaults-file="$HOME/.my.cnf" -e "FLUSH PRIVILEGES;"
mysql --defaults-file="$HOME/.my.cnf" cacti < "${{ github.workspace }}/cacti/cacti.sql"
mysql --defaults-file="$HOME/.my.cnf" -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti

- name: Validate composer files
run: |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

--- develop ---

* issue#343: Fail closed when Composer dependencies are missing and test the production scanner path against live SNMP
* compat: Replace vendored Net_DNS2 with Composer-managed mikepultz/netdns2 1.5, retaining the established resolver API
* issue: Harden device-type vendor filtering against SQL injection
* issue: Validate aggregated MAC bulk-action IDs and use prepared deletion SQL
Expand Down
6 changes: 6 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"description": "Cacti Mactrack plugin test tooling",
"type": "cacti-plugin",
"license": "GPL-2.0-or-later",
"autoload": {
"psr-4": {
"Cacti\\Mactrack\\": "src/"
}
},
"require-dev": {
"pestphp/pest": "^1.23",
"phpunit/phpunit": "^9.6",
Expand All @@ -14,6 +19,7 @@
"test:integration": "pest tests/Pest/Integration",
"test:e2e:static": "pest tests/Pest/E2E",
"test:coverage": "pest --coverage",
"test:coverage:runtime": "pest --configuration=phpunit.runtime.xml --coverage --min=100",
"analyse": "psalm --no-progress"
},
"config": {
Expand Down
13 changes: 10 additions & 3 deletions mactrack_resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@

include('../../include/cli_check.php');
include_once($config['base_path'] . '/plugins/mactrack/lib/mactrack_functions.php');
require_once $config['base_path'] . '/plugins/mactrack/vendor/autoload.php';
require_once $config['base_path'] . '/plugins/mactrack/src/Runtime/DependencyBootstrap.php';

if (!class_exists('Net_DNS2_Resolver')) {
fwrite(STDERR, "Mactrack DNS dependency is unavailable. Run composer install --no-dev in the plugin directory.\n");
$dependenciesLoaded = \Cacti\Mactrack\Runtime\DependencyBootstrap::load(
$config['base_path'] . '/plugins/mactrack/vendor/autoload.php',
['Net_DNS2_Resolver'],
static function (string $message): void {
fwrite(STDERR, $message . "\n");
}
);

if (!$dependenciesLoaded) {
exit(1);
}

Expand Down
13 changes: 13 additions & 0 deletions phpunit.runtime.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/Pest.php" colors="true">
<testsuites>
<testsuite name="Runtime dependency contract">
<directory suffix="Test.php">tests/Pest/Unit/Runtime</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src/Runtime</directory>
</include>
</coverage>
</phpunit>
19 changes: 9 additions & 10 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,17 @@ function plugin_mactrack_version() {
function plugin_mactrack_check_config() {
global $config;

$autoload = $config['base_path'] . '/plugins/mactrack/vendor/autoload.php';
require_once $config['base_path'] . '/plugins/mactrack/src/Runtime/DependencyBootstrap.php';

if (!is_file($autoload)) {
cacti_log('ERROR: Mactrack requires Composer dependencies. Run composer install --no-dev in plugins/mactrack before enabling the plugin.', false, 'MACTRACK');

return false;
}

require_once $autoload;
$loaded = \Cacti\Mactrack\Runtime\DependencyBootstrap::load(
$config['base_path'] . '/plugins/mactrack/vendor/autoload.php',
['Net_DNS2_Resolver'],
static function (string $message): void {
cacti_log('ERROR: ' . $message, false, 'MACTRACK');
}
);

if (!class_exists('Net_DNS2_Resolver')) {
cacti_log('ERROR: Mactrack DNS dependency is unavailable. Run composer install --no-dev in plugins/mactrack before enabling the plugin.', false, 'MACTRACK');
if (!$loaded) {

return false;
}
Expand Down
46 changes: 46 additions & 0 deletions src/Runtime/DependencyBootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Cacti\Mactrack\Runtime;

use Throwable;

final class DependencyBootstrap {
/**
* @param list<class-string> $requiredClasses
* @param null|callable(string):void $reporter
*/
public static function load(
string $autoloadPath,
array $requiredClasses = ['Net_DNS2_Resolver'],
?callable $reporter = null
): bool {
$reporter ??= static function (string $_message): void {
};

if (!is_file($autoloadPath)) {
$reporter('Mactrack requires Composer dependencies. Run composer install --no-dev in the plugin directory.');

return false;
}
Comment on lines +22 to +26

try {
require_once $autoloadPath;
} catch (Throwable $error) {
$reporter('Mactrack could not load Composer dependencies: ' . $error->getMessage());

return false;
}

foreach ($requiredClasses as $requiredClass) {
if (!class_exists($requiredClass)) {
$reporter('Mactrack Composer dependency is unavailable: ' . $requiredClass . '. Run composer install --no-dev in the plugin directory.');

return false;
}
}

return true;
}
}
75 changes: 75 additions & 0 deletions tests/Pest/Unit/Runtime/DependencyBootstrapTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

use Cacti\Mactrack\Runtime\DependencyBootstrap;

beforeAll(function (): void {
require_once __DIR__ . '/../../../../src/Runtime/DependencyBootstrap.php';
});

it('reports a missing Composer autoloader', function (): void {
$messages = [];

$loaded = DependencyBootstrap::load(
__DIR__ . '/fixtures/does-not-exist.php',
['MactrackMissingDependency'],
static function (string $message) use (&$messages): void {
$messages[] = $message;
}
);

expect($loaded)->toBeFalse()
->and($messages)->toBe([
'Mactrack requires Composer dependencies. Run composer install --no-dev in the plugin directory.',
]);
});

it('fails quietly when no reporter is supplied', function (): void {
expect(DependencyBootstrap::load(
__DIR__ . '/fixtures/does-not-exist.php',
['MactrackMissingDependency']
))->toBeFalse();
});

it('reports an autoloader failure', function (): void {
$messages = [];

$loaded = DependencyBootstrap::load(
__DIR__ . '/fixtures/throwing-autoload.php',
['MactrackThrowingDependency'],
static function (string $message) use (&$messages): void {
$messages[] = $message;
}
);

expect($loaded)->toBeFalse()
->and($messages)->toBe(['Mactrack could not load Composer dependencies: fixture failure']);
});

it('reports an unavailable required class', function (): void {
$messages = [];

$loaded = DependencyBootstrap::load(
__DIR__ . '/fixtures/empty-autoload.php',
['MactrackUnavailableDependency'],
static function (string $message) use (&$messages): void {
$messages[] = $message;
}
);

expect($loaded)->toBeFalse()
->and($messages)->toBe([
'Mactrack Composer dependency is unavailable: MactrackUnavailableDependency. Run composer install --no-dev in the plugin directory.',
]);
});

it('loads every required dependency', function (): void {
$loaded = DependencyBootstrap::load(
__DIR__ . '/fixtures/working-autoload.php',
['MactrackFixtureDependency']
);

expect($loaded)->toBeTrue()
->and(class_exists('MactrackFixtureDependency'))->toBeTrue();
});
3 changes: 3 additions & 0 deletions tests/Pest/Unit/Runtime/fixtures/empty-autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

declare(strict_types=1);
5 changes: 5 additions & 0 deletions tests/Pest/Unit/Runtime/fixtures/throwing-autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

declare(strict_types=1);

throw new RuntimeException('fixture failure');
6 changes: 6 additions & 0 deletions tests/Pest/Unit/Runtime/fixtures/working-autoload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

declare(strict_types=1);

final class MactrackFixtureDependency {
}
13 changes: 6 additions & 7 deletions tests/Unit/test_device_type_sql_safety.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,19 @@

$resolverSource = file_get_contents(__DIR__ . '/../../mactrack_resolver.php');

if ($resolverSource === false ||
strpos($resolverSource, 'require_once $config[\'base_path\'] . \'/plugins/mactrack/vendor/autoload.php\'') === false ||
strpos($resolverSource, "class_exists('Net_DNS2_Resolver')") === false) {
if ($resolverSource === false ||
strpos($resolverSource, '\\Cacti\\Mactrack\\Runtime\\DependencyBootstrap::load(') === false ||
strpos($resolverSource, 'if (!$dependenciesLoaded)') === false) {
fwrite(STDERR, "DNS resolver must load and verify the Composer-managed NetDNS2 dependency\n");
exit(1);
}

$setupSource = file_get_contents(__DIR__ . '/../../setup.php');

if ($setupSource === false ||
if ($setupSource === false ||
strpos($setupSource, '/plugins/mactrack/vendor/autoload.php') === false ||
strpos($setupSource, 'require_once $autoload;') === false ||
strpos($setupSource, "class_exists('Net_DNS2_Resolver')") === false ||
strpos($setupSource, 'return false;') === false) {
strpos($setupSource, '\\Cacti\\Mactrack\\Runtime\\DependencyBootstrap::load(') === false ||
strpos($setupSource, 'if (!$loaded)') === false) {
fwrite(STDERR, "Mactrack configuration checks must block enablement without a usable Composer dependency\n");
exit(1);
}
Expand Down
20 changes: 20 additions & 0 deletions tests/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM composer:2 AS composer

FROM php:8.3-cli

RUN apt-get update \
&& apt-get install -y --no-install-recommends $PHPIZE_DEPS git unzip \
&& pecl install pcov \
&& docker-php-ext-enable pcov \
&& rm -rf /var/lib/apt/lists/* /tmp/pear

COPY --from=composer /usr/bin/composer /usr/local/bin/composer

WORKDIR /app

COPY composer.json composer.lock ./
RUN composer install --no-interaction --no-progress --prefer-dist

COPY . .

CMD ["sh", "-ec", "php vendor/bin/pest && php vendor/bin/pest --configuration=phpunit.runtime.xml --coverage --min=100 && composer validate --strict"]
8 changes: 8 additions & 0 deletions tests/docker/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

SOURCE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
IMAGE="mactrack-tests:php83"

docker build --file "$SOURCE_DIR/tests/docker/Dockerfile" --tag "$IMAGE" "$SOURCE_DIR"
docker run --rm "$IMAGE"
8 changes: 6 additions & 2 deletions tests/e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
FROM php:8.2-apache
FROM composer:2 AS composer

FROM php:8.3-apache

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
&& apt-get install -y --no-install-recommends default-mysql-client libfreetype6-dev libgmp-dev libicu-dev libjpeg-dev libldap2-dev libonig-dev libpng-dev libsnmp-dev libxml2-dev rrdtool snmp snmpd \
&& apt-get install -y --no-install-recommends default-mysql-client fping libfreetype6-dev libgmp-dev libicu-dev libjpeg-dev libldap2-dev libonig-dev libpng-dev libsnmp-dev libxml2-dev rrdtool snmp snmpd unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j"$(nproc)" gd gmp intl ldap mbstring mysqli pcntl pdo_mysql snmp sockets xml \
&& rm -rf /var/lib/apt/lists/*

RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
&& { echo 'date.timezone = UTC'; echo 'memory_limit = 512M'; echo 'max_execution_time = 60'; } > /usr/local/etc/php/conf.d/mactrack-e2e.ini

COPY --from=composer /usr/bin/composer /usr/local/bin/composer
Loading