├── .codecov.yml ├── CONTRIBUTING.md ├── client └── dist │ ├── css │ └── securityalerts.css │ └── javascript │ └── summaryalerts.js ├── src ├── Extensions │ ├── SecurityAlertExtension.php │ ├── SiteSummaryExtension.php │ └── PackageSecurityExtension.php ├── Models │ └── SecurityAlert.php ├── Jobs │ └── SecurityAlertCheckJob.php └── Tasks │ └── SecurityAlertCheckTask.php ├── phpcs.xml.dist ├── phpunit.xml.dist ├── .editorconfig ├── templates └── SecurityAlertSummary.ss ├── _config └── extensions.yml ├── .upgrade.yml ├── lang └── en.yml ├── license.md ├── composer.json └── README.md /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! Create an issue, explaining a bug or proposal. Submit pull requests if you feel brave. 4 | -------------------------------------------------------------------------------- /client/dist/css/securityalerts.css: -------------------------------------------------------------------------------- 1 | .site-summary__security-alerts { 2 | margin: 0; 3 | } 4 | 5 | .package-summary__security-alerts ul { 6 | margin-bottom: 0; 7 | } 8 | 9 | .security-alerts__list { 10 | margin-top: 8px; 11 | display: none; 12 | } 13 | 14 | .security-alerts__toggler { 15 | cursor: pointer; 16 | } 17 | -------------------------------------------------------------------------------- /src/Extensions/SecurityAlertExtension.php: -------------------------------------------------------------------------------- 1 | Package::class 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeSniffer ruleset for SilverStripe coding conventions. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | tests/ 4 | 5 | 6 | 7 | src/ 8 | 9 | tests/ 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*{.yml,.scss,.css,.js,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /templates/SecurityAlertSummary.ss: -------------------------------------------------------------------------------- 1 |

2 | <%t SecurityAlertSummary.TITLE "Security alert" %>
3 | <% if $Count > 1 %> 4 | <%t SecurityAlertSummary.NOTICE_MANY "Notices have been issued for {count} of your modules. Review and updating is recommended." count=$Count %> 5 | <% else %> 6 | <%t SecurityAlertSummary.NOTICE_ONE "A notice has been issued for {count} of your modules. Review and updating is recommended." count=$Count %> 7 | <% end_if %> 8 |

9 | -------------------------------------------------------------------------------- /_config/extensions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: packagerelations 3 | after: '#updatecheckerextensions' 4 | Only: 5 | moduleexists: bringyourownideas/silverstripe-maintenance 6 | --- 7 | BringYourOwnIdeas\Maintenance\Model\Package: 8 | extensions: 9 | - BringYourOwnIdeas\SecurityChecker\Extensions\PackageSecurityExtension 10 | BringYourOwnIdeas\SecurityChecker\Models\SecurityAlert: 11 | extensions: 12 | - BringYourOwnIdeas\SecurityChecker\Extensions\SecurityAlertExtension 13 | BringYourOwnIdeas\Maintenance\Reports\SiteSummary: 14 | extensions: 15 | - BringYourOwnIdeas\SecurityChecker\Extensions\SiteSummaryExtension 16 | -------------------------------------------------------------------------------- /src/Models/SecurityAlert.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 16 | 'Version' => 'Varchar(255)', 17 | 'Title' => 'Varchar(255)', 18 | 'ExternalLink' => 'Varchar(255)', 19 | 'Identifier' => 'Varchar(255)', 20 | ); 21 | 22 | private static $summary_fields = array( 23 | 'PackageName', 24 | 'Version', 25 | 'Title', 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.upgrade.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | PackageSecurityExtension: BringYourOwnIdeas\SecurityChecker\Extensions\PackageSecurityExtension 3 | SecurityAlertExtension: BringYourOwnIdeas\SecurityChecker\Extensions\SecurityAlertExtension 4 | SiteSummaryExtension: BringYourOwnIdeas\SecurityChecker\Extensions\SiteSummaryExtension 5 | SecurityAlertCheckJob: BringYourOwnIdeas\SecurityChecker\Jobs\SecurityAlertCheckJob 6 | SecurityAlert: BringYourOwnIdeas\SecurityChecker\Models\SecurityAlert 7 | SecurityAlertCheckTask: BringYourOwnIdeas\SecurityChecker\Tasks\SecurityAlertCheckTask 8 | SecurityAlertCheckJobTest: BringYourOwnIdeas\SecurityChecker\Tests\SecurityAlertCheckJobTest 9 | SecurityAlertCheckTaskTest: BringYourOwnIdeas\SecurityChecker\Tests\SecurityAlertCheckTaskTest 10 | -------------------------------------------------------------------------------- /lang/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | BringYourOwnIdeas\SecurityChecker\Extensions\PackageSecurityExtension: 3 | BADGE_SECURITY: 'RISK: Security' 4 | BringYourOwnIdeas\SecurityChecker\Jobs\SecurityAlertCheckJob: 5 | Title: 'Check if any composer managed modules have known security vulnerabilities.' 6 | BringYourOwnIdeas\SecurityChecker\Models\SecurityAlert: 7 | PLURALNAME: 'Security Alerts' 8 | PLURALS: 9 | one: 'A Security Alert' 10 | other: '{count} Security Alerts' 11 | SINGULARNAME: 'Security Alert' 12 | SecurityAlertSummary: 13 | NOTICE_MANY: 'Notices have been issued for {count} of your modules. Review and updating is recommended.' 14 | NOTICE_ONE: 'A notice has been issued for {count} of your modules. Review and updating is recommended.' 15 | TITLE: 'Security alert' 16 | -------------------------------------------------------------------------------- /client/dist/javascript/summaryalerts.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.entwine('ss', function($) { 3 | $('.package-summary__security-alerts').entwine({ 4 | IsShown: false, 5 | toggleSecurityNotices: function() { 6 | if (this.getIsShown()) { 7 | this.hideSecurityNotices(); 8 | } else { 9 | this.showSecurityNotices(); 10 | } 11 | }, 12 | showSecurityNotices: function() { 13 | this.getAlertList().show(); 14 | this.setIsShown(true); 15 | }, 16 | hideSecurityNotices: function() { 17 | this.getAlertList().hide(); 18 | this.setIsShown(false); 19 | }, 20 | getAlertList: function() { 21 | return this.children('.security-alerts__list'); 22 | } 23 | }); 24 | $('.security-alerts__toggler').entwine({ 25 | onclick: function(event) { 26 | this.parent() 27 | .nextAll('.package-summary__security-alerts') 28 | .toggleSecurityNotices(); 29 | event.preventDefault(); 30 | } 31 | }); 32 | }); 33 | })(jQuery) 34 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Peter Thaleikis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /src/Extensions/SiteSummaryExtension.php: -------------------------------------------------------------------------------- 1 | owner->sourceRecords()->filter('SecurityAlerts.ID:GreaterThan', 0); 36 | 37 | if ($securityWarnings->exists()) { 38 | $alerts['SecurityAlerts'] = $securityWarnings->renderWith('SecurityAlertSummary'); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bringyourownideas/silverstripe-composer-security-checker", 3 | "description": "Provides information if your SilverStripe application uses dependencies with known vulnerabilities.", 4 | "type": "silverstripe-vendormodule", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "silverstripe", 8 | "environment", 9 | "composer", 10 | "updates", 11 | "vulnerabilities", 12 | "security" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Peter Thaleikis", 17 | "homepage": "https://thaleikis.de" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.6.0", 22 | "silverstripe/framework": "^4", 23 | "sensiolabs/security-checker": "^5 || ^6", 24 | "symbiote/silverstripe-queuedjobs": "^4" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^5.7", 28 | "squizlabs/php_codesniffer": "^3" 29 | }, 30 | "prefer-stable": true, 31 | "minimum-stability": "dev", 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "2.x-dev" 35 | }, 36 | "expose": [ 37 | "client/dist" 38 | ] 39 | }, 40 | "replace": { 41 | "spekulatius/silverstripe-composer-security-checker": "self.version" 42 | }, 43 | "suggest": { 44 | "bringyourownideas/silverstripe-maintenance": "Display advisory information via Site Summary Report", 45 | "roave/security-advisories": "Raises a conflict on attempt to install a vulnerable module" 46 | }, 47 | "support": { 48 | "issues": "https://github.com/bringyourownideas/silverstripe-composer-security-checker/issues" 49 | }, 50 | "homepage": "https://github.com/bringyourownideas/silverstripe-composer-security-checker" 51 | } 52 | -------------------------------------------------------------------------------- /src/Jobs/SecurityAlertCheckJob.php: -------------------------------------------------------------------------------- 1 | '%$' . SecurityAlertCheckTask::class, 19 | ]; 20 | 21 | /** 22 | * @var SecurityAlertCheckTask 23 | */ 24 | protected $checkTask; 25 | 26 | /** 27 | * @return SecurityAlertCheckTask 28 | */ 29 | public function getCheckTask() 30 | { 31 | return $this->checkTask; 32 | } 33 | 34 | /** 35 | * @param SecurityAlertCheckTask $checkTask 36 | * @return SecurityAlertCheckJob 37 | */ 38 | public function setCheckTask(SecurityAlertCheckTask $checkTask) 39 | { 40 | $this->checkTask = $checkTask; 41 | return $this; 42 | } 43 | 44 | public function getTitle() 45 | { 46 | return _t( 47 | __CLASS__ . '.Title', 48 | 'Check if any composer managed modules have known security vulnerabilities.' 49 | ); 50 | } 51 | 52 | public function getJobType() 53 | { 54 | $this->totalSteps = 1; 55 | 56 | return QueuedJob::QUEUED; 57 | } 58 | 59 | public function process() 60 | { 61 | // run the task 62 | $task = $this->getCheckTask(); 63 | $task->run(null); 64 | 65 | // mark job as completed 66 | $this->isComplete = true; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Extensions/PackageSecurityExtension.php: -------------------------------------------------------------------------------- 1 | SecurityAlert::class 14 | ]; 15 | 16 | private static $summary_fields = [ 17 | 'listSecurityAlertIdentifiers' => 'Security alerts', 18 | ]; 19 | 20 | /** 21 | * Simply returns a comma separated list of active SecurityAlert Identifiers for this record. 22 | * Used in CSV exports as a type of brief indication (as opposed to full info) 23 | */ 24 | public function listSecurityAlertIdentifiers() 25 | { 26 | $alerts = $this->owner->SecurityAlerts()->Column('Identifier'); 27 | return $alerts ? implode(', ', $alerts) : null; 28 | } 29 | 30 | /** 31 | * updates the badges that render as part of the screen targeted 32 | * summary for this Package 33 | * 34 | * @param ArrayList $badges 35 | */ 36 | public function updateBadges($badges) 37 | { 38 | if ($this->owner->SecurityAlerts()->exists()) { 39 | $badges->push(ArrayData::create([ 40 | 'Title' => _t(__CLASS__ . '.BADGE_SECURITY', 'RISK: Security'), 41 | 'Type' => 'warning security-alerts__toggler', 42 | ])); 43 | } 44 | } 45 | 46 | /** 47 | * Adds security alert notifications into the schema 48 | * 49 | * @param array &$schema 50 | * @return string 51 | */ 52 | public function updateDataSchema(&$schema) 53 | { 54 | // The keys from the SecurityAlert model that we need in the React component 55 | $keysToPass = ['Identifier', 'ExternalLink']; 56 | 57 | $alerts = []; 58 | foreach ($this->owner->SecurityAlerts()->toNestedArray() as $alert) { 59 | $alerts[] = array_intersect_key($alert, array_flip($keysToPass)); 60 | } 61 | 62 | $schema['securityAlerts'] = $alerts; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Security Checker 2 | 3 | **WARNING**: As of January 2021, this module no longer works because the underlying service has been shut down (see [announcement](https://github.com/sensiolabs/security-checker) and [discussion](https://github.com/bringyourownideas/silverstripe-composer-security-checker/issues/57)). 4 | 5 | __NOTE__: This module is no longer commercially supported in Silverstripe CMS 5 and it does not provide a CMS5-compatible version. 6 | 7 | [![Build Status](https://api.travis-ci.org/bringyourownideas/silverstripe-composer-security-checker.svg?branch=master)](https://travis-ci.org/bringyourownideas/silverstripe-composer-security-checker) 8 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bringyourownideas/silverstripe-composer-security-checker/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/bringyourownideas/silverstripe-composer-security-checker/?branch=master) 9 | [![codecov](https://codecov.io/gh/bringyourownideas/silverstripe-composer-security-checker/branch/master/graph/badge.svg)](https://codecov.io/gh/bringyourownideas/silverstripe-composer-security-checker) 10 | 11 | Adds a task which runs a check if any of the dependencies has known security vulnerabilities. It uses the 12 | [SensioLabs Security Check Web service](http://security.sensiolabs.org/) and the [Security Advisories Database](https://github.com/FriendsOfPHP/security-advisories). 13 | 14 | BSD 3-clause [License](https://github.com/bringyourownideas/silverstripe-composer-security-checker/blob/master/license.md) 15 | 16 | ## Requirements 17 | 18 | * SilverStripe Framework ^4 19 | * SilverStripe QueuedJobs ^4 20 | 21 | ### Suggested Module 22 | 23 | This module will automatically amend the SiteSummary report provided by the [SilverStripe Maintenance module](https://github.com/bringyourownideas/silverstripe-maintenance), adding alerts if security updates are present for installed modules. 24 | 25 | ## Installation 26 | 27 | The following installation commands includes schedulding a queuedjob to populate the data. Run the following command to install this package as a development dependency: 28 | 29 | ``` 30 | composer require bringyourownideas/silverstripe-composer-security-checker 2.x-dev 31 | 32 | vendor/bin/sake dev/build 33 | vendor/bin/sake dev/tasks/ProcessJobQueueTask 34 | ``` 35 | 36 | ## Usage 37 | 38 | The information gets updated via a BuildTask, which in turn can be run via a queuedjob. 39 | You will need to set up a scheduled process (e.g. `cron`) to run either the buildtask directly, or the task to process the queuedjobs queue in order to refresh the information. 40 | 41 | Use the information is stored in the `SecurityAlert` object, and can be consumed as needed. Please be careful how you expose this information. If the SilverStripe Maintenance module is present, a relationship will be connected between `Package` and `SecurityAlert`. 42 | 43 | ## Documentation 44 | 45 | Please see the user guide section of the [SilverStripe Maintenance module](https://github.com/bringyourownideas/silverstripe-maintenance/tree/1/docs/en/userguide). 46 | -------------------------------------------------------------------------------- /src/Tasks/SecurityAlertCheckTask.php: -------------------------------------------------------------------------------- 1 | '%$' . SecurityChecker::class, 29 | ]; 30 | 31 | protected $title = 'Composer security checker'; 32 | 33 | protected $description = 34 | 'Checks if any modules managed through composer have known security vulnerabilities at the used version.'; 35 | 36 | /** 37 | * @return SecurityChecker 38 | */ 39 | public function getSecurityChecker() 40 | { 41 | return $this->securityChecker; 42 | } 43 | 44 | /** 45 | * @param SecurityChecker $securityChecker 46 | * @return $this 47 | */ 48 | public function setSecurityChecker(SecurityChecker $securityChecker) 49 | { 50 | $this->securityChecker = $securityChecker; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Most SilverStripe issued alerts are _not_ assiged CVEs. 56 | * However they have their own identifier in the form of a 57 | * prefix to the title - we can use this instead of a CVE ID. 58 | * 59 | * @param string $cve 60 | * @param string $title 61 | * 62 | * @return string 63 | */ 64 | protected function discernIdentifier($cve, $title) 65 | { 66 | $identifier = $cve; 67 | if (!$identifier || $identifier === '~') { 68 | $identifier = explode(':', $title); 69 | $identifier = array_shift($identifier); 70 | } 71 | $this->extend('updateIdentifier', $identifier, $cve, $title); 72 | return $identifier; 73 | } 74 | 75 | public function run($request) 76 | { 77 | // to keep the list up to date while removing resolved issues we keep all of found issues 78 | $validEntries = array(); 79 | 80 | // use the security checker of 81 | $checker = $this->getSecurityChecker(); 82 | $result = $checker->check(BASE_PATH . DIRECTORY_SEPARATOR . 'composer.lock'); 83 | $alerts = json_decode((string) $result, true); 84 | 85 | // go through all alerts for packages - each can contain multiple issues 86 | foreach ($alerts as $package => $packageDetails) { 87 | // go through each individual known security issue 88 | foreach ($packageDetails['advisories'] as $details) { 89 | $identifier = $this->discernIdentifier($details['cve'], $details['title']); 90 | $vulnerability = null; 91 | 92 | // check if this vulnerability is already known 93 | $existingVulns = SecurityAlert::get()->filter(array( 94 | 'PackageName' => $package, 95 | 'Version' => $packageDetails['version'], 96 | 'Identifier' => $identifier, 97 | )); 98 | 99 | // Is this vulnerability known? No, lets add it. 100 | if (!$existingVulns->Count()) { 101 | $vulnerability = SecurityAlert::create(); 102 | $vulnerability->PackageName = $package; 103 | $vulnerability->Version = $packageDetails['version']; 104 | $vulnerability->Title = $details['title']; 105 | $vulnerability->ExternalLink = $details['link']; 106 | $vulnerability->Identifier = $identifier; 107 | 108 | $vulnerability->write(); 109 | 110 | // add the new entries to the list of valid entries 111 | $validEntries[] = $vulnerability->ID; 112 | } else { 113 | // add existing vulnerabilities (probably just 1) to the list of valid entries 114 | $validEntries = array_merge($validEntries, $existingVulns->column('ID')); 115 | } 116 | 117 | // Relate this vulnerability to an existing Package, if the 118 | // bringyourownideas/silverstripe-maintenance module is installed 119 | if ($vulnerability && $vulnerability->hasExtension(SecurityAlertExtension::class) 120 | && class_exists(Package::class) 121 | && !$vulnerability->PackageRecordID 122 | && $packageRecord = Package::get()->find('Name', $package) 123 | ) { 124 | $vulnerability->PackageRecordID = $packageRecord->ID; 125 | $vulnerability->write(); 126 | } 127 | } 128 | } 129 | 130 | // remove all entries which are resolved (no longer $validEntries) 131 | $tableName = DataObjectSchema::create()->tableName(SecurityAlert::class); 132 | $removeOldSecurityAlerts = SQLDelete::create("\"$tableName\""); 133 | if (empty($validEntries)) { 134 | // There were no SecurityAlerts listed for our installation - so flush any old data 135 | $removeOldSecurityAlerts->execute(); 136 | } else { 137 | $removable = SecurityAlert::get()->exclude(array('ID' => $validEntries)); 138 | // Be careful not to remove all SecurityAlerts on the case that every entry is valid 139 | if ($removable->exists()) { 140 | // SQLConditionalExpression does not support IN() syntax via addWhere 141 | // so we have to build this up manually 142 | $convertIDsToQuestionMarks = function ($id) { 143 | return '?'; 144 | }; 145 | $queryArgs = $removable->column('ID'); 146 | $paramPlaceholders = implode(',', array_map($convertIDsToQuestionMarks, $queryArgs)); 147 | 148 | $removeOldSecurityAlerts = $removeOldSecurityAlerts->addWhere([ 149 | '"ID" IN(' . $paramPlaceholders . ')' => $queryArgs 150 | ]); 151 | $removeOldSecurityAlerts->execute(); 152 | } 153 | } 154 | 155 | // notify that the task finished. 156 | $this->output('The task finished running. You can find the updated information in the database now.'); 157 | } 158 | 159 | /** 160 | * prints a message during the run of the task 161 | * 162 | * @param string $text 163 | */ 164 | protected function output($text) 165 | { 166 | echo Director::is_cli() ? $text . PHP_EOL : "

$text

\n"; 167 | } 168 | } 169 | --------------------------------------------------------------------------------