From 762f379f9a3f8e29735b4ef21b5ee68c6b6be6b6 Mon Sep 17 00:00:00 2001 From: Daniel Bannert Date: Fri, 7 Sep 2018 00:19:21 +0200 Subject: [PATCH] finished #63 --- src/Automatic/Automatic.php | 87 +- src/Automatic/Container.php | 7 +- src/Automatic/Contract/Container.php | 10 + src/Automatic/Contract/Crawler.php | 32 - src/Automatic/Contract/Security/Formatter.php | 18 + src/Automatic/Security/Audit.php | 211 +++-- .../Security/Command/AuditCommand.php | 78 +- .../Command/Formatter/JsonFormatter.php | 17 + .../Command/Formatter/SimpleFormatter.php | 38 + .../Command/Formatter/TextFormatter.php | 27 + src/Automatic/Security/Downloader.php | 38 +- src/Common/Package.php | 8 +- tests/Automatic/AutomaticTest.php | 18 +- .../Fixture/composer_1.7.1_composer.lock | 860 ++++++++++++++++++ tests/Automatic/OperationsResolverTest.php | 16 +- tests/Automatic/Security/AuditTest.php | 196 +++- tests/Automatic/Security/DownloaderTest.php | 29 +- tests/Common/PackageTest.php | 20 +- 18 files changed, 1557 insertions(+), 153 deletions(-) delete mode 100644 src/Automatic/Contract/Crawler.php create mode 100644 src/Automatic/Contract/Security/Formatter.php create mode 100644 src/Automatic/Security/Command/Formatter/JsonFormatter.php create mode 100644 src/Automatic/Security/Command/Formatter/SimpleFormatter.php create mode 100644 src/Automatic/Security/Command/Formatter/TextFormatter.php create mode 100644 tests/Automatic/Fixture/composer_1.7.1_composer.lock diff --git a/src/Automatic/Automatic.php b/src/Automatic/Automatic.php index 54637b3f..9c1f529b 100644 --- a/src/Automatic/Automatic.php +++ b/src/Automatic/Automatic.php @@ -48,7 +48,9 @@ use Narrowspark\Automatic\Prefetcher\ParallelDownloader; use Narrowspark\Automatic\Prefetcher\Prefetcher; use Narrowspark\Automatic\Prefetcher\TruncatedComposerRepository; +use Narrowspark\Automatic\Security\Audit; use Narrowspark\Automatic\Security\Command\AuditCommandProvider; +use Narrowspark\Automatic\Security\Downloader; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; @@ -106,10 +108,26 @@ class Automatic implements PluginInterface, EventSubscriberInterface, Capable private $operations = []; /** - * @var array + * List of package messages. + * + * @var string[] */ private $postInstallOutput = ['']; + /** + * The SecurityAdvisories database. + * + * @var array + */ + private $securityAdvisories; + + /** + * Found package vulnerabilities. + * + * @var array[] + */ + private $foundVulnerabilities = []; + /** * Get the Container instance. * @@ -136,8 +154,8 @@ public static function getSubscribedEvents(): array InstallerEvents::POST_DEPENDENCIES_SOLVING => [['populateFilesCacheDir', \PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_INSTALL => [['populateFilesCacheDir', ~\PHP_INT_MAX]], PackageEvents::PRE_PACKAGE_UPDATE => [['populateFilesCacheDir', ~\PHP_INT_MAX]], - PackageEvents::POST_PACKAGE_INSTALL => 'record', - PackageEvents::POST_PACKAGE_UPDATE => 'record', + PackageEvents::POST_PACKAGE_INSTALL => [['record'], ['audit']], + PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['audit']], PackageEvents::POST_PACKAGE_UNINSTALL => 'record', PluginEvents::PRE_FILE_DOWNLOAD => 'onFileDownload', ScriptEvents::POST_INSTALL_CMD => 'onPostInstall', @@ -180,6 +198,16 @@ public function activate(Composer $composer, IOInterface $io): void $this->container = new Container($composer, $io); + $extra = $this->container->get('composer-extra'); + $downloader = new Downloader(); + + if (isset($extra[Util::COMPOSER_EXTRA_KEY]['audit']['timeout'])) { + $downloader->setTimeout($extra[Util::COMPOSER_EXTRA_KEY]['audit']['timeout']); + } + $this->container->set(Audit::class, static function (Container $container) use ($downloader) { + return new Audit($container->get('vendor-dir'), $downloader); + }); + /** @var \Composer\Installer\InstallationManager $installationManager */ $installationManager = $this->container->get(Composer::class)->getInstallationManager(); $installationManager->addInstaller($this->container->get(ConfiguratorInstaller::class)); @@ -188,7 +216,7 @@ public function activate(Composer $composer, IOInterface $io): void /** @var \Narrowspark\Automatic\LegacyTagsManager $tagsManager */ $tagsManager = $this->container->get(LegacyTagsManager::class); - $this->configureLegacyTagsManager($io, $tagsManager); + $this->configureLegacyTagsManager($io, $tagsManager, $extra); $composer->setRepositoryManager($this->extendRepositoryManager($composer, $io, $tagsManager)); @@ -206,6 +234,8 @@ public function activate(Composer $composer, IOInterface $io): void $container->get(InputInterface::class) ); }); + + $this->securityAdvisories = $this->container->get(Audit::class)->getSecurityAdvisories($this->container->get(IOInterface::class)); } /** @@ -219,7 +249,18 @@ public function postInstallOut(Event $event): void { $event->stopPropagation(); - $this->container->get(IOInterface::class)->write($this->postInstallOutput); + /** @var \Composer\IO\IOInterface $io */ + $io = $this->container->get(IOInterface::class); + + $io->write($this->postInstallOutput); + + $count = \count(\array_filter($this->foundVulnerabilities)); + + if ($count !== 0) { + $io->write('[!] Audit Security Report: ' . \sprintf('%s vulnerabilit%s found - run "composer audit" for more information', $count, $count === 1 ? 'y' : 'ies')); + } else { + $io->write('[+] Audit Security Report: No known vulnerabilities found'); + } } /** @@ -244,6 +285,36 @@ public function record(PackageEvent $event): void } } + /** + * Audit composer package operations. + * + * @param \Composer\Installer\PackageEvent $event + * + * @return void + */ + public function audit(PackageEvent $event): void + { + $operation = $event->getOperation(); + + if ($operation instanceof UninstallOperation) { + return; + } + + if ($operation instanceof UpdateOperation) { + $composerPackage = $operation->getTargetPackage(); + } else { + $composerPackage = $operation->getPackage(); + } + + [$vulnerabilities, $messages] = $this->container->get(Audit::class)->checkPackage( + $composerPackage->getName(), + $composerPackage->getVersion(), + $this->securityAdvisories + ); + + $this->foundVulnerabilities += $vulnerabilities; + } + /** * Execute on composer create project event. * @@ -303,7 +374,7 @@ public function runSkeletonGenerator(Event $event): void $lock->read(); if ($lock->has(SkeletonInstaller::LOCK_KEY)) { - $this->operations = []; + $this->operations = $this->foundVulnerabilities = []; $skeletonGenerator = new SkeletonGenerator( $this->container->get(IOInterface::class), @@ -877,12 +948,12 @@ private static function getComposerVersion(): string * * @param \Composer\IO\IOInterface $io * @param \Narrowspark\Automatic\LegacyTagsManager $tagsManager + * @param array $extra * * @return void */ - private function configureLegacyTagsManager(IOInterface $io, LegacyTagsManager $tagsManager): void + private function configureLegacyTagsManager(IOInterface $io, LegacyTagsManager $tagsManager, array $extra): void { - $extra = $this->container->get('composer-extra'); $envRequire = \getenv('AUTOMATIC_REQUIRE'); if ($envRequire !== false) { diff --git a/src/Automatic/Container.php b/src/Automatic/Container.php index 49b3d161..487591dd 100644 --- a/src/Automatic/Container.php +++ b/src/Automatic/Container.php @@ -164,12 +164,7 @@ public function __construct(Composer $composer, IOInterface $io) } /** - * Set a new entry to the container. - * - * @param string $id - * @param callable $callback - * - * @return void + * {@inheritdoc} */ public function set(string $id, callable $callback): void { diff --git a/src/Automatic/Contract/Container.php b/src/Automatic/Contract/Container.php index 09db85aa..6c005655 100644 --- a/src/Automatic/Contract/Container.php +++ b/src/Automatic/Contract/Container.php @@ -13,6 +13,16 @@ interface Container */ public function get(string $id); + /** + * Set a new entry to the container. + * + * @param string $id + * @param callable $callback + * + * @return void + */ + public function set(string $id, callable $callback): void; + /** * Returns all container entries. * diff --git a/src/Automatic/Contract/Crawler.php b/src/Automatic/Contract/Crawler.php deleted file mode 100644 index 514840e3..00000000 --- a/src/Automatic/Contract/Crawler.php +++ /dev/null @@ -1,32 +0,0 @@ -composerVendorPath = $composerVendorPath; + $this->downloader = $downloader; $this->versionParser = new VersionParser(); - $this->downloader = new Downloader(); $this->filesystem = new Filesystem(); } - public function checkPackage(string $name, string $version): array + /** + * Checks a package on name and version. + * + * @param string $name + * @param string $version + * @param array $securityAdvisories + * + * @return array[] + */ + public function checkPackage(string $name, string $version, array $securityAdvisories): array { + if (! isset($securityAdvisories[$name])) { + return []; + } + $package = new Package($name, $this->versionParser->normalize($version), $version); + + [$messages, $vulnerabilities] = $this->checkPackageAgainstSecurityAdvisories($securityAdvisories, $package); + + \ksort($vulnerabilities); + + return [$vulnerabilities, $messages]; } + /** + * Checks a composer lock file. + * + * @param string $lock The path to the composer.lock file + * + * @throws \Narrowspark\Automatic\Common\Contract\Exception\RuntimeException When the lock file does not exist + * + * @return array[] + */ public function checkLock(string $lock): array { + if (! \file_exists($lock)) { + throw new RuntimeException('Lock file does not exist.'); + } + $lockContents = $this->getLockContents($lock); /** @var \Composer\Package\Package[] $packages */ @@ -81,73 +126,26 @@ public function checkLock(string $lock): array $messages = []; foreach ($packages as $package) { - $name = $package->getName(); - - if (! isset($securityAdvisories[$name])) { + if (! isset($securityAdvisories[$package->getName()])) { continue; } - foreach ($securityAdvisories[$name] as $key => $advisoryData) { - foreach ($advisoryData['branches'] as $name => $branch) { - if (! isset($branch['versions'])) { - $messages[$name][] = \sprintf('Key "versions" is not set for branch "%s".', $key); - } elseif (! \is_array($branch['versions'])) { - $messages[$name][] = \sprintf('"versions" is expected to be an array for branch "%s".', $key); - } else { - $constraints = []; - - foreach ($branch['versions'] as $version) { - $op = null; - - foreach (Constraint::getSupportedOperators() as $operators) { - if (\mb_strpos($version, $operators) === 0) { - $op = $operators; - - break; - } - } - - if (null === $op) { - $messages[$name][] = \sprintf('Version "%s" does not contain a supported operator.', $version); - - continue; - } - - $constraints[] = new Constraint($op, \mb_substr($version, \mb_strlen($op))); - } - - $affectedConstraint = new MultiConstraint($constraints); - $affectedPackage = $affectedConstraint->matches(new Constraint('==', $package->getVersion())); - - if ($affectedPackage) { - $composerPackage = \mb_substr($advisoryData['reference'], 11); - - $vulnerabilities[$composerPackage] = $vulnerabilities[$composerPackage] ?? [ - 'version' => $package->getPrettyVersion(), - 'advisories' => [], - ]; - - $vulnerabilities[$composerPackage]['advisories'][$key] = [ - 'title' => $advisoryData['title'] ?? '', - 'link' => $advisoryData['link'] ?? '', - 'cve' => $advisoryData['cve'] ?? '', - ]; - } - } - } - } - } - - if (\count($messages) !== 0) { - \var_dump($messages); + [$messages, $vulnerabilities] = $this->checkPackageAgainstSecurityAdvisories($securityAdvisories, $package, $messages, $vulnerabilities); } \ksort($vulnerabilities); - return [\count($vulnerabilities), $vulnerabilities]; + return [$vulnerabilities, $messages]; } - private function getSecurityAdvisories(): array + /** + * Get the news security advisories from narrowspark/security-advisories. + * + * @param null|\Composer\IO\IOInterface $io + * + * @return array + */ + public function getSecurityAdvisories(?IOInterface $io = null): array { if (! \extension_loaded('curl')) { $sha = $this->downloader->downloadWithComposer(self::SECURITY_ADVISORIES_BASE_URL . self::SECURITY_ADVISORIES_SHA); @@ -168,10 +166,14 @@ private function getSecurityAdvisories(): array $oldSha = \file_get_contents($securityAdvisoriesShaPath); if ($oldSha === $sha) { - return \json_decode(\file_get_contents($securityAdvisoriesPath), true); + return \json_decode((string) \file_get_contents($securityAdvisoriesPath), true); } } + if ($io !== null) { + $io->writeError('Downloading the Security Advisories database'); + } + if (! \extension_loaded('curl')) { $securityAdvisories = $this->downloader->downloadWithComposer(self::SECURITY_ADVISORIES_BASE_URL . self::SECURITY_ADVISORIES); } else { @@ -181,7 +183,7 @@ private function getSecurityAdvisories(): array $this->filesystem->dumpFile($securityAdvisoriesShaPath, $sha); $this->filesystem->dumpFile($securityAdvisoriesPath, $securityAdvisories); - return \json_decode(\file_get_contents($securityAdvisoriesPath), true); + return \json_decode((string) \file_get_contents($securityAdvisoriesPath), true); } /** @@ -191,7 +193,7 @@ private function getSecurityAdvisories(): array */ private function getLockContents(string $lock): array { - $contents = \json_decode(\file_get_contents($lock), true); + $contents = \json_decode((string) \file_get_contents($lock), true); $packages = ['packages' => [], 'packages-dev' => []]; foreach (['packages', 'packages-dev'] as $key) { @@ -215,4 +217,81 @@ private function getLockContents(string $lock): array return $packages; } + + /** + * Check if a package has some security issues. + * + * @param array $securityAdvisories + * @param \Composer\Package\Package $package + * @param array $messages + * @param array $vulnerabilities + * + * @return array[] + */ + private function checkPackageAgainstSecurityAdvisories( + array $securityAdvisories, + Package $package, + array $messages = [], + array $vulnerabilities = [] + ): array { + $name = $package->getName(); + + foreach ($securityAdvisories[$name] as $key => $advisoryData) { + if (! \is_array($advisoryData['branches'])) { + $messages[$name][] = '"branches" is expected to be an array.'; + + continue; + } + + foreach ($advisoryData['branches'] as $name => $branch) { + if (! isset($branch['versions'])) { + $messages[$name][] = \sprintf('Key [versions] is not set for branch [%s].', $key); + } elseif (! \is_array($branch['versions'])) { + $messages[$name][] = \sprintf('Key [versions] is expected to be an array for branch [%s].', $key); + } else { + $constraints = []; + + foreach ($branch['versions'] as $version) { + $op = null; + + foreach (Constraint::getSupportedOperators() as $operators) { + if (\mb_strpos($version, $operators) === 0) { + $op = $operators; + + break; + } + } + + if (null === $op) { + $messages[$name][] = \sprintf('Version [%s] does not contain a supported operator.', $version); + + continue; + } + + $constraints[] = new Constraint($op, \mb_substr($version, \mb_strlen($op))); + } + + $affectedConstraint = new MultiConstraint($constraints); + $affectedPackage = $affectedConstraint->matches(new Constraint('==', $package->getVersion())); + + if ($affectedPackage) { + $composerPackage = \mb_substr($advisoryData['reference'], 11); + + $vulnerabilities[$composerPackage] = $vulnerabilities[$composerPackage] ?? [ + 'version' => $package->getPrettyVersion(), + 'advisories' => [], + ]; + + $vulnerabilities[$composerPackage]['advisories'][$key] = [ + 'title' => $advisoryData['title'] ?? '', + 'link' => $advisoryData['link'] ?? '', + 'cve' => $advisoryData['cve'] ?? '', + ]; + } + } + } + } + + return [$messages, $vulnerabilities]; + } } diff --git a/src/Automatic/Security/Command/AuditCommand.php b/src/Automatic/Security/Command/AuditCommand.php index 928f0137..881d040e 100644 --- a/src/Automatic/Security/Command/AuditCommand.php +++ b/src/Automatic/Security/Command/AuditCommand.php @@ -3,9 +3,18 @@ namespace Narrowspark\Automatic\Security\Command; use Composer\Command\BaseCommand; +use Composer\Factory; +use Narrowspark\Automatic\Common\Contract\Exception\RuntimeException; +use Narrowspark\Automatic\Common\Util; +use Narrowspark\Automatic\Security\Audit; +use Narrowspark\Automatic\Security\Command\Formatter\JsonFormatter; +use Narrowspark\Automatic\Security\Command\Formatter\SimpleFormatter; +use Narrowspark\Automatic\Security\Command\Formatter\TextFormatter; +use Narrowspark\Automatic\Security\Downloader; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; class AuditCommand extends BaseCommand { @@ -17,8 +26,7 @@ protected function configure(): void $this ->setName('audit') ->setDefinition([ - new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format', 'text'), - new InputOption('endpoint', '', InputOption::VALUE_REQUIRED, 'The security checker server URL'), + new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format', 'txt'), new InputOption('timeout', '', InputOption::VALUE_REQUIRED, 'The HTTP timeout in seconds'), ]) ->setDescription('Checks security issues in your project dependencies') @@ -34,7 +42,71 @@ protected function configure(): void /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { + $composer = $this->getComposer(); + $downloader = new Downloader(); + $extra = $composer->getPackage()->getExtra(); + + if (isset($extra[Util::COMPOSER_EXTRA_KEY]['audit']['timeout'])) { + $downloader->setTimeout($extra[Util::COMPOSER_EXTRA_KEY]['audit']['timeout']); + } elseif (($timeout = $input->getOption('timeout')) !== null) { + $downloader->setTimeout($timeout); + } + + $audit = new Audit(\rtrim($composer->getConfig()->get('vendor-dir'), '/'), $downloader); + $composerFile = \str_replace('json', 'lock', Factory::getComposerFile()); + + $output = new SymfonyStyle($input, $output); + + $output->writeln('=== Audit Security Report ==='); + $output->comment('This checker can only detect vulnerabilities that are referenced in the SensioLabs security advisories database.'); + + try { + [$vulnerabilities, $messages] = $audit->checkLock($composerFile); + } catch (RuntimeException $exception) { + /** @var \Symfony\Component\Console\Helper\FormatterHelper $formatter */ + $formatter = $this->getHelperSet()->get('formatter'); + + $output->writeln($formatter->formatBlock($exception->getMessage(), 'error', true)); + + return 1; + } + + if (\count($messages) !== 0) { + $output->note('Please report this found messages to https://github.com/narrowspark/security-advisories.'); + + foreach ($messages as $key => $message) { + $output->writeln($key . ': ' . $message); + } + } + + $count = \count($vulnerabilities); + + if (\count($vulnerabilities) !== 0) { + switch ($input->getOption('format')) { + case 'json': + $formatter = new JsonFormatter(); + + break; + case 'simple': + $formatter = new SimpleFormatter(); + + break; + case 'txt': + default: + $formatter = new TextFormatter(); + } + + $formatter->displayResults($output, $vulnerabilities); + $output->writeln('[!] ' . \sprintf('%s vulnerabilit%s found - ', $count, $count === 1 ? 'y' : 'ies') . + 'We recommend you to check the related security advisories and upgrade these dependencies.'); + + return 1; + } + + $output->writeln('[+] No known vulnerabilities found'); + + return 0; } } diff --git a/src/Automatic/Security/Command/Formatter/JsonFormatter.php b/src/Automatic/Security/Command/Formatter/JsonFormatter.php new file mode 100644 index 00000000..c76b25a7 --- /dev/null +++ b/src/Automatic/Security/Command/Formatter/JsonFormatter.php @@ -0,0 +1,17 @@ +writeln((string) \json_encode($vulnerabilities, \JSON_PRETTY_PRINT)); + } +} diff --git a/src/Automatic/Security/Command/Formatter/SimpleFormatter.php b/src/Automatic/Security/Command/Formatter/SimpleFormatter.php new file mode 100644 index 00000000..a08e974a --- /dev/null +++ b/src/Automatic/Security/Command/Formatter/SimpleFormatter.php @@ -0,0 +1,38 @@ + $issues) { + $dependencyFullName = $dependency . ' (' . $issues['version'] . ')'; + $output->writeln('' . $dependencyFullName . "\n" . \str_repeat('-', \mb_strlen($dependencyFullName)) . "\n"); + + foreach ($issues['advisories'] as $issue => $details) { + $output->write(' * '); + + if ($details['cve']) { + $output->write('' . $details['cve'] . ': '); + } + + $output->writeln($details['title']); + + if ('' !== $details['link']) { + $output->writeln(' ' . $details['link']); + } + + $output->writeln(''); + } + } + } + } +} diff --git a/src/Automatic/Security/Command/Formatter/TextFormatter.php b/src/Automatic/Security/Command/Formatter/TextFormatter.php new file mode 100644 index 00000000..6f3225a4 --- /dev/null +++ b/src/Automatic/Security/Command/Formatter/TextFormatter.php @@ -0,0 +1,27 @@ + $issues) { + $output->section(\sprintf('%s (%s)', $dependency, $issues['version'])); + + $details = \array_map(function ($value) { + return \sprintf("%s: %s\n %s", $value['cve'] ?: '(no CVE ID)', $value['title'], $value['link']); + }, $issues['advisories']); + + $output->listing($details); + } + } + } +} diff --git a/src/Automatic/Security/Downloader.php b/src/Automatic/Security/Downloader.php index 642d761d..f75676ec 100644 --- a/src/Automatic/Security/Downloader.php +++ b/src/Automatic/Security/Downloader.php @@ -12,8 +12,32 @@ */ final class Downloader { - private $timeout = 5; + /** + * The HTTP timeout in seconds. + * + * @var int + */ + private $timeout = 20; + + /** + * Sets the HTTP timeout in seconds. + * + * @param int $timeout The HTTP timeout in seconds + * + * @return void + */ + public function setTimeout(int $timeout): void + { + $this->timeout = $timeout; + } + /** + * Download a file from a url with composer's StreamContextFactory class. + * + * @param string $url + * + * @return string + */ public function downloadWithComposer(string $url): string { $opts = [ @@ -52,7 +76,7 @@ public function downloadWithComposer(string $url): string } // status code - if (! \preg_match('{HTTP/\d\.\d (\d+) }i', $http_response_header[0], $match)) { + if ((bool) \preg_match('{HTTP/\d\.\d (\d+) }i', $http_response_header[0], $match) === false) { throw new RuntimeException('An unknown error occurred.'); } @@ -62,6 +86,8 @@ public function downloadWithComposer(string $url): string } /** + * Download a file with the curl extension. + * * @param string $url * * @throws \Narrowspark\Automatic\Common\Contract\Exception\RuntimeException @@ -78,7 +104,7 @@ public function downloadWithCurl(string $url): string \curl_setopt($curl, \CURLOPT_HTTPHEADER, ['Accept: application/text']); \curl_setopt($curl, \CURLOPT_CONNECTTIMEOUT, $this->timeout); \curl_setopt($curl, \CURLOPT_TIMEOUT, 10); - \curl_setopt($curl, \CURLOPT_FOLLOWLOCATION, \ini_get('open_basedir') ? 0 : 1); + \curl_setopt($curl, \CURLOPT_FOLLOWLOCATION, \is_string(\ini_get('open_basedir')) ? 0 : 1); \curl_setopt($curl, \CURLOPT_MAXREDIRS, 3); \curl_setopt($curl, \CURLOPT_FAILONERROR, false); \curl_setopt($curl, \CURLOPT_SSL_VERIFYPEER, 1); @@ -103,7 +129,7 @@ public function downloadWithCurl(string $url): string throw new RuntimeException(\sprintf('An error occurred: %s.', $error)); } - $body = \mb_substr($response, \curl_getinfo($curl, \CURLINFO_HEADER_SIZE)); + $body = \mb_substr((string) $response, \curl_getinfo($curl, \CURLINFO_HEADER_SIZE)); $statusCode = (int) \curl_getinfo($curl, \CURLINFO_HTTP_CODE); \curl_close($curl); @@ -120,11 +146,11 @@ private function getUserAgent(): string { return \sprintf( 'Narrowspark-Automatic/%s (%s; %s; %s%s)', - Automatic::VERSION === '@package_version@' ? 'source' : Automatic::VERSION, + Automatic::VERSION, \function_exists('php_uname') ? \php_uname('s') : 'Unknown', \function_exists('php_uname') ? \php_uname('r') : 'Unknown', 'PHP ' . \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION . '.' . \PHP_RELEASE_VERSION, - \getenv('CI') ? '; CI' : '' + \getenv('CI') !== false ? '; CI' : '' ); } diff --git a/src/Common/Package.php b/src/Common/Package.php index 91f8642b..b6e0e26e 100644 --- a/src/Common/Package.php +++ b/src/Common/Package.php @@ -28,7 +28,7 @@ final class Package implements PackageContract private $parentName; /** - * The package version. + * The package pretty version. * * @var null|string */ @@ -290,9 +290,9 @@ public static function createFromLock(string $name, array $packageData): Package $package = new static($name, $packageData['version']); - foreach ($packageData as $key => $date) { - if ($date !== null && isset($keyToFunctionMappers[$key])) { - $package->{$keyToFunctionMappers[$key]}($date); + foreach ($packageData as $key => $data) { + if ($data !== null && isset($keyToFunctionMappers[$key])) { + $package->{$keyToFunctionMappers[$key]}($data); } } diff --git a/tests/Automatic/AutomaticTest.php b/tests/Automatic/AutomaticTest.php index b97b1644..40b28349 100644 --- a/tests/Automatic/AutomaticTest.php +++ b/tests/Automatic/AutomaticTest.php @@ -74,7 +74,7 @@ protected function tearDown(): void \putenv('COMPOSER_CACHE_DIR='); \putenv('COMPOSER_CACHE_DIR'); - (new Filesystem())->remove($this->composerCachePath); + (new Filesystem())->remove([$this->composerCachePath, __DIR__ . \DIRECTORY_SEPARATOR . 'narrowspark']); } public function testGetSubscribedEvents(): void @@ -139,6 +139,9 @@ public function testActivate(): void $this->ioMock->shouldReceive('isInteractive') ->once() ->andReturn(true); + $this->ioMock->shouldReceive('writeError') + ->once() + ->with('Downloading the Security Advisories database'); $this->automatic->activate($this->composerMock, $this->ioMock); @@ -218,6 +221,8 @@ public function testRecordWithUninstallRecord(): void public function testRecordWithInstallRecord(): void { + \putenv('COMPOSER_VENDOR_DIR=' . __DIR__); + $automatic = new Automatic(); $packageEventMock = $this->mock(PackageEvent::class); @@ -269,10 +274,15 @@ public function isInteractive(): bool ); $automatic->record($packageEventMock); + + \putenv('COMPOSER_VENDOR_DIR='); + \putenv('COMPOSER_VENDOR_DIR'); } public function testRecordWithInstallRecordAndAutomaticPackage(): void { + \putenv('COMPOSER_VENDOR_DIR=' . __DIR__); + $automatic = new Automatic(); $packageEventMock = $this->mock(PackageEvent::class); @@ -344,6 +354,9 @@ public function isInteractive(): bool $automatic->record($packageEventMock); $automatic->record($automaticPackageEventMock); + + \putenv('COMPOSER_VENDOR_DIR='); + \putenv('COMPOSER_VENDOR_DIR'); } public function testExecuteAutoScripts(): void @@ -420,6 +433,9 @@ public function testPostInstallOut(): void $this->ioMock->shouldReceive('write') ->once() ->with(['']); + $this->ioMock->shouldReceive('write') + ->once() + ->with('[+] Audit Security Report: No known vulnerabilities found'); $containerMock = $this->mock(ContainerContract::class); $containerMock->shouldReceive('get') diff --git a/tests/Automatic/Fixture/composer_1.7.1_composer.lock b/tests/Automatic/Fixture/composer_1.7.1_composer.lock new file mode 100644 index 00000000..278fa805 --- /dev/null +++ b/tests/Automatic/Fixture/composer_1.7.1_composer.lock @@ -0,0 +1,860 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f96b35b2deaec90bb30cceddb419accc", + "packages": [ + { + "name": "composer/ca-bundle", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/46afded9720f40b9dc63542af4e3e43a1177acb0", + "reference": "46afded9720f40b9dc63542af4e3e43a1177acb0", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2018-08-08T08:57:40+00:00" + }, + { + "name": "composer/composer", + "version": "1.7.2", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/576aab9b5abb2ed11a1c52353a759363216a4ad2", + "reference": "576aab9b5abb2ed11a1c52353a759363216a4ad2", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/semver": "^1.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^1.1", + "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0" + }, + "conflict": { + "symfony/console": "2.8.38" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7", + "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "time": "2018-08-16T14:57:12+00:00" + }, + { + "name": "composer/semver", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2016-08-30T16:08:34+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/cb17687e9f936acd7e7245ad3890f953770dec1b", + "reference": "cb17687e9f936acd7e7245ad3890f953770dec1b", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "time": "2018-04-30T10:33:04+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/b8e9745fb9b06ea6664d8872c4505fb16df4611c", + "reference": "b8e9745fb9b06ea6664d8872c4505fb16df4611c", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2018-08-31T19:07:57+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.7", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2018-02-14T22:26:30+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2018-01-24T12:46:19+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phra" + ], + "time": "2015-10-13T18:44:15+00:00" + }, + { + "name": "symfony/console", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ca80b8ced97cf07390078b29773dc384c39eee1f", + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0" + }, + "suggest": { + "psr/log-implementation": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:24:31+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-08-18T16:52:46+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068", + "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2018-07-26T11:24:31+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/process", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/86cdb930a6a855b0ab35fb60c1504cb36184f843", + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2018-08-03T11:13:38+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/tests/Automatic/OperationsResolverTest.php b/tests/Automatic/OperationsResolverTest.php index cad3623d..f2a7c8a2 100644 --- a/tests/Automatic/OperationsResolverTest.php +++ b/tests/Automatic/OperationsResolverTest.php @@ -234,14 +234,14 @@ public function testResolveWithRemoveAndLock(): void ->once() ->with(Automatic::LOCK_PACKAGES, $name) ->andReturn([ - 'pretty-name' => $name, - 'version' => '1.0-dev', - 'parent' => null, - 'is-dev' => false, - 'url' => null, - 'operation' => 'install', - 'type' => 'library', - 'requires' => [ + 'pretty-name' => $name, + 'version' => '1.0-dev', + 'parent' => null, + 'is-dev' => false, + 'url' => null, + 'operation' => 'install', + 'type' => 'library', + 'requires' => [ 'viserio/contract', ], 'autoload' => [], diff --git a/tests/Automatic/Security/AuditTest.php b/tests/Automatic/Security/AuditTest.php index 6af458f0..c79fe71d 100644 --- a/tests/Automatic/Security/AuditTest.php +++ b/tests/Automatic/Security/AuditTest.php @@ -2,7 +2,10 @@ declare(strict_types=1); namespace Narrowspark\Automatic\Test\Security; +use Composer\Util\Filesystem; +use Narrowspark\Automatic\Common\Contract\Exception\RuntimeException; use Narrowspark\Automatic\Security\Audit; +use Narrowspark\Automatic\Security\Downloader; use PHPUnit\Framework\TestCase; /** @@ -22,7 +25,7 @@ protected function setUp() { parent::setUp(); - $this->audit = new Audit(__DIR__); + $this->audit = new Audit(__DIR__, new Downloader()); } /** @@ -32,19 +35,200 @@ protected function tearDown() { parent::tearDown(); - $dir = __DIR__ . \DIRECTORY_SEPARATOR . 'narrowspark' . \DIRECTORY_SEPARATOR . 'automatic'; + (new Filesystem())->remove(__DIR__ . \DIRECTORY_SEPARATOR . 'narrowspark'); + } + + public function testCheckPackageWithSymfony(): void + { + [$vulnerabilities, $messages] = $this->audit->checkPackage('symfony/symfony', 'v2.5.2', $this->audit->getSecurityAdvisories()); + + $this->assertSymfonySecurity(\count($vulnerabilities), $vulnerabilities); + static::assertCount(0, $messages); + } + + public function testCheckPackageWithSymfonyAndCache(): void + { + [$vulnerabilities, $messages] = $this->audit->checkPackage('symfony/symfony', 'v2.5.2', $this->audit->getSecurityAdvisories()); - @\unlink($dir . \DIRECTORY_SEPARATOR . 'security-advisories.json'); - @\unlink($dir . \DIRECTORY_SEPARATOR . 'security-advisories-sha'); - @\rmdir($dir); + $this->assertSymfonySecurity(\count($vulnerabilities), $vulnerabilities); + static::assertCount(0, $messages); + + [$vulnerabilities, $messages] = $this->audit->checkPackage('symfony/symfony', 'v2.5.2', $this->audit->getSecurityAdvisories()); + + $this->assertSymfonySecurity(\count($vulnerabilities), $vulnerabilities); } public function testCheckLockWithSymfony252(): void { - [$vulnerabilitiesCount, $vulnerabilities] = $this->audit->checkLock( + [$vulnerabilities, $messages] = $this->audit->checkLock( \dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'symfony_2.5.2_composer.lock' ); + $this->assertSymfonySecurity(\count($vulnerabilities), $vulnerabilities); + static::assertCount(0, $messages); + } + + public function testCheckLockWithComposer171(): void + { + [$vulnerabilities, $messages] = $this->audit->checkLock( + \dirname(__DIR__) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'composer_1.7.1_composer.lock' + ); + + static::assertCount(0, $vulnerabilities); + static::assertCount(0, $messages); + } + + public function testCheckLockThrowsException(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Lock file does not exist.'); + + $this->audit->checkLock(''); + } + + public function testCheckPackageWithCustomPackage(): void + { + static::assertCount(0, $this->audit->checkPackage('fooa/fooa', 'v2.5.2', $this->audit->getSecurityAdvisories())); + } + + /** + * @param int $vulnerabilitiesCount + * @param array $vulnerabilities + * + * @return void + */ + private function assertSymfonySecurity(int $vulnerabilitiesCount, array $vulnerabilities): void + { static::assertSame(1, $vulnerabilitiesCount); + static::assertEquals( + [ + 'symfony/symfony' => [ + 'version' => 'v2.5.2', + 'advisories' => [ + 'CVE-2016-4423' => [ + 'title' => 'CVE-2016-4423: Large username storage in session', + 'link' => 'https://symfony.com/cve-2016-4423', + 'cve' => 'CVE-2016-4423', + ], + 'CVE-2017-16654' => [ + 'title' => 'CVE-2017-16654: Intl bundle readers breaking out of paths', + 'link' => 'https://symfony.com/cve-2017-16654', + 'cve' => 'CVE-2017-16654', + ], + 'CVE-2017-16652' => [ + 'title' => 'CVE-2017-16652: Open redirect vulnerability on security handlers', + 'link' => 'https://symfony.com/cve-2017-16652', + 'cve' => 'CVE-2017-16652', + ], + 'CVE-2014-6061' => [ + 'title' => 'Security issue when parsing the Authorization header', + 'link' => 'https://symfony.com/cve-2014-6061', + 'cve' => 'CVE-2014-6061', + ], + 'CVE-2015-4050' => [ + 'title' => 'CVE-2015-4050: ESI unauthorized access', + 'link' => 'https://symfony.com/cve-2015-4050', + 'cve' => 'CVE-2015-4050', + ], + 'CVE-2018-11408' => [ + 'title' => 'CVE-2018-11408: Open redirect vulnerability on security handlers', + 'link' => 'https://symfony.com/cve-2018-11408', + 'cve' => 'CVE-2018-11408', + ], + 'CVE-2018-11385' => [ + 'title' => 'CVE-2018-11385: Session Fixation Issue for Guard Authentication', + 'link' => 'https://symfony.com/cve-2018-11385', + 'cve' => 'CVE-2018-11385', + ], + 'CVE-2014-4931' => [ + 'title' => 'Code injection in the way Symfony implements translation caching in FrameworkBundle', + 'link' => 'https://symfony.com/blog/security-releases-cve-2014-4931-symfony-2-3-18-2-4-8-and-2-5-2-released', + 'cve' => 'CVE-2014-4931', + ], + 'CVE-2016-1902' => [ + 'title' => 'CVE-2016-1902: SecureRandom\'s fallback not secure when OpenSSL fails ', + 'link' => 'https://symfony.com/cve-2016-1902', + 'cve' => 'CVE-2016-1902', + ], + 'CVE-2018-14773' => [ + 'title' => 'CVE-2018-14773: Remove support for legacy and risky HTTP headers', + 'link' => 'https://symfony.com/blog/cve-2018-14773-remove-support-for-legacy-and-risky-http-headers', + 'cve' => 'CVE-2018-14773', + ], + 'CVE-2015-8124' => [ + 'title' => 'CVE-2015-8124: Session Fixation in the "Remember Me" Login Feature', + 'link' => 'https://symfony.com/cve-2015-8124', + 'cve' => 'CVE-2015-8124', + ], + 'CVE-2015-2309' => [ + 'title' => 'Unsafe methods in the Request class', + 'link' => 'https://symfony.com/cve-2015-2309', + 'cve' => 'CVE-2015-2309', + ], + 'CVE-2017-16653' => [ + 'title' => 'CVE-2017-16653: CSRF protection does not use different tokens for HTTP and HTTPS', + 'link' => 'https://symfony.com/cve-2017-16653', + 'cve' => 'CVE-2017-16653', + ], + 'CVE-2017-11365' => [ + 'title' => 'CVE-2017-11365: Empty passwords validation issue', + 'link' => 'https://symfony.com/cve-2017-11365', + 'cve' => 'CVE-2017-11365', + ], + 'CVE-2018-11386' => [ + 'title' => 'CVE-2018-11386: Denial of service when using PDOSessionHandler', + 'link' => 'https://symfony.com/cve-2018-11386', + 'cve' => 'CVE-2018-11386', + ], + 'CVE-2018-11406' => [ + 'title' => 'CVE-2018-11406: CSRF Token Fixation', + 'link' => 'https://symfony.com/cve-2018-11406', + 'cve' => 'CVE-2018-11406', + ], + 'CVE-2014-6072' => [ + 'title' => 'CSRF vulnerability in the Web Profiler', + 'link' => 'https://symfony.com/cve-2014-6072', + 'cve' => 'CVE-2014-6072', + ], + 'CVE-2018-11407' => [ + 'title' => 'CVE-2018-11407: Unauthorized access on a misconfigured LDAP server when using an empty password', + 'link' => 'https://symfony.com/cve-2018-11407', + 'cve' => 'CVE-2018-11407', + ], + 'CVE-2015-8125' => [ + 'title' => 'CVE-2015-8125: Potential Remote Timing Attack Vulnerability in Security Remember-Me Service', + 'link' => 'https://symfony.com/cve-2015-8125', + 'cve' => 'CVE-2015-8125', + ], + 'CVE-2015-2308' => [ + 'title' => 'Esi Code Injection', + 'link' => 'https://symfony.com/cve-2015-2308', + 'cve' => 'CVE-2015-2308', + ], + 'CVE-2016-2403' => [ + 'title' => 'CVE-2016-2403: Unauthorized access on a misconfigured Ldap server when using an empty password', + 'link' => 'https://symfony.com/cve-2016-2403', + 'cve' => 'CVE-2016-2403', + ], + 'CVE-2014-5244' => [ + 'title' => 'Denial of service with a malicious HTTP Host header', + 'link' => 'https://symfony.com/cve-2014-5244', + 'cve' => 'CVE-2014-5244', + ], + 'CVE-2014-5245' => [ + 'title' => 'Direct access of ESI URLs behind a trusted proxy', + 'link' => 'https://symfony.com/cve-2014-5245', + 'cve' => 'CVE-2014-5245', + ], + 'CVE-2017-16790' => [ + 'title' => 'CVE-2017-16790: Ensure that submitted data are uploaded files', + 'link' => 'https://symfony.com/cve-2017-16790', + 'cve' => 'CVE-2017-16790', + ], + ], + ], + ], + $vulnerabilities + ); } } diff --git a/tests/Automatic/Security/DownloaderTest.php b/tests/Automatic/Security/DownloaderTest.php index 0bb8ff9a..d35e1ed7 100644 --- a/tests/Automatic/Security/DownloaderTest.php +++ b/tests/Automatic/Security/DownloaderTest.php @@ -10,10 +10,33 @@ */ final class DownloaderTest extends TestCase { - public function testGetSecurityAdvisories(): void + /** + * @var string + */ + private const SECURITY_ADVISORIES_SHA = 'https://raw.githubusercontent.com/narrowspark/security-advisories/master/security-advisories-sha'; + + /** + * @var \Narrowspark\Automatic\Security\Downloader + */ + private $downloader; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + parent::setUp(); + + $this->downloader = new Downloader(); + } + + public function testDownloadWithComposer(): void { - $d = new Downloader(); + static::assertNotEmpty($this->downloader->downloadWithComposer(self::SECURITY_ADVISORIES_SHA)); + } - $d->getSecurityAdvisories(); + public function testDownloadWithCurl(): void + { + static::assertNotEmpty($this->downloader->downloadWithCurl(self::SECURITY_ADVISORIES_SHA)); } } diff --git a/tests/Common/PackageTest.php b/tests/Common/PackageTest.php index cf662214..7a0496bd 100644 --- a/tests/Common/PackageTest.php +++ b/tests/Common/PackageTest.php @@ -178,16 +178,16 @@ public function testToArray(): void public function testCreateFromLock(): void { $lockdata = [ - 'pretty-name' => 'test/Test', - 'version' => '1', - 'parent' => null, - 'is-dev' => false, - 'url' => null, - 'operation' => null, - 'type' => null, - 'requires' => [], - 'automatic-extra' => [], - 'created' => $this->package->getTime(), + 'pretty-name' => 'test/Test', + 'version' => '1', + 'parent' => null, + 'is-dev' => false, + 'url' => null, + 'operation' => null, + 'type' => null, + 'requires' => [], + 'automatic-extra' => [], + 'created' => $this->package->getTime(), ]; static::assertInstanceOf(ContractPackage::class, Package::createFromLock('test/test', $lockdata));