diff --git a/composer.json b/composer.json index c37e842b..37856b99 100644 --- a/composer.json +++ b/composer.json @@ -35,8 +35,7 @@ "narrowspark/coding-standard": "^1.2.0", "narrowspark/testing-helper": "^7.0.0", "nyholm/nsa": "^1.1.0", - "phpunit/phpunit": "^7.2.0", - "symfony/phpunit-bridge": "^4.0.8" + "phpunit/phpunit": "^7.2.0" }, "config": { "optimize-autoloader": true, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 760cfdbb..84b2dc5a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -34,9 +34,4 @@ - - - - - diff --git a/src/Automatic/Automatic.php b/src/Automatic/Automatic.php index 182b8639..12e9dcb6 100644 --- a/src/Automatic/Automatic.php +++ b/src/Automatic/Automatic.php @@ -23,6 +23,8 @@ use Composer\Json\JsonFile; use Composer\Package\BasePackage; use Composer\Package\Locker; +use Composer\Plugin\Capability\CommandProvider as CommandProviderContract; +use Composer\Plugin\Capable; use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginInterface; use Composer\Plugin\PreFileDownloadEvent; @@ -46,17 +48,22 @@ 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\CommandProvider; +use Narrowspark\Automatic\Security\Downloader; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; -class Automatic implements PluginInterface, EventSubscriberInterface +class Automatic implements PluginInterface, EventSubscriberInterface, Capable { use ExpandTargetDirTrait; use GetGenericPropertyReaderTrait; + public const VERSION = '0.5.0'; + /** * @var string */ @@ -101,10 +108,26 @@ class Automatic implements PluginInterface, EventSubscriberInterface 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. * @@ -131,16 +154,26 @@ 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'], ['auditPackage']], + PackageEvents::POST_PACKAGE_UPDATE => [['record'], ['auditPackage']], PackageEvents::POST_PACKAGE_UNINSTALL => 'record', PluginEvents::PRE_FILE_DOWNLOAD => 'onFileDownload', - ScriptEvents::POST_INSTALL_CMD => 'onPostInstall', - ScriptEvents::POST_UPDATE_CMD => 'onPostUpdate', + ScriptEvents::POST_INSTALL_CMD => [['onPostInstall'], ['auditComposerLock']], + ScriptEvents::POST_UPDATE_CMD => [['onPostUpdate'], ['auditComposerLock']], ScriptEvents::POST_CREATE_PROJECT_CMD => [['onPostCreateProject', \PHP_INT_MAX], ['runSkeletonGenerator']], ]; } + /** + * {@inheritdoc} + */ + public function getCapabilities(): array + { + return [ + CommandProviderContract::class => CommandProvider::class, + ]; + } + /** * {@inheritdoc} */ @@ -165,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)); @@ -173,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)); @@ -191,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)); } /** @@ -204,7 +249,40 @@ 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'); + } + } + + /** + * Audit composer.lock. + * + * @param \Composer\Script\Event $event + * + * @return void + */ + public function auditComposerLock(Event $event): void + { + if (\count($this->foundVulnerabilities) !== 0) { + return; + } + + $data = $this->container->get(Audit::class)->checkLock(Util::getComposerLockFile()); + + if (\count($data) === 0) { + return; + } + + $this->foundVulnerabilities += $data[0]; } /** @@ -229,6 +307,40 @@ public function record(PackageEvent $event): void } } + /** + * Audit composer package operations. + * + * @param \Composer\Installer\PackageEvent $event + * + * @return void + */ + public function auditPackage(PackageEvent $event): void + { + $operation = $event->getOperation(); + + if ($operation instanceof UninstallOperation) { + return; + } + + if ($operation instanceof UpdateOperation) { + $composerPackage = $operation->getTargetPackage(); + } else { + $composerPackage = $operation->getPackage(); + } + + $data = $this->container->get(Audit::class)->checkPackage( + $composerPackage->getName(), + $composerPackage->getVersion(), + $this->securityAdvisories + ); + + if (\count($data) === 0) { + return; + } + + $this->foundVulnerabilities += $data[0]; + } + /** * Execute on composer create project event. * @@ -288,7 +400,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), @@ -862,12 +974,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/CommandProvider.php b/src/Automatic/CommandProvider.php new file mode 100644 index 00000000..26fc41b5 --- /dev/null +++ b/src/Automatic/CommandProvider.php @@ -0,0 +1,19 @@ +composerVendorPath = $composerVendorPath; + $this->downloader = $downloader; + $this->versionParser = new VersionParser(); + $this->filesystem = new Filesystem(); + } + + /** + * 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 */ + $packages = []; + + foreach (['packages', 'packages-dev'] as $key) { + $data = $lockContents[$key]; + + foreach ($data as $pkgData) { + $packages[] = new Package($pkgData['name'], $this->versionParser->normalize($pkgData['version']), $pkgData['version']); + } + } + + $securityAdvisories = $this->getSecurityAdvisories(); + $vulnerabilities = []; + $messages = []; + + foreach ($packages as $package) { + if (! isset($securityAdvisories[$package->getName()])) { + continue; + } + + [$messages, $vulnerabilities] = $this->checkPackageAgainstSecurityAdvisories($securityAdvisories, $package, $messages, $vulnerabilities); + } + + \ksort($vulnerabilities); + + return [$vulnerabilities, $messages]; + } + + /** + * 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); + } else { + $sha = $this->downloader->downloadWithCurl(self::SECURITY_ADVISORIES_BASE_URL . self::SECURITY_ADVISORIES_SHA); + } + + $narrowsparkAutomaticPath = $this->composerVendorPath . \DIRECTORY_SEPARATOR . 'narrowspark' . \DIRECTORY_SEPARATOR . 'automatic' . \DIRECTORY_SEPARATOR; + + if (! $this->filesystem->exists($narrowsparkAutomaticPath)) { + $this->filesystem->mkdir($narrowsparkAutomaticPath); + } + + $securityAdvisoriesShaPath = $narrowsparkAutomaticPath . self::SECURITY_ADVISORIES_SHA; + $securityAdvisoriesPath = $narrowsparkAutomaticPath . self::SECURITY_ADVISORIES; + + if ($this->filesystem->exists($securityAdvisoriesShaPath)) { + $oldSha = \file_get_contents($securityAdvisoriesShaPath); + + if ($oldSha === $sha) { + 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 { + $securityAdvisories = $this->downloader->downloadWithCurl(self::SECURITY_ADVISORIES_BASE_URL . self::SECURITY_ADVISORIES); + } + + $this->filesystem->dumpFile($securityAdvisoriesShaPath, $sha); + $this->filesystem->dumpFile($securityAdvisoriesPath, $securityAdvisories); + + return \json_decode((string) \file_get_contents($securityAdvisoriesPath), true); + } + + /** + * @param string $lock + * + * @return array + */ + private function getLockContents(string $lock): array + { + $contents = \json_decode((string) \file_get_contents($lock), true); + $packages = ['packages' => [], 'packages-dev' => []]; + + foreach (['packages', 'packages-dev'] as $key) { + if (! \is_array($contents[$key])) { + continue; + } + + foreach ($contents[$key] as $package) { + $data = [ + 'name' => $package['name'], + 'version' => $package['version'], + ]; + + if (isset($package['time']) && false !== \mb_strpos($package['version'], 'dev')) { + $data['time'] = $package['time']; + } + + $packages[$key][] = $data; + } + } + + 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 new file mode 100644 index 00000000..7895fa5d --- /dev/null +++ b/src/Automatic/Security/Command/AuditCommand.php @@ -0,0 +1,120 @@ +setName('audit') + ->setDefinition([ + new InputOption('composer-lock', '', InputOption::VALUE_REQUIRED, 'Path to a composer.lock'), + 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') + ->setHelp( + <<<'EOF' +The %command.name% command looks for security issues in the +project dependencies: +%command.full_name% +EOF + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $downloader = new Downloader(); + + if (($timeout = $input->getOption('timeout')) !== null) { + $downloader->setTimeout((int) $timeout); + } + + $config = Factory::createConfig(new NullIO()); + $audit = new Audit(\rtrim($config->get('vendor-dir'), '/'), $downloader); + + if ($input->getOption('composer-lock') !== null) { + $composerFile = $input->getOption('composer-lock'); + } else { + $composerFile = Util::getComposerLockFile(); + } + + $output = new SymfonyStyle($input, $output); + $output->writeln('=== Audit Security Report ==='); + + 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; + } + $output->comment('This checker can only detect vulnerabilities that are referenced in the SensioLabs security advisories database.'); + + 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 new file mode 100644 index 00000000..f75676ec --- /dev/null +++ b/src/Automatic/Security/Downloader.php @@ -0,0 +1,177 @@ +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 = [ + 'http' => [ + 'method' => 'GET', + 'ignore_errors' => true, + 'follow_location' => true, + 'max_redirects' => 3, + 'timeout' => $this->timeout, + 'user_agent' => $this->getUserAgent(), + ], + 'ssl' => [ + 'verify_peer' => 1, + 'verify_host' => 2, + ], + ]; + + $caPathOrFile = CaBundle::getSystemCaRootBundlePath(); + + if (\is_dir($caPathOrFile) || (\is_link($caPathOrFile) && \is_dir(\readlink($caPathOrFile)))) { + $opts['ssl']['capath'] = $caPathOrFile; + } else { + $opts['ssl']['cafile'] = $caPathOrFile; + } + + $context = StreamContextFactory::getContext($url, $opts); + $level = \error_reporting(0); + $body = \file_get_contents($url, false, $context); + + \error_reporting($level); + + if ($body === false) { + $error = \error_get_last(); + + throw new RuntimeException(\sprintf('An error occurred: %s.', $error['message'])); + } + + // status code + if ((bool) \preg_match('{HTTP/\d\.\d (\d+) }i', $http_response_header[0], $match) === false) { + throw new RuntimeException('An unknown error occurred.'); + } + + $this->checkStatus((int) $match[1], $body); + + return \trim($body); + } + + /** + * Download a file with the curl extension. + * + * @param string $url + * + * @throws \Narrowspark\Automatic\Common\Contract\Exception\RuntimeException + * + * @return string + */ + public function downloadWithCurl(string $url): string + { + $curl = \curl_init(); + + \curl_setopt($curl, \CURLOPT_RETURNTRANSFER, true); + \curl_setopt($curl, \CURLOPT_HEADER, true); + \curl_setopt($curl, \CURLOPT_URL, $url); + \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, \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); + \curl_setopt($curl, \CURLOPT_SSL_VERIFYHOST, 2); + \curl_setopt($curl, \CURLOPT_USERAGENT, $this->getUserAgent()); + + $caPathOrFile = CaBundle::getSystemCaRootBundlePath(); + + if (\is_dir($caPathOrFile) || (\is_link($caPathOrFile) && \is_dir(\readlink($caPathOrFile)))) { + \curl_setopt($curl, \CURLOPT_CAPATH, $caPathOrFile); + } else { + \curl_setopt($curl, \CURLOPT_CAINFO, $caPathOrFile); + } + + $response = \curl_exec($curl); + + if ($response === false) { + $error = \curl_error($curl); + + \curl_close($curl); + + throw new RuntimeException(\sprintf('An error occurred: %s.', $error)); + } + + $body = \mb_substr((string) $response, \curl_getinfo($curl, \CURLINFO_HEADER_SIZE)); + $statusCode = (int) \curl_getinfo($curl, \CURLINFO_HTTP_CODE); + + \curl_close($curl); + + $this->checkStatus($statusCode, $body); + + return \trim($body); + } + + /** + * @return string + */ + private function getUserAgent(): string + { + return \sprintf( + 'Narrowspark-Automatic/%s (%s; %s; %s%s)', + 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') !== false ? '; CI' : '' + ); + } + + /** + * Check response status. + * + * @param int $statusCode + * @param string $body + * + * @throws \Narrowspark\Automatic\Common\Contract\Exception\RuntimeException + * + * @return void + */ + private function checkStatus(int $statusCode, string $body): void + { + if ($statusCode === 400) { + throw new RuntimeException($body); + } + + if ($statusCode !== 200) { + throw new RuntimeException(\sprintf('The web service failed for an unknown reason (HTTP %s).', $statusCode), $statusCode); + } + } +} diff --git a/src/Automatic/SkeletonGenerator.php b/src/Automatic/SkeletonGenerator.php index e4468ac9..715934fc 100644 --- a/src/Automatic/SkeletonGenerator.php +++ b/src/Automatic/SkeletonGenerator.php @@ -113,10 +113,11 @@ public function run(): void $status = $this->installationManager->run(); + // @codeCoverageIgnoreStart if ($status !== 0) { exit($status); } - + // @codeCoverageIgnoreEnd $generator->generate(); } 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/CommandProviderTest.php b/tests/Automatic/CommandProviderTest.php new file mode 100644 index 00000000..20c71656 --- /dev/null +++ b/tests/Automatic/CommandProviderTest.php @@ -0,0 +1,22 @@ +getCommands(); + + static::assertInstanceOf(AuditCommand::class, $commands[0]); + } +} 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/Fixture/symfony_2.5.2_composer.lock b/tests/Automatic/Fixture/symfony_2.5.2_composer.lock new file mode 100644 index 00000000..3d2a17f3 --- /dev/null +++ b/tests/Automatic/Fixture/symfony_2.5.2_composer.lock @@ -0,0 +1,992 @@ +{ + "_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": "f4984f8e9643d2860a86d45a862b64a4", + "packages": [ + { + "name": "doctrine/annotations", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-12-06T07:11:42+00:00" + }, + { + "name": "doctrine/cache", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", + "shasum": "" + }, + "require": { + "php": "~7.1" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^4.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Caching library offering an object-oriented API for many cache backends", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2018-08-21T18:01:43+00:00" + }, + { + "name": "doctrine/collections", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2017-07-22T10:37:32+00:00" + }, + { + "name": "doctrine/common", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "a210246d286c77d2b89040f8691ba7b3a713d2c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/a210246d286c77d2b89040f8691ba7b3a713d2c1", + "reference": "a210246d286c77d2b89040f8691ba7b3a713d2c1", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/inflector": "^1.0", + "doctrine/lexer": "^1.0", + "doctrine/persistence": "^1.0", + "doctrine/reflection": "^1.0", + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^1.0", + "phpunit/phpunit": "^6.3", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^4.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Common Library for Doctrine projects", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "annotations", + "collections", + "eventmanager", + "persistence", + "spl" + ], + "time": "2018-07-12T21:16:12+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^4.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Doctrine Event Manager component", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "eventdispatcher", + "eventmanager" + ], + "time": "2018-06-11T11:59:03+00:00" + }, + { + "name": "doctrine/inflector", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5527a48b7313d15261292c149e55e26eae771b0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a", + "reference": "5527a48b7313d15261292c149e55e26eae771b0a", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Common String Manipulations with regard to casing and singular/plural rules.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "inflection", + "pluralize", + "singularize", + "string" + ], + "time": "2018-01-09T20:05:19+00:00" + }, + { + "name": "doctrine/lexer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09T13:34:57+00:00" + }, + { + "name": "doctrine/persistence", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "af1ec238659a83e320f03e0e454e200f689b4b97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/af1ec238659a83e320f03e0e454e200f689b4b97", + "reference": "af1ec238659a83e320f03e0e454e200f689b4b97", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/cache": "^1.0", + "doctrine/collections": "^1.0", + "doctrine/event-manager": "^1.0", + "doctrine/reflection": "^1.0", + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^4.0", + "phpstan/phpstan": "^0.8", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Doctrine Persistence abstractions.", + "homepage": "https://doctrine-project.org/projects/persistence.html", + "keywords": [ + "persistence" + ], + "time": "2018-07-12T12:37:50+00:00" + }, + { + "name": "doctrine/reflection", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/reflection.git", + "reference": "02538d3f95e88eb397a5f86274deb2c6175c2ab6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/reflection/zipball/02538d3f95e88eb397a5f86274deb2c6175c2ab6", + "reference": "02538d3f95e88eb397a5f86274deb2c6175c2ab6", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^4.0", + "doctrine/common": "^2.8", + "phpstan/phpstan": "^0.9.2", + "phpstan/phpstan-phpunit": "^0.9.4", + "phpunit/phpunit": "^7.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Doctrine Reflection component", + "homepage": "https://www.doctrine-project.org/projects/reflection.html", + "keywords": [ + "reflection" + ], + "time": "2018-06-14T14:45:07+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": "symfony/icu", + "version": "v1.0.1", + "target-dir": "Symfony/Component/Icu", + "source": { + "type": "git", + "url": "https://github.com/symfony/icu.git", + "reference": "fdba214b1e087c149843bde976263c53ac10c975" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/icu/zipball/fdba214b1e087c149843bde976263c53ac10c975", + "reference": "fdba214b1e087c149843bde976263c53ac10c975", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/intl": "~2.3" + }, + "type": "library", + "autoload": { + "psr-0": { + "Symfony\\Component\\Icu\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Contains an excerpt of the ICU data and classes to load it.", + "homepage": "http://symfony.com", + "keywords": [ + "icu", + "intl" + ], + "abandoned": "symfony/intl", + "time": "2013-10-04T09:12:07+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/symfony", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/symfony.git", + "reference": "e66ee967571b89234c90946fe0d50dad195ad29c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/symfony/zipball/e66ee967571b89234c90946fe0d50dad195ad29c", + "reference": "e66ee967571b89234c90946fe0d50dad195ad29c", + "shasum": "" + }, + "require": { + "doctrine/common": "~2.2", + "php": ">=5.3.3", + "psr/log": "~1.0", + "symfony/icu": "~1.0", + "twig/twig": "~1.12" + }, + "replace": { + "symfony/browser-kit": "self.version", + "symfony/class-loader": "self.version", + "symfony/config": "self.version", + "symfony/console": "self.version", + "symfony/css-selector": "self.version", + "symfony/debug": "self.version", + "symfony/dependency-injection": "self.version", + "symfony/doctrine-bridge": "self.version", + "symfony/dom-crawler": "self.version", + "symfony/event-dispatcher": "self.version", + "symfony/expression-language": "self.version", + "symfony/filesystem": "self.version", + "symfony/finder": "self.version", + "symfony/form": "self.version", + "symfony/framework-bundle": "self.version", + "symfony/http-foundation": "self.version", + "symfony/http-kernel": "self.version", + "symfony/intl": "self.version", + "symfony/locale": "self.version", + "symfony/monolog-bridge": "self.version", + "symfony/options-resolver": "self.version", + "symfony/process": "self.version", + "symfony/propel1-bridge": "self.version", + "symfony/property-access": "self.version", + "symfony/proxy-manager-bridge": "self.version", + "symfony/routing": "self.version", + "symfony/security": "self.version", + "symfony/security-acl": "self.version", + "symfony/security-bundle": "self.version", + "symfony/security-core": "self.version", + "symfony/security-csrf": "self.version", + "symfony/security-http": "self.version", + "symfony/serializer": "self.version", + "symfony/stopwatch": "self.version", + "symfony/swiftmailer-bridge": "self.version", + "symfony/templating": "self.version", + "symfony/translation": "self.version", + "symfony/twig-bridge": "self.version", + "symfony/twig-bundle": "self.version", + "symfony/validator": "self.version", + "symfony/web-profiler-bundle": "self.version", + "symfony/yaml": "self.version" + }, + "require-dev": { + "doctrine/data-fixtures": "1.0.*", + "doctrine/dbal": "~2.2", + "doctrine/orm": "~2.2,>=2.2.3", + "egulias/email-validator": "1.1.0", + "ircmaxell/password-compat": "1.0.*", + "monolog/monolog": "~1.3", + "ocramius/proxy-manager": ">=0.3.1,<0.6-dev", + "propel/propel1": "1.6.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\": "src/" + }, + "classmap": [ + "src/Symfony/Component/HttpFoundation/Resources/stubs", + "src/Symfony/Component/Intl/Resources/stubs" + ], + "files": [ + "src/Symfony/Component/Intl/Resources/stubs/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "The Symfony PHP framework", + "homepage": "http://symfony.com", + "keywords": [ + "framework" + ], + "time": "2014-07-15T15:39:46+00:00" + }, + { + "name": "twig/twig", + "version": "v1.35.4", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", + "reference": "7e081e98378a1e78c29cc9eba4aefa5d78a05d2a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-ctype": "^1.8" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.35-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "https://twig.symfony.com/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "time": "2018-07-13T07:12:17+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 new file mode 100644 index 00000000..c79fe71d --- /dev/null +++ b/tests/Automatic/Security/AuditTest.php @@ -0,0 +1,234 @@ +audit = new Audit(__DIR__, new Downloader()); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + parent::tearDown(); + + (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()); + + $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 + { + [$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/Command/AuditCommandTest.php b/tests/Automatic/Security/Command/AuditCommandTest.php new file mode 100644 index 00000000..c90f3555 --- /dev/null +++ b/tests/Automatic/Security/Command/AuditCommandTest.php @@ -0,0 +1,158 @@ +application = new Application(); + } + + public function testAuditCommand(): void + { + \putenv('COMPOSER=' . \dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'composer_1.7.1_composer.lock'); + + $commandTester = $this->executeCommand(new AuditCommand()); + + static::assertContains('[+] No known vulnerabilities found', \trim($commandTester->getDisplay(true))); + + \putenv('COMPOSER='); + \putenv('COMPOSER'); + } + + public function testAuditCommandWithComposerLockOption(): void + { + $commandTester = $this->executeCommand( + new AuditCommand(), + ['--composer-lock' => \dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'composer_1.7.1_composer.lock'] + ); + + $output = \trim($commandTester->getDisplay(true)); + + static::assertContains('=== Audit Security Report ===', $output); + static::assertContains('This checker can only detect vulnerabilities that are referenced', $output); + static::assertContains('[+] No known vulnerabilities found', $output); + } + + public function testAuditCommandWithEmptyComposerLockPath(): void + { + $commandTester = $this->executeCommand( + new AuditCommand(), + ['--composer-lock' => 'composer_1.7.1_composer.lock'] + ); + + $output = \trim($commandTester->getDisplay(true)); + + static::assertContains(\trim('=== Audit Security Report ==='), $output); + static::assertContains(\trim('Lock file does not exist.'), $output); + } + + public function testAuditCommandWithError(): void + { + $commandTester = $this->executeCommand( + new AuditCommand(), + ['--composer-lock' => \dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'symfony_2.5.2_composer.lock'] + ); + + $output = \trim($commandTester->getDisplay(true)); + + static::assertContains('=== Audit Security Report ===', $output); + static::assertContains('This checker can only detect vulnerabilities that are referenced', $output); + static::assertContains('symfony/symfony (v2.5.2)', $output); + static::assertContains('[!] 1 vulnerability found - We recommend you to check the related security advisories and upgrade these dependencies.', $output); + } + + public function testAuditCommandWithErrorAndJsonFormat(): void + { + $commandTester = $this->executeCommand( + new AuditCommand(), + [ + '--composer-lock' => \dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'symfony_2.5.2_composer.lock', + '--format' => 'json', + '--timeout' => '20', + ] + ); + + $output = \trim($commandTester->getDisplay(true)); + + $jsonOutput = \str_replace( + [ + '=== Audit Security Report ===', + '//', + 'This checker can only detect vulnerabilities that are referenced', + 'in the', + 'SensioLabs security advisories database.', + '[!] 1 vulnerability found - We recommend you to check the related security advisories and upgrade these dependencies.', + ], + '', + $output + ); + + static::assertJson($jsonOutput); + static::assertContains('=== Audit Security Report ===', $output); + static::assertContains('This checker can only detect vulnerabilities that are referenced', $output); + static::assertContains('[!] 1 vulnerability found - We recommend you to check the related security advisories and upgrade these dependencies.', $output); + } + + public function testAuditCommandWithErrorAndSimpleFormat(): void + { + $commandTester = $this->executeCommand( + new AuditCommand(), + [ + '--composer-lock' => \dirname(__DIR__, 2) . \DIRECTORY_SEPARATOR . 'Fixture' . \DIRECTORY_SEPARATOR . 'symfony_2.5.2_composer.lock', + '--format' => 'simple', + ] + ); + + $output = \trim($commandTester->getDisplay(true)); + + static::assertContains('=== Audit Security Report ===', $output); + static::assertContains(\trim('symfony/symfony (v2.5.2) +------------------------ +'), $output); + static::assertContains('This checker can only detect vulnerabilities that are referenced', $output); + static::assertContains('[!] 1 vulnerability found - We recommend you to check the related security advisories and upgrade these dependencies.', $output); + } + + /** + * @param \Symfony\Component\Console\Command\Command $command + * @param array $input + * @param array $options + * + * @return \Symfony\Component\Console\Tester\CommandTester + */ + protected function executeCommand(Command $command, array $input = [], array $options = []): CommandTester + { + $this->application->add($command); + + $reflectionProperty = (new \ReflectionClass($command))->getProperty('defaultName'); + $reflectionProperty->setAccessible(true); + + $command = $this->application->find($reflectionProperty->getValue($command)); + + $commandTester = new CommandTester($command); + $commandTester->execute(['command' => $command->getName()] + $input, $options); + + return $commandTester; + } +} diff --git a/tests/Automatic/Security/DownloaderTest.php b/tests/Automatic/Security/DownloaderTest.php new file mode 100644 index 00000000..d35e1ed7 --- /dev/null +++ b/tests/Automatic/Security/DownloaderTest.php @@ -0,0 +1,42 @@ +downloader = new Downloader(); + } + + public function testDownloadWithComposer(): void + { + static::assertNotEmpty($this->downloader->downloadWithComposer(self::SECURITY_ADVISORIES_SHA)); + } + + 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));