From 8762cdef5b377496e4a302c1cd213e671f459f97 Mon Sep 17 00:00:00 2001 From: Martijn Wagena Date: Thu, 27 Aug 2026 23:11:49 +0200 Subject: [PATCH 1/3] Align README with public distribution The release branch already documents the packagist.org install; develop would otherwise reintroduce the private repository instructions on the next merge forward. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2eec458..58fb49e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Kite PHP SDK -Framework-agnostic PHP client for [Kite](https://gitlab.concept7.nl/workflow/kite) monitoring. Collects project metadata (PHP version, database, frontend tooling, installed packages) and reports it to the Kite API. +Framework-agnostic PHP client for [Kite](https://kite-monitor.com) monitoring. Collects project metadata (PHP version, database, frontend tooling, installed packages) and reports it to the Kite API. ## Requirements @@ -15,7 +15,7 @@ composer require concept7/kite-php-sdk ## Configuration -Set the `KITE_TOKEN` environment variable with the token generated from the [Kite Dashboard](https://kite-monitor.concept7.dev/). +Set the `KITE_TOKEN` environment variable with the token generated from the [Kite Dashboard](https://kite-monitor.com/). ```php use Concept7\Kite\Kite; @@ -26,7 +26,7 @@ $config = new KiteConfig( ); ``` -The API base URL defaults to `https://kite-monitor.concept7.dev`. Override it for development: +The API base URL defaults to `https://kite-monitor.com`. Override it for development: ```php $config = new KiteConfig( From ef9d3f25528906f8e2bf1fd45c2bca3776726242 Mon Sep 17 00:00:00 2001 From: Martijn Wagena Date: Thu, 27 Aug 2026 23:23:45 +0200 Subject: [PATCH 2/3] Rewrite README against the actual API Keeps develop in step with main so the corrected documentation is not reverted on the next merge forward. --- README.md | 153 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 58fb49e..cea9172 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Kite PHP SDK -Framework-agnostic PHP client for [Kite](https://kite-monitor.com) monitoring. Collects project metadata (PHP version, database, frontend tooling, installed packages) and reports it to the Kite API. +Framework-agnostic PHP client for [Kite](https://kite-monitor.com) monitoring. Collects project metadata (PHP, Node and database versions, installed packages), reports it to the Kite API, and scans those packages for known security advisories. + +This is the core SDK. For a framework integration, use [`concept7/laravel-kite`](https://github.com/concept7/laravel-kite) or [`concept7/wordpress-kite`](https://github.com/concept7/wordpress-kite) instead — both build on this package. ## Requirements @@ -35,53 +37,55 @@ $config = new KiteConfig( ); ``` +A config without a token is invalid, and `report()` and `checkAdvisories()` both throw an `Exception` rather than sending anything. + ## Usage -### Basic +### Reporting ```php -$result = Kite::make($config)->report(); +$report = Kite::make($config)->report(); + +echo $report->message; ``` -`Kite::make()` automatically registers the following default actions: +`report()` returns a `ProjectReportDto`. It runs every registered action through a pipeline, drops the records whose `value` came back empty, and posts the result. + +`Kite::make()` registers these actions by default: - `GetPhpVersionAction` — PHP version +- `GetNodeVersionAction` — Node version - `GetMysqlVersionAction` — MySQL/MariaDB version -- `GetTailwindVersionAction` — Tailwind CSS version (from `package-lock.json`) -- `GetKiteVersionAction` — Kite SDK version ### Adding extra actions -Add project-specific actions alongside the defaults: +Add your own actions alongside the defaults — see [Writing a custom action](#writing-a-custom-action) for what one looks like: ```php -use Concept7\Kite\Actions\GetComposerPackageVersionAction; - -$result = Kite::make($config) - ->addAction(new GetComposerPackageVersionAction( - metaKey: 'statamic_version', - packages: ['statamic/cms'], - )) +$report = Kite::make($config) + ->addAction(new GetRedisVersionAction) ->report(); ``` Or multiple at once: ```php -$result = Kite::make($config) +$report = Kite::make($config) ->addActions([ - new GetComposerPackageVersionAction('statamic_version', ['statamic/cms']), - new GetComposerPackageVersionAction('livewire_version', ['livewire/livewire']), + new GetRedisVersionAction, + new GetStatamicVersionAction, ]) ->report(); ``` ### Replacing default actions -Use `setActions()` to completely override the default actions: +Use `setActions()` to completely override the defaults: ```php -$result = Kite::make($config) +use Concept7\Kite\Actions\GetPhpVersionAction; + +$report = Kite::make($config) ->setActions([ new GetPhpVersionAction, new MyCustomAction, @@ -91,10 +95,12 @@ $result = Kite::make($config) ### Project info collector -Add a `ProjectInfoCollectorInterface` implementation to send additional project information: +Packages and project details are only sent when a `ProjectInfoCollectorInterface` implementation is registered. Its `collect()` returns an array that becomes the `project_info` payload; a `packages` key in it is what gets scanned and filtered. ```php use Concept7\Kite\Contracts\ProjectInfoCollectorInterface; +use Concept7\Kite\Support\ComposerDependencies; +use Concept7\Kite\Support\NpmDependencies; class MyProjectInfoCollector implements ProjectInfoCollectorInterface { @@ -103,62 +109,119 @@ class MyProjectInfoCollector implements ProjectInfoCollectorInterface return [ 'hostname' => gethostname(), 'environment' => getenv('APP_ENV') ?: 'production', + 'packages' => array_merge( + ComposerDependencies::all(), + NpmDependencies::installed(), + ), ]; } } -$result = Kite::make($config) +$report = Kite::make($config) ->projectInfoCollector(new MyProjectInfoCollector) ->report(); ``` ### Package collection -The SDK provides helpers to collect installed packages with ecosystem tagging: +Helpers to collect installed packages with ecosystem tagging: ```php -use Concept7\Kite\Support\ComposerDependencies; -use Concept7\Kite\Support\NpmDependencies; - // Direct Composer dependencies (from composer.json require) ComposerDependencies::direct(); -// All Composer dependencies (including transitive) +// All Composer dependencies, including transitive ones ComposerDependencies::all(); -// Installed npm packages (from package-lock.json) +// All npm packages, from package-lock.json NpmDependencies::installed(); ``` -Each returns an array of `['name' => '...', 'version' => '...', 'ecosystem' => Ecosystem::Composer|Npm]`. +Each accepts an optional base path and defaults to `getcwd()`. Every entry has this shape: + +```php +[ + 'name' => 'vendor/package', + 'version' => '1.2.3', + 'ecosystem' => Ecosystem::Composer, + 'is_direct' => true, + 'required_by' => ['vendor/other-package'], +] +``` + +### Package filtering + +Before sending, `report()` fetches the project's config from the Kite API. Unless the project is set to share all packages, the reported list is filtered down to the packages Kite is configured to monitor. The advisory scan always runs against the *full* list, so vulnerable transitive packages still surface. + +If that config call fails, nothing is filtered out of the scan and no packages are reported. + +## Security advisories + +`report()` scans the collected packages and includes the findings in the payload. A failing scan is swallowed — it never blocks the report. -### Full fluent chain +To scan without sending a full report — for a scheduled re-scan between reports, say: ```php -$result = Kite::make($config) +Kite::make($config) ->projectInfoCollector(new MyProjectInfoCollector) - ->addAction(new GetComposerPackageVersionAction('statamic_version', ['statamic/cms'])) - ->report(); + ->checkAdvisories(); +``` + +`checkAdvisories()` returns early when no collector is registered or the collector reports no packages. + +The scanners can also be used directly: + +```php +use Concept7\Kite\Support\ComposerAdvisories; +use Concept7\Kite\Support\NpmAdvisories; + +ComposerAdvisories::scan($packages); +NpmAdvisories::scan($packages); ``` +Each picks out the packages belonging to its own ecosystem and ignores the rest: + +| Scanner | Ecosystem | Source | +|---|---|---| +| `ComposerAdvisories` | `composer` | [Packagist](https://packagist.org) advisories API, version-matched with `composer/semver` | +| `NpmAdvisories` | `npm` | [OSV.dev](https://osv.dev) batch query API | + +Both return advisories in the same shape: + +```php +[ + 'advisory_id' => 'PKSA-xxxx-xxxx-xxxx', + 'package' => 'vendor/package', + 'version' => '1.2.3', + 'ecosystem' => 'composer', + 'title' => 'Advisory title', + 'link' => 'https://...', + 'cve' => 'CVE-2026-0000', + 'severity' => 'high', + 'reported_at' => '2026-08-27', +] +``` + +`severity` is normalised through the `Severity` enum (`critical`, `high`, `medium`, `low`); OSV's `moderate` maps to `medium`, and anything unrecognised becomes `null`. + ## Writing a custom action -Implement `ActionInterface` to create your own action. Each action receives a `Collection` of metadata records and passes it along via `$next`: +Implement `ActionInterface`. Each action receives a `Collection` of metadata records and passes it along via `$next`: ```php use Closure; use Concept7\Kite\Contracts\ActionInterface; -use Illuminate\Process\Factory as ProcessFactory; use Illuminate\Support\Collection; +use Symfony\Component\Process\Process; class GetRedisVersionAction implements ActionInterface { public function handle(Collection $data, Closure $next): Collection { - $process = new ProcessFactory; - $result = $process->run('redis-server --version'); + $process = new Process(['redis-server', '--version']); + $process->run(); - if ($result->successful() && preg_match('/v=(\d+\.\d+\.\d+)/', $result->output(), $matches)) { + if ($process->isSuccessful() && preg_match('/v=(\d+\.\d+\.\d+)/', $process->getOutput(), $matches)) { $data->push([ 'key' => 'redis_version', 'value' => $matches[1], @@ -170,18 +233,15 @@ class GetRedisVersionAction implements ActionInterface } ``` -Each record is an array with `key` and `value`. Records with a `null` or empty `value` are automatically filtered out before the report is sent. +Each record is an array with `key` and `value`. Records with a `null` or empty `value` are filtered out before the report is sent, so an action that cannot determine its value can simply push nothing — or push `null` and let it be dropped. ## Built-in actions | Action | Meta key | Source | |---|---|---| | `GetPhpVersionAction` | `php_version` | `phpversion()` | -| `GetMysqlVersionAction` | `database_version` | `mysql --version` | -| `GetTailwindVersionAction` | `tailwind_version` | `package-lock.json` | -| `GetKiteVersionAction` | `kite_version` | `composer.lock` | -| `GetComposerPackageVersionAction` | configurable | `composer.lock` | -| `GetNodePackageVersionAction` | configurable | `package-lock.json` | +| `GetNodeVersionAction` | `node_version` | `node --version`, falling back to `.nvmrc` | +| `GetMysqlVersionAction` | `database_version` | `mysql --version`, prefixed `mysql_` or `mariadb_` | ## Ecosystems @@ -193,6 +253,13 @@ The `Ecosystem` enum tags packages by source: | `Ecosystem::Npm` | `npm` | | `Ecosystem::Wordpress` | `wordpress` | +## Testing + +```bash +composer test +composer lint +``` + ## License MIT From 2bb678eed58bca1cec934d00b12abab0960871b7 Mon Sep 17 00:00:00 2001 From: Jurn Spijksma Date: Tue, 8 Sep 2026 16:29:42 +0200 Subject: [PATCH 3/3] Handle disabled shell_exec when detecting Node --- src/Actions/GetNodeVersionAction.php | 9 +++++++++ tests/GetNodeVersionActionTest.php | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Actions/GetNodeVersionAction.php b/src/Actions/GetNodeVersionAction.php index 7ef197b..7a9a93f 100644 --- a/src/Actions/GetNodeVersionAction.php +++ b/src/Actions/GetNodeVersionAction.php @@ -49,6 +49,10 @@ private function resolveVersion(): ?string protected function resolveVersionFromBinary(): ?string { + if (! $this->canUseShellExec()) { + return null; + } + $output = shell_exec('node --version 2>/dev/null'); if (blank($output)) { @@ -59,4 +63,9 @@ protected function resolveVersionFromBinary(): ?string return preg_match('/^\d+\.\d+/', $version) ? $version : null; } + + protected function canUseShellExec(): bool + { + return function_exists('shell_exec'); + } } diff --git a/tests/GetNodeVersionActionTest.php b/tests/GetNodeVersionActionTest.php index c453d09..09a1d25 100644 --- a/tests/GetNodeVersionActionTest.php +++ b/tests/GetNodeVersionActionTest.php @@ -31,6 +31,27 @@ protected function resolveVersionFromBinary(): ?string rmdir($dir); }); +test('reads version from .nvmrc when shell_exec is disabled', function () { + $dir = sys_get_temp_dir().'/kite-sdk-test-'.uniqid(); + mkdir($dir); + file_put_contents($dir.'/.nvmrc', '20.11.0'); + + $action = new class($dir) extends GetNodeVersionAction + { + protected function canUseShellExec(): bool + { + return false; + } + }; + + $result = $action->handle(new Collection, fn ($data) => $data); + + expect($result[0]['value'])->toBe('20.11.0'); + + unlink($dir.'/.nvmrc'); + rmdir($dir); +}); + test('strips leading v from .nvmrc version', function () { $dir = sys_get_temp_dir().'/kite-sdk-test-'.uniqid(); mkdir($dir);