├── .github ├── dependabot.yml └── workflows │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── laravel-security-checker.php ├── phpunit.xml.dist ├── phpunit10.xml.dist ├── resources ├── lang │ ├── de │ │ └── messages.php │ ├── en │ │ └── messages.php │ ├── es │ │ └── messages.php │ └── nl │ │ └── messages.php └── views │ └── security-mail.blade.php ├── src ├── Console │ ├── SecurityCommand.php │ ├── SecurityMailCommand.php │ └── SecuritySlackCommand.php ├── Formatter │ ├── FormatterInterface.php │ └── SimpleFormatter.php ├── Mailables │ └── SecurityMail.php ├── Notifications │ └── SecuritySlackNotification.php └── ServiceProvider.php └── tests ├── SecurityCommandTest.php ├── SecurityMailCommandTest.php ├── SecurityMailTest.php ├── SecuritySlackCommandTest.php ├── SecuritySlackTest.php └── TestCase.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] 15 | laravel: ['6.*', '7.*', '8.*', '9.*', '10.*', '11.*', '12.*'] 16 | exclude: 17 | - php: 7.4 18 | laravel: 12.* 19 | - php: 8.0 20 | laravel: 12.* 21 | - php: 8.1 22 | laravel: 12.* 23 | - php: 7.4 24 | laravel: 11.* 25 | - php: 8.0 26 | laravel: 11.* 27 | - php: 8.1 28 | laravel: 11.* 29 | - php: 7.4 30 | laravel: 10.* 31 | - php: 8.0 32 | laravel: 10.* 33 | - php: 8.4 34 | laravel: 10.* 35 | - php: 7.4 36 | laravel: 9.* 37 | - php: 8.3 38 | laravel: 9.* 39 | - php: 8.4 40 | laravel: 9.* 41 | - php: 8.2 42 | laravel: 8.* 43 | - php: 8.3 44 | laravel: 8.* 45 | - php: 8.4 46 | laravel: 8.* 47 | - php: 8.1 48 | laravel: 7.* 49 | - php: 8.2 50 | laravel: 7.* 51 | - php: 8.3 52 | laravel: 7.* 53 | - php: 8.4 54 | laravel: 7.* 55 | - php: 8.1 56 | laravel: 6.* 57 | - php: 8.2 58 | laravel: 6.* 59 | - php: 8.3 60 | laravel: 6.* 61 | - php: 8.4 62 | laravel: 6.* 63 | 64 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 65 | 66 | steps: 67 | - name: Checkout code 68 | uses: actions/checkout@v4 69 | 70 | - name: Setup PHP 71 | uses: shivammathur/setup-php@v2 72 | with: 73 | php-version: ${{ matrix.php }} 74 | extensions: dom, curl, libxml, mbstring, zip, json 75 | coverage: pcov 76 | 77 | - name: Install dependencies 78 | run: | 79 | composer require "illuminate/contracts=${{ matrix.laravel }}" --prefer-stable --no-update 80 | composer update --prefer-dist --no-interaction --no-progress 81 | 82 | - name: Grab PHPUnit major version 83 | id: phpunit_version 84 | run: echo "MAJOR_VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+' | cut -d '.' -f 1)" >> $GITHUB_OUTPUT 85 | 86 | - name: Execute tests (PHPUnit >= 10) 87 | if: "${{ fromJSON(steps.phpunit_version.outputs.MAJOR_VERSION) >= 10 }}" 88 | run: vendor/bin/phpunit --configuration=phpunit10.xml.dist 89 | 90 | - name: Execute tests (PHPUnit < 10) 91 | if: "${{ fromJSON(steps.phpunit_version.outputs.MAJOR_VERSION) < 10 }}" 92 | run: vendor/bin/phpunit --configuration=phpunit.xml.dist 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | .idea 4 | build 5 | .phpunit.result.cache 6 | .phpunit.cache -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Laravel Security Checker 2 | 3 | ## v2.5.0 (2024-06-02) 4 | * add support for Laravel 11, PHP 8.3 5 | 6 | ## v2.4.0 (2023-04-28) 7 | * add support for Laravel 10, PHP 8.2 8 | 9 | ## v2.3.0 (2022-03-02) 10 | * add support for Laravel 9, PHP 8.1 11 | * dropped support for PHP 7.3, minimum version is now 7.4 12 | 13 | ## v2.2.1 (2021-03-10) 14 | FIXED 15 | * a permission failure when using this package with multiple users on the same server by adding `temp-dir` to available config options (thanks @thomasderoo4!) 16 | 17 | ## v2.2.0 (2021-02-03) 18 | A big thank you to @paras-malhotra and Enlightn for helping out this release. 19 | 20 | * switched out travis with github actions, tests will now run in a matrix from Laravel 6 to 8 in combination with PHP 7.3 to 8 21 | * `sensiolabs/security-checker` is abandoned, replaced with successor `enlightn/security-checker` 22 | * allowed PHP 8 in the version constraints 23 | * removed support for Laravel < 6.0, supported versions are now: 6.x to 8.x 24 | * added Spanish language files (thanks @gfmr806) 25 | 26 | ## v2.1.0 (2020-09-28) 27 | * dropped support for PHP 7.1, minimum version is now PHP 7.2 28 | * upgraded `guzzlehttp/guzzle` to a new major version (^v7.0.0) 29 | * added support for Laravel 8 (thanks @romanstingler, @nessimabadi!) 30 | 31 | ## v2.0.0 (2020-03-04) 32 | * dropped support for PHP 7.0, minimum version is now PHP 7.1.3 33 | * upgraded `sensiolabs/security-checker` to a new major version (^v6.0.0) 34 | * added support for Laravel 7 (thanks @cino!) 35 | 36 | ## v1.2.0 (2020-03-03) 37 | * improved the `security:now` command to return exit code 1 when vulnerabilities were found, this enables integration into CI flows 38 | 39 | ## v1.1.1 (2019-09-23) 40 | * added support for Laravel 6.0 (thanks @davejamesmiller!) 41 | 42 | ## v1.1.0 (2019-03-09) 43 | * added support for Laravel 5.8 (thanks @DevDavido!) 44 | * added logging for email and slack commands 45 | 46 | ## v1.0.0 (2019-01-12) 47 | * @DevDavido notified me about the SensioLabs Security Checker upgrade, I implemented their changes 48 | * bumped the package to a stable tag, I think it has matured enough now. :-) 49 | 50 | ## v0.3.0 (2018-08-03) 51 | * updated to work on Laravel 5.5.x, 5.6.x and 5.7.x, thanks @jorgenb 52 | * dropped support for PHP 5.x 53 | * added Slack notifications on vulnerabilities, thanks @jorgenb 54 | * renamed LCS_EMAIL_WITHOUT_VULNERABILITIES TO LCS_NOTIFY_WITHOUT_VULNERABILITIES to reflect the Slack notification 55 | 56 | ## v0.2.2 (2017-07-23) 57 | * wrote tests to cover 100% of the package functionality 58 | 59 | ## v0.2.1 (2017-07-23) 60 | * fixed a bug in the email where the CVE wasn't displayed correctly 61 | * added DE and NL languages. thanks @mijndert 62 | 63 | ## v0.2.0 (2017-07-22) 64 | * added configuration option that won't email you when there are no vulnerabilities and it is enabled by default. 65 | * code improvements 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jorijn Schrijvershof 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Laravel Security Checker 2 | [![Latest Stable Version](https://img.shields.io/packagist/v/jorijn/laravel-security-checker.svg)](https://packagist.org/packages/jorijn/laravel-security-checker) 3 | [![Total Downloads](https://img.shields.io/packagist/dt/jorijn/laravel-security-checker.svg)](https://packagist.org/packages/jorijn/laravel-security-checker) 4 | [![License](https://img.shields.io/github/license/jorijn/laravel-security-checker.svg)](https://packagist.org/packages/jorijn/laravel-security-checker) 5 | ![Tests](https://github.com/Jorijn/laravel-security-checker/workflows/tests/badge.svg) 6 | 7 | This package provides an effortless way for you to check your local `composer.lock` against the [Security Advisories Database](https://github.com/FriendsOfPHP/security-advisories). 8 | It can either display the results in your console or email them to you on a scheduled basis. It uses Laravel's markdown system, so it should fit nicely in your styling. 9 | 10 | #### Screenshot 11 | screenshot-email 12 | 13 | ## Installation 14 | Require this package with composer using the following command: 15 | 16 | ```bash 17 | composer require jorijn/laravel-security-checker 18 | ``` 19 | 20 | ### Configuration 21 | 22 | #### Email 23 | If you want the package to send reports by email, you'll need to specify a recipient. 24 | 25 | ##### Option 1 26 | Add it to your `.env` file. 27 | 28 | ``` 29 | LCS_MAIL_TO="someone@example.net" 30 | ``` 31 | 32 | ##### Option 2 33 | Publish the configuration file and change it there. 34 | 35 | ```bash 36 | php artisan vendor:publish --provider="Jorijn\LaravelSecurityChecker\ServiceProvider" --tag="config" 37 | ``` 38 | 39 | If you want to control on how the email is formatted you can have Laravel export the view for you using: 40 | 41 | ```bash 42 | php artisan vendor:publish --provider="Jorijn\LaravelSecurityChecker\ServiceProvider" --tag="views" 43 | ``` 44 | 45 | By default, the package won't email you when there are no vulnerabilities found. You can change this setting by adding the following entry to your `.env` file. 46 | 47 | ``` 48 | LCS_NOTIFY_WITHOUT_VULNERABILITIES=true 49 | ``` 50 | 51 | #### Slack 52 | If you want the package to send the report to a Slack channel, you will need to specify a Slack Webhook 53 | in your `.env` file. 54 | 55 | E.g.: 56 | 57 | ``` 58 | LCS_SLACK_WEBHOOK=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX 59 | ``` 60 | 61 | ### Scheduling 62 | The package exposes a new command for you: 63 | 64 | ```bash 65 | php artisan security-check:email 66 | ``` 67 | 68 | You can hook it up into a regular crontab or add it into the Laravel Scheduler (`app/Console/Kernel.php`) like this: 69 | 70 | ```php 71 | protected function schedule(Schedule $schedule) 72 | { 73 | $schedule->command(\Jorijn\LaravelSecurityChecker\Console\SecurityMailCommand::class) 74 | ->weekly(); 75 | } 76 | ``` 77 | 78 | ## Running as a command 79 | This package provides a wrapper around the [Enlightn Security Checker](https://github.com/enlightn/security-checker) command. You can call it using `php artisan security-check:now`. 80 | 81 | ![screenshot-console](https://user-images.githubusercontent.com/85466/28452254-17f3476e-6df2-11e7-9e5e-1c3d52b57722.png) 82 | 83 | ## Translations 84 | If you need to translate this package into your own language you can do so by publishing the translation files: 85 | 86 | ```bash 87 | php artisan vendor:publish --provider="Jorijn\LaravelSecurityChecker\ServiceProvider" --tag="translations" 88 | ``` 89 | 90 | Please consider helping out by creating a pull request with your language to help out others. 91 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jorijn/laravel-security-checker", 3 | "description": "Added Laravel functionality to the Enlightn Security Checker. Adds a command to check for, and optionally emails you, vulnerabilities when they affect you.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "security", 8 | "composer", 9 | "dependencies" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Jorijn Schrijvershof", 14 | "email": "jorijn@jorijn.com" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=7.4|^8.0", 19 | "guzzlehttp/guzzle": "^7.0", 20 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 21 | "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 22 | "illuminate/bus": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/mail": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/queue": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "illuminate/notifications": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 26 | "ext-json": "*", 27 | "enlightn/security-checker": "^1.3|^2.0" 28 | }, 29 | "require-dev": { 30 | "laravel/slack-notification-channel": "^1.0|^2.0|^3.2", 31 | "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0|^10.5|^11.5.3", 32 | "squizlabs/php_codesniffer": "~2.3|^3.6", 33 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 34 | "mockery/mockery": "^1.2" 35 | }, 36 | "suggest": { 37 | "laravel/slack-notification-channel": "If you need slack notifications, you must install this package." 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Jorijn\\LaravelSecurityChecker\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Jorijn\\LaravelSecurityChecker\\Tests\\": "tests" 47 | } 48 | }, 49 | "scripts": { 50 | "test": "phpunit", 51 | "cs": "phpcs --standard=psr2 src/" 52 | }, 53 | "extra": { 54 | "laravel": { 55 | "providers": [ 56 | "Jorijn\\LaravelSecurityChecker\\ServiceProvider" 57 | ] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/laravel-security-checker.php: -------------------------------------------------------------------------------- 1 | [ 14 | env('LCS_MAIL_TO', null) 15 | ], 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Laravel Security Checker — Email settings 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Decides whether the package should send an email even if there aren't 23 | | any vulnerabilities found. 24 | | 25 | */ 26 | 27 | 'notify_even_without_vulnerabilities' => env('LCS_NOTIFY_WITHOUT_VULNERABILITIES', false), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Laravel Security Checker — Slack Webhook URL 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Which Slack Webhook URL should we post to when using Slack notifications? 35 | | 36 | */ 37 | 38 | 'slack_webhook_url' => env('LCS_SLACK_WEBHOOK', null), 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Laravel Security Checker — Temp dir 42 | |-------------------------------------------------------------------------- 43 | | 44 | | Decides where enlightn/security-checker will place its temp files. 45 | | Useful when using this package with multiple users/permissions on a single server. 46 | | See: https://github.com/enlightn/security-checker/issues/17 47 | | https://github.com/Jorijn/laravel-security-checker/issues/35 48 | | Value: 49 | | An absolute path to a directory to place the temp files in. 50 | | null = default /tmp directory 51 | | 52 | */ 53 | 'temp_dir' => null 54 | ]; 55 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /phpunit10.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./src 25 | 26 | 27 | -------------------------------------------------------------------------------- /resources/lang/de/messages.php: -------------------------------------------------------------------------------- 1 | '{0} Keine Pakete haben Schwachstellen bekannt|{1} 1 Paket hat Schwachstellen bekannt|[2,*] :count Pakete haben Schwachstellen bekannt', 15 | 'title' => 'Titel', 16 | 'view' => 'Ansehen', 17 | 'cve' => 'CVE', 18 | 'information' => 'Information', 19 | 'no_recipients_configured' => 'Es wurden noch keine Empfänger konfiguriert!', 20 | 'body_no_vulnerabilities' => 'Es scheint, dass keine Pakete irgendwelche bekannten Schwachstellen haben. [Hier](https://github.com/jorijn/laravel-security-checker#configuration) können Sie mehr über die Deaktivierung dieser Nachricht lesen.' 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/en/messages.php: -------------------------------------------------------------------------------- 1 | '{0} No packages have known vulnerabilities|{1} 1 package has known vulnerabilities|[2,*] :count packages have known vulnerabilities', 15 | 'title' => 'Title', 16 | 'view' => 'View', 17 | 'cve' => 'CVE', 18 | 'information' => 'Information', 19 | 'no_recipients_configured' => 'No recipients has been configured yet!', 20 | 'body_no_vulnerabilities' => 'It seems that no packages have any known vulnerabilities. You can read more about disabling this message [here](https://github.com/jorijn/laravel-security-checker#configuration).' 21 | ]; -------------------------------------------------------------------------------- /resources/lang/es/messages.php: -------------------------------------------------------------------------------- 1 | '{0} Ningún paquete tiene vulnerabilidades conocidas | {1} 1 paquete tiene vulnerabilidades conocidas | [2, *]: recuento de paquetes tienen vulnerabilidades conocidas', 15 | 'title' => 'Titulo', 16 | 'view' => 'Vista', 17 | 'cve' => 'CVE', 18 | 'information' => 'Información', 19 | 'no_recipients_configured' => 'Aún no se ha configurado ningún destinatario.!', 20 | 'body_no_vulnerabilities' => 'Parece que ningún paquete tiene vulnerabilidades conocidas. Puede leer más sobre cómo deshabilitar este mensaje [aquí](https://github.com/jorijn/laravel-security-checker#configuration).' 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/nl/messages.php: -------------------------------------------------------------------------------- 1 | '{0} Er zijn geen pakketten met bekende kwetsbaarheden|{1} 1 pakket heeft bekende kwetsbaarheden|[2,*] :count pakketten hebben bekende kwetsbaarheden', 15 | 'title' => 'Titel', 16 | 'view' => 'Bekijken', 17 | 'cve' => 'CVE', 18 | 'information' => 'Informatie', 19 | 'no_recipients_configured' => 'Er zijn nog geen ontvangers geconfigureerd!', 20 | 'body_no_vulnerabilities' => 'Het lijkt erop dat er geen pakketten zijn met bekende kwetsbaarheden. Lees [hier](https://github.com/jorijn/laravel-security-checker#configuration) hoe je dit bericht uit kunt schakelen.' 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/views/security-mail.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | # {{ $title }} 3 | 4 | @foreach ($packages as $package => $report) 5 | ## {{ $package }} — {{ $report['version'] }} 6 | 7 | @component('mail::table') 8 | | @lang('laravel-security-checker::messages.title') | @lang('laravel-security-checker::messages.cve') | @lang('laravel-security-checker::messages.information') | 9 | | :---- | :-- | :---------- | 10 | @foreach($report['advisories'] as $key => $information) 11 | | {{ $information['title'] }} | {{ $information['cve'] or '' }} | [@lang('laravel-security-checker::messages.view')]({{ $information['link'] }}) 12 | @endforeach 13 | @endcomponent 14 | @endforeach 15 | 16 | @if (count($packages) === 0) 17 | {{ __('laravel-security-checker::messages.body_no_vulnerabilities') }} 18 | @endif 19 | 20 | @endcomponent -------------------------------------------------------------------------------- /src/Console/SecurityCommand.php: -------------------------------------------------------------------------------- 1 | checker = $checker; 36 | } 37 | 38 | /** 39 | * Execute the command 40 | */ 41 | public function handle() 42 | { 43 | // get the path to composer.lock 44 | $composerLock = base_path('composer.lock'); 45 | 46 | // and feed it into the SecurityChecker 47 | $checkResult = $this->checker->check($composerLock); 48 | 49 | // then display it using the formatter provided 50 | app(SimpleFormatter::class)->displayResults($this->getOutput(), $composerLock, $checkResult); 51 | 52 | return (int) (count($checkResult) > 0); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Console/SecurityMailCommand.php: -------------------------------------------------------------------------------- 1 | checker = $checker; 38 | } 39 | 40 | /** 41 | * Execute the command 42 | */ 43 | public function handle() 44 | { 45 | // get the path to composer.lock 46 | $composerLock = base_path('composer.lock'); 47 | 48 | // and feed it into the SecurityChecker 49 | Log::debug('about to check for vulnerabilities'); 50 | $checkResult = $this->checker->check($composerLock); 51 | 52 | // if the user didn't want any email if there are no results, 53 | // cancel execution here. 54 | $proceed = config('laravel-security-checker.notify_even_without_vulnerabilities', false); 55 | if ($proceed !== true && \count($checkResult) === 0) { 56 | Log::info('no vulnerabilities were found, not sending any email'); 57 | return 0; 58 | } 59 | 60 | // get the recipients and filter out any configuration mistakes 61 | $recipients = collect(config('laravel-security-checker.recipients', [ ]))->filter(function ($recipient) { 62 | return $recipient !== null && !empty($recipient); 63 | }); 64 | 65 | if ($recipients->count() === 0) { 66 | Log::error('vulnerabilities were found, but there are no recipients configured'); 67 | $this->error( 68 | /** @scrutinizer ignore-type */__('laravel-security-checker::messages.no_recipients_configured') 69 | ); 70 | return 1; 71 | } 72 | 73 | Log::warning('vulnerabilities were found, emailed to configured recipients'); 74 | Mail::to($recipients->toArray())->send(new SecurityMail($checkResult)); 75 | 76 | return 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Console/SecuritySlackCommand.php: -------------------------------------------------------------------------------- 1 | checker = $checker; 38 | } 39 | 40 | /** 41 | * Execute the command 42 | */ 43 | public function handle() 44 | { 45 | // require that the user specifies a slack channel in the .env file 46 | if (!config('laravel-security-checker.slack_webhook_url')) { 47 | Log::error('checking for vulnerabilities using slack was requested but no hook is configured'); 48 | throw new \Exception('No Slack Webhook has been specified.'); 49 | } 50 | 51 | // get the path to composer.lock 52 | $composerLock = base_path('composer.lock'); 53 | 54 | // and feed it into the SecurityChecker 55 | Log::debug('about to check for vulnerabilities'); 56 | $vulnerabilities = $this->checker->check($composerLock); 57 | 58 | // cancel execution here if user does not want to be notified when there are 0 vulns. 59 | $proceed = config('laravel-security-checker.notify_even_without_vulnerabilities', false); 60 | if (count($vulnerabilities) === 0 && $proceed !== true) { 61 | Log::info('no vulnerabilities were found, not sending a slack notification'); 62 | 63 | return 0; 64 | } 65 | 66 | Log::warning('vulnerabilities were found, sending slack notification to configured hook'); 67 | Notification::route('slack', config('laravel-security-checker.slack_webhook_url', null)) 68 | ->notify(new SecuritySlackNotification($vulnerabilities, realpath($composerLock))); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Formatter/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | writeln(sprintf('Security Check Report: %s', realpath($lockFilePath))); 19 | 20 | if ($count = count($vulnerabilities)) { 21 | $status = 'CRITICAL'; 22 | $style = 'error'; 23 | } else { 24 | $status = 'OK'; 25 | $style = 'info'; 26 | } 27 | 28 | $output->writeln(sprintf( 29 | '<%s>[%s] %d %s known vulnerabilities', 30 | $style, 31 | $status, 32 | $count, 33 | 1 === $count ? 'package has' : 'packages have' 34 | )); 35 | 36 | if (0 !== $count) { 37 | $output->write("\n"); 38 | 39 | foreach ($vulnerabilities as $dependency => $issues) { 40 | $dependencyFullName = $dependency.' ('.$issues['version'].')'; 41 | $output->writeln(''.$dependencyFullName."\n".str_repeat( 42 | '-', 43 | strlen($dependencyFullName) 44 | )."\n"); 45 | 46 | foreach ($issues['advisories'] as $issue => $details) { 47 | $output->write(' * '); 48 | if ($details['cve']) { 49 | $output->write(''.$details['cve'].': '); 50 | } 51 | $output->writeln($details['title']); 52 | 53 | if ('' !== $details['link']) { 54 | $output->writeln(' '.$details['link']); 55 | } 56 | 57 | $output->writeln(''); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Mailables/SecurityMail.php: -------------------------------------------------------------------------------- 1 | checkResult = $checkResult; 24 | } 25 | 26 | /** 27 | * Build the message. 28 | * 29 | * @return $this 30 | */ 31 | public function build() 32 | { 33 | $packageCount = count($this->checkResult); 34 | $title = trans_choice('laravel-security-checker::messages.subject_new_vulnerabilities', $packageCount, [ 35 | 'count' => $packageCount 36 | ]); 37 | $subject = '['.config('app.name').'] '.$title; 38 | 39 | return $this->subject($subject) 40 | ->markdown('laravel-security-checker::security-mail', [ 41 | 'title' => $title, 42 | 'packages' => $this->checkResult 43 | ]); 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function getCheckResult() 50 | { 51 | return $this->checkResult; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Notifications/SecuritySlackNotification.php: -------------------------------------------------------------------------------- 1 | vulnerabilities = $vulnerabilities; 35 | $this->composerLockPath = $composerLockPath; 36 | } 37 | 38 | /** 39 | * Get the notification's delivery channels. 40 | * 41 | * @return array 42 | */ 43 | public function via() 44 | { 45 | return [ 'slack' ]; 46 | } 47 | 48 | /** 49 | * Get the slack representation of the notification. 50 | * 51 | * @return SlackMessage 52 | */ 53 | public function toSlack() 54 | { 55 | return (new SlackMessage) 56 | ->from(config('app.url')) 57 | ->content("*Security Check Report:* `{$this->composerLockPath}`") 58 | ->attachment(function ($attachment) { 59 | $attachment->content($this->textFormatter())->markdown([ 'text' ]); 60 | }); 61 | } 62 | 63 | /** 64 | * Get the array representation of the notification. 65 | * 66 | * @return array 67 | */ 68 | public function toArray() 69 | { 70 | return $this->vulnerabilities; 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | protected function textFormatter() 77 | { 78 | $packageCount = \count($this->vulnerabilities); 79 | $content = trans_choice('laravel-security-checker::messages.subject_new_vulnerabilities', $packageCount, [ 80 | 'count' => $packageCount, 81 | ]); 82 | 83 | if ($packageCount > 0) { 84 | foreach ($this->vulnerabilities as $dependency => $issues) { 85 | $dependencyFullName = sprintf('%s (%s)', $dependency, $issues[ 'version' ]); 86 | 87 | $content .= PHP_EOL; 88 | $content .= sprintf('*%s*', $dependencyFullName); 89 | $content .= PHP_EOL; 90 | $content .= str_repeat('-', \strlen($dependencyFullName)); 91 | $content .= PHP_EOL; 92 | 93 | foreach ($issues[ 'advisories' ] as $issue => $details) { 94 | $content .= ' * '; 95 | 96 | if ($details[ 'cve' ]) { 97 | $content .= $details[ 'cve' ].' '; 98 | } 99 | 100 | $content .= $details[ 'title' ].' '; 101 | 102 | if (!empty($details[ 'link' ])) { 103 | $content .= $details[ 'link' ]; 104 | } 105 | 106 | $content .= PHP_EOL; 107 | } 108 | } 109 | } 110 | 111 | return $content; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom($configPath, 'laravel-security-checker'); 28 | 29 | $this->app->bind(SecurityChecker::class, function() { 30 | $temp_dir = config('laravel-security-checker.temp_dir', null); 31 | return new SecurityChecker($temp_dir); 32 | }); 33 | } 34 | 35 | /** 36 | * Bootstrap the application events. 37 | * 38 | * @return void 39 | */ 40 | public function boot() 41 | { 42 | // configuration 43 | $configPath = __DIR__.'/../config/laravel-security-checker.php'; 44 | $this->publishes([ $configPath => $this->getConfigPath() ], 'config'); 45 | 46 | // views 47 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-security-checker'); 48 | $this->publishes([ 49 | __DIR__.'/../resources/views' => resource_path('views/vendor/laravel-security-checker'), 50 | ], 'views'); 51 | 52 | // translations 53 | $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'laravel-security-checker'); 54 | $this->publishes([ 55 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/laravel-security-checker'), 56 | ], 'translations'); 57 | 58 | if ($this->app->runningInConsole()) { 59 | $this->commands([ 60 | SecurityCommand::class, 61 | SecurityMailCommand::class, 62 | SecuritySlackCommand::class, 63 | ]); 64 | } 65 | } 66 | 67 | /** 68 | * Get the config path 69 | * 70 | * @return string 71 | */ 72 | protected function getConfigPath() 73 | { 74 | return config_path('laravel-security-checker.php'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/SecurityCommandTest.php: -------------------------------------------------------------------------------- 1 | bindPassingSecurityChecker(); 10 | 11 | $this->artisan( 12 | 'security-check:now' 13 | )->assertExitCode(0); 14 | } 15 | 16 | public function testFireMethodWithVulnerabilitiesFound() 17 | { 18 | $this->bindFailingSecurityChecker(); 19 | 20 | $this->artisan( 21 | 'security-check:now' 22 | )->assertExitCode(1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/SecurityMailCommandTest.php: -------------------------------------------------------------------------------- 1 | bindFailingSecurityChecker(); 19 | 20 | // set the recipient for testing 21 | Config::set('laravel-security-checker.recipients', [ 'recipient@example.net' ]); 22 | 23 | // execute the command 24 | $this->artisan('security-check:email')->assertExitCode(0); 25 | 26 | Mail::assertSent(SecurityMail::class, function (SecurityMail $mail) { 27 | return $mail->hasTo('recipient@example.net'); 28 | }); 29 | } 30 | 31 | /** 32 | * Tests if the email will cancel if there are no recipients 33 | */ 34 | public function testFireMethodWithoutRecipients(): void 35 | { 36 | Mail::fake(); 37 | 38 | $this->bindFailingSecurityChecker(); 39 | 40 | // set the recipient for testing 41 | Config::set('laravel-security-checker.recipients', [ ]); 42 | 43 | $this->artisan('security-check:email')->assertExitCode(1); 44 | 45 | // check that the mail wasn't sent 46 | Mail::assertNotSent(SecurityMail::class); 47 | } 48 | 49 | /** 50 | * Tests if the email will cancel if there are no vulnerabilities 51 | */ 52 | public function testFireMethodWithoutVulnerabilities(): void 53 | { 54 | Mail::fake(); 55 | 56 | $this->bindPassingSecurityChecker(); 57 | 58 | // set the recipient for testing 59 | Config::set('laravel-security-checker.recipients', [ 'recipient@example.net' ]); 60 | Config::set('laravel-security-checker.notify_even_without_vulnerabilities', false); 61 | 62 | $this->artisan('security-check:email')->assertExitCode(0); 63 | 64 | // check that the mail wasn't sent 65 | Mail::assertNotSent(SecurityMail::class); 66 | } 67 | 68 | /** 69 | * Tests if the email will cancel if there are no vulnerabilities 70 | */ 71 | public function testFireMethodWithoutVulnerabilitiesWithSending(): void 72 | { 73 | Mail::fake(); 74 | 75 | $this->bindPassingSecurityChecker(); 76 | 77 | // set the recipient for testing 78 | Config::set('laravel-security-checker.recipients', [ 'recipient@example.net' ]); 79 | Config::set('laravel-security-checker.notify_even_without_vulnerabilities', true); 80 | 81 | $this->artisan('security-check:email')->assertExitCode(0); 82 | 83 | // check that the mail was sent 84 | Mail::assertSent(SecurityMail::class, function (SecurityMail $mail) { 85 | return $mail->hasTo('recipient@example.net') 86 | && [ ] === $mail->getCheckResult(); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/SecurityMailTest.php: -------------------------------------------------------------------------------- 1 | getFakeVulnerabilityReport(); 17 | $mailable = new SecurityMail($vulnerabilities); 18 | 19 | $this->assertInstanceOf(SecurityMail::class, $mailable); 20 | 21 | // build the mailable with the build() method -- this fills all template 22 | // variables and generates the subject and title. 23 | $generatedMailable = $mailable->build(); 24 | $vulnerabilityCount = count($vulnerabilities); 25 | $translatedSubject = trans_choice( 26 | 'laravel-security-checker::messages.subject_new_vulnerabilities', 27 | $vulnerabilityCount, 28 | [ 'count' => $vulnerabilityCount ] 29 | ); 30 | 31 | // assert that this has been done correctly. 32 | $this->assertEquals($generatedMailable->viewData['title'], $translatedSubject); 33 | $this->assertEquals($generatedMailable->subject, '['.Config::get('app.name').'] '. $translatedSubject); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/SecuritySlackCommandTest.php: -------------------------------------------------------------------------------- 1 | bindFailingSecurityChecker(); 25 | 26 | // set the recipient for testing 27 | Config::set( 28 | 'laravel-security-checker.slack_webhook_url', 29 | 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' 30 | ); 31 | 32 | $this->artisan('security-check:slack')->assertExitCode(0); 33 | 34 | // https://github.com/laravel/framework/pull/21379 35 | Notification::assertSentTo( 36 | new AnonymousNotifiable, 37 | SecuritySlackNotification::class, 38 | function ($notification, $channels, $notifiable) { 39 | return $notifiable->routes['slack'] === config('laravel-security-checker.slack_webhook_url'); 40 | } 41 | ); 42 | } 43 | 44 | /** 45 | * Tests that no notification is sent if a Slack Webhook has not been configured. 46 | */ 47 | public function testHandleMethodWithoutSlackWebHook(): void 48 | { 49 | Notification::fake(); 50 | 51 | $this->bindFailingSecurityChecker(); 52 | 53 | // set the recipient for testing 54 | Config::set('laravel-security-checker.slack_webhook_url', null); 55 | 56 | // Class should throw exception if no webhook is configured 57 | $this->expectException(\Exception::class); 58 | 59 | $this->artisan('security-check:slack')->assertExitCode(1); 60 | 61 | Notification::assertNotSentTo( 62 | new AnonymousNotifiable, 63 | SecuritySlackNotification::class 64 | ); 65 | } 66 | 67 | /** 68 | * Test that no notification get's sent if there are no vulnerabilities 69 | */ 70 | public function testHandleMethodWithoutVulnerabilities(): void 71 | { 72 | Notification::fake(); 73 | 74 | $this->bindPassingSecurityChecker(); 75 | 76 | // set the recipient for testing 77 | Config::set( 78 | 'laravel-security-checker.slack_webhook_url', 79 | 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' 80 | ); 81 | 82 | $this->artisan('security-check:slack')->assertExitCode(0); 83 | 84 | // check that the notification wasn't sent 85 | Notification::assertNotSentTo( 86 | new AnonymousNotifiable, 87 | SecuritySlackNotification::class 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/SecuritySlackTest.php: -------------------------------------------------------------------------------- 1 | getFakeVulnerabilityReport(); 15 | $composerLockPath = '/path/to/composer.lock'; 16 | $notification = new SecuritySlackNotification($vulnerabilities, $composerLockPath); 17 | 18 | $this->assertInstanceOf(SecuritySlackNotification::class, $notification); 19 | 20 | $generatedNotification = $notification->toSlack(); 21 | 22 | $this->assertEquals('slack', $notification->via()[0]); 23 | $this->assertEquals($notification->toArray(), $vulnerabilities); 24 | $this->assertEquals($generatedNotification->username, config('app.url')); 25 | $this->assertEquals("*Security Check Report:* `{$composerLockPath}`", $generatedNotification->content); 26 | $this->assertCount(count($generatedNotification->attachments), $vulnerabilities); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | allows('check')->andReturns([]); 23 | 24 | // bind Mockery instance to the app container 25 | $this->app->instance(SecurityChecker::class, $securityCheckerMock); 26 | } 27 | 28 | protected function bindFailingSecurityChecker(): void 29 | { 30 | $securityCheckerMock = \Mockery::mock(SecurityChecker::class); 31 | $securityCheckerMock->allows('check')->andReturns($this->getFakeVulnerabilityReport()); 32 | 33 | // bind Mockery instance to the app container 34 | $this->app->instance(SecurityChecker::class, $securityCheckerMock); 35 | } 36 | 37 | /** 38 | * Returns a fake vulnerability report that is digestible by our package 39 | * 40 | * @return array 41 | */ 42 | public function getFakeVulnerabilityReport(): array 43 | { 44 | return [ 45 | 'bugsnag/bugsnag-laravel' => [ 46 | 'version' => 'v2.0.1', 47 | 'advisories' => [ 48 | 'bugsnag/bugsnag-laravel/CVE-2016-5385.yaml' => [ 49 | 'title' => 'HTTP Proxy header vulnerability', 50 | 'link' => 'https://github.com/bugsnag/bugsnag-laravel/releases/tag/v2.0.2', 51 | 'cve' => 'CVE-2016-5385' 52 | ] 53 | ] 54 | ] 55 | ]; 56 | } 57 | } 58 | --------------------------------------------------------------------------------