├── .gitignore ├── .php_cs.dist ├── LICENSE ├── README.md ├── SensioLabs └── Security │ ├── Command │ └── SecurityCheckerCommand.php │ ├── Crawler.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── HttpException.php │ └── RuntimeException.php │ ├── Result.php │ └── SecurityChecker.php ├── box.json ├── composer.json └── security-checker /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@Symfony' => true, 6 | '@Symfony:risky' => true, 7 | 'array_syntax' => ['syntax' => 'short'], 8 | 'php_unit_fqcn_annotation' => true, 9 | 'no_unreachable_default_argument_value' => false, 10 | 'braces' => ['allow_single_line_closure' => true], 11 | 'heredoc_to_nowdoc' => false, 12 | 'ordered_imports' => true, 13 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 14 | 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'], 15 | ]) 16 | ->setRiskyAllowed(true) 17 | ->setFinder(PhpCsFixer\Finder::create()->in(__DIR__)) 18 | ; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2020 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SensioLabs Security Checker 2 | =========================== 3 | 4 | **WARNING**: Don't use this piece of software anymore as the underlying web 5 | service will stop working at the end of January 2021. Instead, use the 6 | [Open-Source CLI tool][1] that does the same locally, or use the [Symfony 7 | CLI][2] tool. 8 | 9 | [1]: https://github.com/fabpot/local-php-security-checker 10 | [2]: https://symfony.com/download 11 | -------------------------------------------------------------------------------- /SensioLabs/Security/Command/SecurityCheckerCommand.php: -------------------------------------------------------------------------------- 1 | checker = $checker; 31 | 32 | parent::__construct(); 33 | } 34 | 35 | /** 36 | * @see Command 37 | */ 38 | protected function configure() 39 | { 40 | $this 41 | ->setName('security:check') 42 | ->setDefinition([ 43 | new InputArgument('lockfile', InputArgument::OPTIONAL, 'The path to the composer.lock file', 'composer.lock'), 44 | new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format', 'ansi'), 45 | new InputOption('end-point', '', InputOption::VALUE_REQUIRED, 'The security checker server URL'), 46 | new InputOption('timeout', '', InputOption::VALUE_REQUIRED, 'The HTTP timeout in seconds'), 47 | new InputOption('token', '', InputOption::VALUE_REQUIRED, 'The server token', ''), 48 | ]) 49 | ->setDescription('Checks security issues in your project dependencies') 50 | ->setHelp(<<%command.name% command looks for security issues in the 52 | project dependencies: 53 | 54 | php %command.full_name% 55 | 56 | You can also pass the path to a composer.lock file as an argument: 57 | 58 | php %command.full_name% /path/to/composer.lock 59 | 60 | By default, the command displays the result in plain text, but you can also 61 | configure it to output JSON instead by using the --format option: 62 | 63 | php %command.full_name% /path/to/composer.lock --format=json 64 | EOF 65 | ); 66 | } 67 | 68 | /** 69 | * @see Command 70 | * @see SecurityChecker 71 | */ 72 | protected function execute(InputInterface $input, OutputInterface $output) 73 | { 74 | if ($endPoint = $input->getOption('end-point')) { 75 | $this->checker->getCrawler()->setEndPoint($endPoint); 76 | } 77 | 78 | if ($timeout = $input->getOption('timeout')) { 79 | $this->checker->getCrawler()->setTimeout($timeout); 80 | } 81 | 82 | if ($token = $input->getOption('token')) { 83 | $this->checker->getCrawler()->setToken($token); 84 | } 85 | 86 | $format = $input->getOption('format'); 87 | if ($input->getOption('no-ansi') && 'ansi' === $format) { 88 | $format = 'text'; 89 | } 90 | 91 | try { 92 | $result = $this->checker->check($input->getArgument('lockfile'), $format); 93 | } catch (ExceptionInterface $e) { 94 | $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($e->getMessage(), 'error', true)); 95 | 96 | return 1; 97 | } 98 | 99 | $output->writeln((string) $result); 100 | 101 | if (\count($result) > 0) { 102 | return 1; 103 | } 104 | 105 | return 0; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /SensioLabs/Security/Crawler.php: -------------------------------------------------------------------------------- 1 | timeout = $timeout; 33 | } 34 | 35 | public function setEndPoint($endPoint) 36 | { 37 | $this->endPoint = $endPoint; 38 | } 39 | 40 | public function setToken($token) 41 | { 42 | $this->addHeader('Authorization', 'Token '.$token); 43 | } 44 | 45 | /** 46 | * Adds a global header that will be sent with all requests to the server. 47 | */ 48 | public function addHeader($key, $value) 49 | { 50 | $this->headers[] = $key.': '.$value; 51 | } 52 | 53 | /** 54 | * Checks a Composer lock file. 55 | * 56 | * @param string $lock The path to the composer.lock file or a string able to be opened via file_get_contents 57 | * @param string $format The format of the result 58 | * @param array $headers An array of headers to add for this specific HTTP request 59 | * 60 | * @return Result 61 | */ 62 | public function check($lock, $format = 'json', array $headers = []) 63 | { 64 | $response = $this->doCheck($lock, $format, $headers); 65 | 66 | $headers = $response->getHeaders(); 67 | if (!isset($headers['x-alerts']) || !ctype_digit($count = $headers['x-alerts'][0])) { 68 | throw new RuntimeException('The web service did not return alerts count.'); 69 | } 70 | 71 | return new Result((int) $count, $response->getContent(), $format); 72 | } 73 | 74 | /** 75 | * @return array An array where the first element is a headers string and second one the response body 76 | */ 77 | private function doCheck($lock, $format = 'json', array $contextualHeaders = []): ResponseInterface 78 | { 79 | $client = HttpClient::create(); 80 | $body = new FormDataPart([ 81 | 'lock' => new DataPart($this->getLockContents($lock), 'composer.lock'), 82 | ]); 83 | $headers = array_merge($this->headers, [ 84 | 'Accept' => $this->getContentType($format), 85 | 'User-Agent' => sprintf('SecurityChecker-CLI/%s FGC PHP', SecurityChecker::VERSION), 86 | ], $body->getPreparedHeaders()->toArray()); 87 | 88 | $response = $client->request('POST', $this->endPoint, [ 89 | 'headers' => $headers, 90 | 'timeout' => $this->timeout, 91 | 'body' => $body->bodyToIterable(), 92 | ]); 93 | 94 | if (400 === $statusCode = $response->getStatusCode()) { 95 | $data = trim($response->getContent(false)); 96 | if ('json' === $format) { 97 | $data = json_decode($data, true)['message'] ?? $data; 98 | } 99 | 100 | throw new HttpException(sprintf('%s (HTTP %s).', $data, $statusCode), $statusCode); 101 | } 102 | 103 | if (200 !== $statusCode) { 104 | throw new HttpException(sprintf('The web service failed for an unknown reason (HTTP %s).', $statusCode), $statusCode); 105 | } 106 | 107 | return $response; 108 | } 109 | 110 | private function getContentType($format) 111 | { 112 | static $formats = [ 113 | 'text' => 'text/plain', 114 | 'simple' => 'text/plain', 115 | 'markdown' => 'text/markdown', 116 | 'yaml' => 'text/yaml', 117 | 'json' => 'application/json', 118 | 'ansi' => 'text/plain+ansi', 119 | ]; 120 | 121 | return isset($formats[$format]) ? $formats[$format] : 'text'; 122 | } 123 | 124 | private function getLockContents($lock) 125 | { 126 | $contents = json_decode(file_get_contents($lock), true); 127 | $hash = isset($contents['content-hash']) ? $contents['content-hash'] : (isset($contents['hash']) ? $contents['hash'] : ''); 128 | $packages = ['content-hash' => $hash, 'packages' => [], 'packages-dev' => []]; 129 | foreach (['packages', 'packages-dev'] as $key) { 130 | if (!\is_array($contents[$key])) { 131 | continue; 132 | } 133 | foreach ($contents[$key] as $package) { 134 | $data = [ 135 | 'name' => $package['name'], 136 | 'version' => $package['version'], 137 | ]; 138 | if (isset($package['time']) && false !== strpos($package['version'], 'dev')) { 139 | $data['time'] = $package['time']; 140 | } 141 | $packages[$key][] = $data; 142 | } 143 | } 144 | 145 | return json_encode($packages); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /SensioLabs/Security/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | count = $count; 23 | $this->vulnerabilities = $vulnerabilities; 24 | $this->format = $format; 25 | } 26 | 27 | public function getFormat() 28 | { 29 | return $this->format; 30 | } 31 | 32 | public function __toString() 33 | { 34 | return $this->vulnerabilities; 35 | } 36 | 37 | public function count() 38 | { 39 | return $this->count; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SensioLabs/Security/SecurityChecker.php: -------------------------------------------------------------------------------- 1 | crawler = null === $crawler ? new Crawler() : $crawler; 25 | } 26 | 27 | /** 28 | * Checks a composer.lock file. 29 | * 30 | * @param string $lock The path to the composer.lock file 31 | * @param string $format The format of the result 32 | * @param array $headers An array of headers to add for this specific HTTP request 33 | * 34 | * @return Result 35 | * 36 | * @throws RuntimeException When the lock file does not exist 37 | * @throws RuntimeException When the certificate can not be copied 38 | */ 39 | public function check($lock, $format = 'json', array $headers = []) 40 | { 41 | if (0 !== strpos($lock, 'data://text/plain;base64,')) { 42 | if (is_dir($lock) && file_exists($lock.'/composer.lock')) { 43 | $lock = $lock.'/composer.lock'; 44 | } elseif (preg_match('/composer\.json$/', $lock)) { 45 | $lock = str_replace('composer.json', 'composer.lock', $lock); 46 | } 47 | 48 | if (!is_file($lock)) { 49 | throw new RuntimeException('Lock file does not exist.'); 50 | } 51 | } 52 | 53 | return $this->crawler->check($lock, $format, $headers); 54 | } 55 | 56 | /** 57 | * @internal 58 | * 59 | * @return Crawler 60 | */ 61 | public function getCrawler() 62 | { 63 | return $this->crawler; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "output": "security-checker.phar", 3 | "chmod": "0755", 4 | "compactors": [ 5 | "Herrera\\Box\\Compactor\\Php" 6 | ], 7 | "extract": false, 8 | "main": "security-checker", 9 | "files": [ 10 | "LICENSE" 11 | ], 12 | "finder": [ 13 | { 14 | "name": "*.*", 15 | "exclude": ["Tests"], 16 | "in": "vendor" 17 | }, 18 | { 19 | "name": ["*.*"], 20 | "in": "SensioLabs" 21 | } 22 | ], 23 | "stub": true, 24 | "web": false 25 | } 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sensiolabs/security-checker", 3 | "description": "A security checker for your composer.lock", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Fabien Potencier", 8 | "email": "fabien.potencier@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=7.1.3", 13 | "symfony/console": "^2.8|^3.4|^4.2|^5.0", 14 | "symfony/http-client": "^4.3|^5.0", 15 | "symfony/mime": "^4.3|^5.0", 16 | "symfony/polyfill-ctype": "^1.11" 17 | }, 18 | "bin": ["security-checker"], 19 | "autoload": { 20 | "psr-4": { "SensioLabs\\Security\\": "SensioLabs/Security" } 21 | }, 22 | "extra": { 23 | "branch-alias": { 24 | "dev-master": "6.0-dev" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /security-checker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new SecurityCheckerCommand(new SecurityChecker(new Crawler()))); 33 | $console->run(); 34 | --------------------------------------------------------------------------------