diff --git a/SensioLabs/Security/Command/SecurityCheckerCommand.php b/SensioLabs/Security/Command/SecurityCheckerCommand.php index 245a018..f4f40bc 100644 --- a/SensioLabs/Security/Command/SecurityCheckerCommand.php +++ b/SensioLabs/Security/Command/SecurityCheckerCommand.php @@ -3,7 +3,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/SensioLabs/Security/Crawler/BaseCrawler.php b/SensioLabs/Security/Crawler/BaseCrawler.php new file mode 100644 index 0000000..785ed07 --- /dev/null +++ b/SensioLabs/Security/Crawler/BaseCrawler.php @@ -0,0 +1,83 @@ +timeout = $timeout; + } + + /** + * {@inheritdoc} + */ + public function setEndPoint($endPoint) + { + $this->endPoint = $endPoint; + } + + /** + * {@inheritdoc} + */ + public function check($lock) + { + $certFile = $this->getCertFile(); + + try { + list($headers, $body) = $this->doCheck($lock, $certFile); + } catch (\Exception $e) { + if (__DIR__.'/../Resources/security.sensiolabs.org.crt' !== $certFile) { + unlink($certFile); + } + + throw $e; + } + + if (!(preg_match('/X-Alerts: (\d+)/', $headers, $matches) || 2 == count($matches))) { + throw new RuntimeException('The web service did not return alerts count.'); + } + + return array(intval($matches[1]), json_decode($body, true)); + } + + /** + * @return array An array where the first element is a headers string and second one the response body + */ + abstract protected function doCheck($lock, $certFile); + + private function getCertFile() + { + $certFile = __DIR__.'/../Resources/security.sensiolabs.org.crt'; + if ('phar://' !== substr(__FILE__, 0, 7)) { + return $certFile; + } + + $tmpFile = tempnam(sys_get_temp_dir(), 'sls'); + if (false === @copy($certFile, $tmpFile)) { + throw new RuntimeException(sprintf('Unable to copy the certificate in "%s".', $tmpFile)); + } + + return $tmpFile; + } +} diff --git a/SensioLabs/Security/Crawler/CrawlerInterface.php b/SensioLabs/Security/Crawler/CrawlerInterface.php new file mode 100644 index 0000000..14d0b67 --- /dev/null +++ b/SensioLabs/Security/Crawler/CrawlerInterface.php @@ -0,0 +1,31 @@ + PHP_VERSION_ID >= 50500 ? new \CurlFile($lock) : '@'.$lock); + + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_URL, $this->endPoint); + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/json')); + curl_setopt($curl, CURLOPT_POSTFIELDS, $postFields); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->timeout); + curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_MAXREDIRS, 3); + curl_setopt($curl, CURLOPT_FAILONERROR, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($curl, CURLOPT_CAINFO, $certFile); + + $response = curl_exec($curl); + + if (false === $response) { + $error = curl_error($curl); + curl_close($curl); + + throw new RuntimeException(sprintf('An error occurred: %s.', $error)); + } + + $headersSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $headers = substr($response, 0, $headersSize); + $body = substr($response, $headersSize); + + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if (400 == $statusCode) { + $data = json_decode($body, true); + $error = $data['error']; + + throw new RuntimeException($error); + } + + if (200 != $statusCode) { + throw new RuntimeException(sprintf('The web service failed for an unknown reason (HTTP %s).', $statusCode)); + } + + return array($headers, $body); + } +} diff --git a/SensioLabs/Security/Crawler/DefaultCrawler.php b/SensioLabs/Security/Crawler/DefaultCrawler.php new file mode 100644 index 0000000..af06a9b --- /dev/null +++ b/SensioLabs/Security/Crawler/DefaultCrawler.php @@ -0,0 +1,49 @@ +crawler = function_exists('curl_init') ? new CurlCrawler() : new FileGetContentsCrawler(); + } + + /** + * {@inheritdoc} + */ + public function check($lock) + { + return $this->crawler->check($lock); + } + + /** + * {@inheritdoc} + */ + public function setTimeout($timeout) + { + $this->crawler->setTimeout($timeout); + } + + /** + * {@inheritdoc} + */ + public function setEndPoint($endPoint) + { + $this->crawler->setEndPoint($endPoint); + } +} diff --git a/SensioLabs/Security/Crawler/FileGetContentsCrawler.php b/SensioLabs/Security/Crawler/FileGetContentsCrawler.php new file mode 100644 index 0000000..c77921b --- /dev/null +++ b/SensioLabs/Security/Crawler/FileGetContentsCrawler.php @@ -0,0 +1,78 @@ + array( + 'method' => 'POST', + 'header' => "Content-Type: multipart/form-data; boundary=$boundary\r\nAccept: application/json", + 'content' => "--$boundary\r\nContent-Disposition: form-data; name=\"lock\"; filename=\"$lock\"\r\nContent-Type: application/octet-stream\r\n\r\n".file_get_contents($lock)."\r\n--$boundary\r\n--\r\n", + 'ignore_errors' => true, + 'follow_location' => true, + 'max_redirects' => 3, + 'timeout' => $this->timeout, + ), + 'ssl' => array( + 'cafile' => $certFile, + 'verify_peer' => 1, + 'verify_host' => 2, + ), + )); + + $level = error_reporting(0); + $body = file_get_contents($this->endPoint, 0, $context); + error_reporting($level); + if (false === $body) { + $error = error_get_last(); + + throw new RuntimeException(sprintf('An error occurred: %s.', $error['message'])); + } + + // status code + if (!preg_match('{HTTP/\d\.\d (\d+) }i', $http_response_header[0], $match)) { + throw new RuntimeException('An unknown error occurred.'); + } + + $statusCode = $match[1]; + if (400 == $statusCode) { + $data = json_decode($body, true); + + throw new RuntimeException($data['error']); + } + + if (200 != $statusCode) { + throw new RuntimeException(sprintf('The web service failed for an unknown reason (HTTP %s).', $statusCode)); + } + + $headers = ''; + foreach ($http_response_header as $header) { + if (false !== strpos($header, 'X-Alerts: ')) { + $headers = $header; + } + } + + return array($headers, $body); + } +} diff --git a/SensioLabs/Security/Formatters/JsonFormatter.php b/SensioLabs/Security/Formatters/JsonFormatter.php index 1e64a3d..a67bf01 100644 --- a/SensioLabs/Security/Formatters/JsonFormatter.php +++ b/SensioLabs/Security/Formatters/JsonFormatter.php @@ -3,7 +3,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/SensioLabs/Security/Formatters/SimpleFormatter.php b/SensioLabs/Security/Formatters/SimpleFormatter.php index 662f5a9..9f3fa46 100644 --- a/SensioLabs/Security/Formatters/SimpleFormatter.php +++ b/SensioLabs/Security/Formatters/SimpleFormatter.php @@ -3,7 +3,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/SensioLabs/Security/Formatters/TextFormatter.php b/SensioLabs/Security/Formatters/TextFormatter.php index 4821806..32cb9ae 100644 --- a/SensioLabs/Security/Formatters/TextFormatter.php +++ b/SensioLabs/Security/Formatters/TextFormatter.php @@ -3,7 +3,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -67,8 +67,8 @@ public function displayResults(OutputInterface $output, $lockFilePath, array $vu } } - $output->writeln(" This checker can only detect vulnerabilities that are referenced"); - $output->writeln(" Disclaimer in the SensioLabs security advisories database. Execute this"); + $output->writeln(' This checker can only detect vulnerabilities that are referenced'); + $output->writeln(' Disclaimer in the SensioLabs security advisories database. Execute this'); $output->writeln(" command regularly to check the newly discovered vulnerabilities.\n"); } } diff --git a/SensioLabs/Security/SecurityChecker.php b/SensioLabs/Security/SecurityChecker.php index e3b0399..5fc741a 100644 --- a/SensioLabs/Security/SecurityChecker.php +++ b/SensioLabs/Security/SecurityChecker.php @@ -3,7 +3,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,21 +12,17 @@ namespace SensioLabs\Security; use SensioLabs\Security\Exception\RuntimeException; +use SensioLabs\Security\Crawler\CrawlerInterface; +use SensioLabs\Security\Crawler\DefaultCrawler; class SecurityChecker { - private $endPoint = 'https://security.sensiolabs.org/check_lock'; private $vulnerabilityCount; - private $timeout = 20; + private $crawler; - public function setTimeout($timeout) + public function __construct(CrawlerInterface $crawler = null) { - $this->timeout = $timeout; - } - - public function setEndPoint($endPoint) - { - $this->endPoint = $endPoint; + $this->crawler = null === $crawler ? new DefaultCrawler() : $crawler; } /** @@ -37,19 +33,10 @@ public function setEndPoint($endPoint) * @return array An array of vulnerabilities * * @throws RuntimeException When the lock file does not exist - * @throws RuntimeException When curl does not work or is unavailable * @throws RuntimeException When the certificate can not be copied */ public function check($lock) { - if (!function_exists('curl_init')) { - throw new RuntimeException('Curl is required to use this command.'); - } - - if (false === $curl = curl_init()) { - throw new RuntimeException('Unable to create a new curl handle.'); - } - if (is_dir($lock) && file_exists($lock.'/composer.lock')) { $lock = $lock.'/composer.lock'; } elseif (preg_match('/composer\.json$/', $lock)) { @@ -60,85 +47,9 @@ public function check($lock) throw new RuntimeException('Lock file does not exist.'); } - $postFields = array('lock' => '@'.$lock); - - if (PHP_VERSION_ID >= 50500) { - $postFields['lock'] = new \CurlFile($lock); - } - - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HEADER, true); - curl_setopt($curl, CURLOPT_URL, $this->endPoint); - curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/json')); - curl_setopt($curl, CURLOPT_POSTFIELDS, $postFields); - curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $this->timeout); - curl_setopt($curl, CURLOPT_TIMEOUT, 10); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_MAXREDIRS, 3); - curl_setopt($curl, CURLOPT_FAILONERROR, false); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); - - $cert = __DIR__.'/Resources/security.sensiolabs.org.crt'; - $tmpFile = null; - if ('phar://' === substr(__FILE__, 0, 7)) { - $tmpFile = tempnam(sys_get_temp_dir(), 'sls'); - if (false === @copy($cert, $cert = $tmpFile)) { - throw new RuntimeException(sprintf('Unable to copy the certificate in "%s".', $tmpFile)); - } - } - curl_setopt($curl, CURLOPT_CAINFO, $cert); - - $response = curl_exec($curl); - - if (false === $response) { - $error = curl_error($curl); - curl_close($curl); - if ($tmpFile) { - unlink($tmpFile); - } - - throw new RuntimeException(sprintf('An error occurred: %s.', $error)); - } - - $headersSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); - $headers = substr($response, 0, $headersSize); - $body = substr($response, $headersSize); - - $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - if (400 == $statusCode) { - $data = json_decode($body, true); - $error = $data['error']; - - curl_close($curl); - if ($tmpFile) { - unlink($tmpFile); - } - - throw new RuntimeException($error); - } - - if (200 != $statusCode) { - curl_close($curl); - if ($tmpFile) { - unlink($tmpFile); - } - - throw new RuntimeException(sprintf('The web service failed for an unknown reason (HTTP %s).', $statusCode)); - } - - curl_close($curl); - if ($tmpFile) { - unlink($tmpFile); - } - - if (!(preg_match('/X-Alerts: (\d+)/', $headers, $matches) || 2 == count($matches))) { - throw new RuntimeException('The web service did not return alerts count.'); - } - - $this->vulnerabilityCount = intval($matches[1]); + list($this->vulnerabilityCount, $vulnerabilities) = $this->crawler->check($lock); - return json_decode($body, true); + return $vulnerabilities; } public function getLastVulnerabilityCount() diff --git a/composer.json b/composer.json index a278c95..b3cd466 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,6 @@ } ], "require": { - "ext-curl": "*", "symfony/console": "~2.0" }, "bin": ["security-checker"], @@ -18,7 +17,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } } } diff --git a/security-checker b/security-checker index 0f8192c..d95dadd 100755 --- a/security-checker +++ b/security-checker @@ -4,7 +4,7 @@ /* * This file is part of the SensioLabs Security Checker. * - * (c) 2013 Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -27,6 +27,6 @@ use Symfony\Component\Console\Application; use SensioLabs\Security\Command\SecurityCheckerCommand; use SensioLabs\Security\SecurityChecker; -$console = new Application('SensioLabs Security Checker', '2.0'); +$console = new Application('SensioLabs Security Checker', '3.0'); $console->add(new SecurityCheckerCommand(new SecurityChecker())); $console->run();