├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpstan-baseline.neon ├── phpstan.neon.dist └── src └── SecurityAdvisoriesCheck.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `security-advisories-health-check` will be documented in this file. 4 | 5 | ## 1.2.1 - 2024-06-05 6 | 7 | ### What's Changed 8 | 9 | * Ignore Packagist connection errors by @emaadali in https://github.com/spatie/security-advisories-health-check/pull/21 10 | 11 | ### New Contributors 12 | 13 | * @emaadali made their first contribution in https://github.com/spatie/security-advisories-health-check/pull/21 14 | 15 | **Full Changelog**: https://github.com/spatie/security-advisories-health-check/compare/1.2.0...1.2.1 16 | 17 | ## 1.2.0 - 2024-03-18 18 | 19 | ### What's Changed 20 | 21 | * Update some dependencies by @kudashevs in https://github.com/spatie/security-advisories-health-check/pull/18 22 | 23 | **Full Changelog**: https://github.com/spatie/security-advisories-health-check/compare/1.1.0...1.2.0 24 | 25 | ## 1.1.0 - 2023-08-25 26 | 27 | - retry calling Packagist to avoid false positives 28 | 29 | ## 0.0.2 - 2022-08-03 30 | 31 | **Full Changelog**: https://github.com/spatie/security-advisories-health-check/compare/0.0.1...0.0.2 32 | 33 | ## 0.0.1 - 2022-08-03 34 | 35 | - experimental release 36 | 37 | ## 1.0.2 - 2021-12-13 38 | 39 | - fix class name 40 | 41 | ## 1.0.1 - 2021-12-13 42 | 43 | - fix installation 44 | 45 | ## 1.0.0 - 2021-12-13 46 | 47 | - initial release 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Laravel Health check to security advisories for PHP packages 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/security-advisories-health-check.svg?style=flat-square)](https://packagist.org/packages/spatie/security-advisories-health-check) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/security-advisories-health-check.svg?style=flat-square)](https://packagist.org/packages/spatie/security-advisories-health-check) 5 | 6 | This package contains a [Laravel Health](https://spatie.be/docs/laravel-health) check that can report any known security issues with the installed PHP packages in your application. 7 | 8 | The security advisories are fetched from Packages and are sources from GitHub, and other sources. 9 | 10 | ```php 11 | // typically, in a service provider 12 | 13 | use Spatie\Health\Facades\Health; 14 | use Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck; 15 | 16 | Health::checks([ 17 | SecurityAdvisoriesCheck::new()->retryTimes(5), 18 | ]); 19 | ``` 20 | 21 | ## Documentation 22 | 23 | The documentation of this package is available [inside the docs of Laravel Health](https://spatie.be/docs/laravel-health/v1/available-checks/security-advisories). 24 | 25 | ## Support us 26 | 27 | [](https://spatie.be/github-ad-click/security-advisories-health-check) 28 | 29 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 30 | 31 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 32 | 33 | ## Testing 34 | 35 | ```bash 36 | composer test 37 | ``` 38 | 39 | ## Changelog 40 | 41 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 42 | 43 | ## Contributing 44 | 45 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 46 | 47 | ## Security Vulnerabilities 48 | 49 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 50 | 51 | ## Credits 52 | 53 | - [Freek Van der Herten](https://github.com/freekmurze) 54 | - [All Contributors](../../contributors) 55 | 56 | ## License 57 | 58 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/security-advisories-health-check", 3 | "description": "A Laravel Health check to security advisories for PHP packages", 4 | "keywords": [ 5 | "spatie", 6 | "laravel", 7 | "security-advisories-health-check" 8 | ], 9 | "homepage": "https://github.com/spatie/security-advisories-health-check", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Freek Van der Herten", 14 | "email": "freek@spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "spatie/packagist-api": "^2.1" 21 | }, 22 | "require-dev": { 23 | "nunomaduro/collision": "^6.1|^7.0|^8.0", 24 | "pestphp/pest": "^1.21|^2.3", 25 | "phpstan/extension-installer": "^1.1", 26 | "phpstan/phpstan-deprecation-rules": "^1.0", 27 | "phpstan/phpstan-phpunit": "^1.0", 28 | "phpunit/phpunit": "^9.5.10|^10.5", 29 | "spatie/laravel-health": "^1.22.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Spatie\\SecurityAdvisoriesHealthCheck\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Spatie\\SecurityAdvisoriesHealthCheck\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "analyse": "vendor/bin/phpstan analyse", 43 | "test": "vendor/bin/pest", 44 | "test-coverage": "vendor/bin/pest coverage" 45 | }, 46 | "config": { 47 | "sort-packages": true, 48 | "allow-plugins": { 49 | "phpstan/extension-installer": true, 50 | "pestphp/pest-plugin": true 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatie/security-advisories-health-check/763a8e6ee714b9bf9cb35953196c2c5a620a8e96/phpstan-baseline.neon -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: 4 6 | paths: 7 | - src 8 | tmpDir: build/phpstan 9 | checkMissingIterableValueType: false 10 | 11 | -------------------------------------------------------------------------------- /src/SecurityAdvisoriesCheck.php: -------------------------------------------------------------------------------- 1 | */ 18 | protected array $ignoredPackages = []; 19 | 20 | protected int $retryTimes = 5; 21 | 22 | protected int $gatewayExceptionCount = 0; 23 | 24 | protected ?Throwable $lastNonGatewayException = null; 25 | 26 | public PackagistClient $packagistClient; 27 | 28 | public function __construct(?PackagistClient $packagistClient = null) 29 | { 30 | parent::__construct(); 31 | 32 | $this->packagistClient = $packagistClient 33 | ?? new PackagistClient(new Client(), new PackagistUrlGenerator()); 34 | } 35 | 36 | public function retryTimes(int $times): self 37 | { 38 | $this->retryTimes = $times; 39 | 40 | return $this; 41 | } 42 | 43 | /** 44 | * @throws Throwable 45 | */ 46 | public function run(): Result 47 | { 48 | $packages = $this->getInstalledPackages(); 49 | 50 | try { 51 | $advisories = $this->retryGetAdvisories($packages); 52 | } catch (Throwable $exception) { 53 | if ($this->allRetriesAreGatewayErrors()) { 54 | return Result::make('Packagist service could not be reached')->ok(); 55 | } 56 | 57 | throw $this->lastNonGatewayException ?? $exception; 58 | } 59 | 60 | if ($advisories->isEmpty()) { 61 | return Result::make('No security vulnerability advisories found')->ok(); 62 | } 63 | 64 | $packageNames = $advisories->keys() 65 | ->map(fn (string $packageName) => "`{$packageName}`") 66 | ->join(', ', ' and '); 67 | 68 | return Result::make() 69 | ->meta($advisories->toArray()) 70 | ->failed("Security advisories found for {$packageNames}"); 71 | } 72 | 73 | public function ignorePackage(string $packageName): self 74 | { 75 | $this->ignoredPackages[] = $packageName; 76 | 77 | return $this; 78 | } 79 | 80 | public function ignoredPackages(array $packageNames): self 81 | { 82 | foreach ($packageNames as $packageName) { 83 | $this->ignorePackage($packageName); 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * @return Collection 91 | */ 92 | protected function getInstalledPackages(): Collection 93 | { 94 | return collect(InstalledVersions::getAllRawData()[0]['versions']) 95 | ->filter(fn (array $packageProperties) => isset($packageProperties['version'])) 96 | ->filter(fn (array $packageProperties, string $packageName) => ! in_array($packageName, $this->ignoredPackages)) 97 | ->mapWithKeys(function (array $packageProperties, string $packageName) { 98 | return [$packageName => $packageProperties['version']]; 99 | }); 100 | } 101 | 102 | /** 103 | * @return Collection 104 | */ 105 | protected function getAdvisories(Collection $packages): Collection 106 | { 107 | $advisories = $this 108 | ->packagistClient 109 | ->getAdvisoriesAffectingVersions($packages->toArray()); 110 | 111 | return collect($advisories); 112 | } 113 | 114 | protected function allRetriesAreGatewayErrors(): bool 115 | { 116 | // Compare `$this->gatewayExceptionCount` with `retryTimes - 1` rather than `retryTimes`. 117 | // The `shouldRetry` callback is not executed on the final retry, so the last exception 118 | // in the retry loop does not increment `$this->gatewayExceptionCount`. 119 | 120 | return $this->gatewayExceptionCount === $this->retryTimes - 1; 121 | } 122 | 123 | /** 124 | * @throws Throwable 125 | */ 126 | protected function retryGetAdvisories(Collection $packages): Collection 127 | { 128 | return retry( 129 | times: $this->retryTimes, 130 | callback: fn () => $this->getAdvisories($packages), 131 | sleepMilliseconds: 2 * 1000, 132 | when: fn ($exception) => $this->shouldRetry($exception) 133 | ); 134 | } 135 | 136 | protected function shouldRetry($exception): bool 137 | { 138 | $isGatewayException = $exception instanceof ServerException 139 | && in_array($exception->getCode(), [502, 503, 504]); 140 | 141 | if ($isGatewayException) { 142 | $this->gatewayExceptionCount++; 143 | } 144 | 145 | $this->lastNonGatewayException = $isGatewayException 146 | ? $this->lastNonGatewayException 147 | : $exception; 148 | 149 | return true; 150 | } 151 | } 152 | --------------------------------------------------------------------------------