securityUpdates = []; $security_advisories_composer_json = $this->fetchAdvisoryComposerJson(); $composer_lock_data = $this->loadSiteComposerLock(); $this->registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json); if ($this->securityUpdates) { // @todo Modernize. drush_set_context('DRUSH_EXIT_CODE', DRUSH_FRAMEWORK_ERROR); $result = new RowsOfFields($this->securityUpdates); return $result; } else { $this->logger()->success("There are no outstanding security updates for Drupal projects."); } } /** * Emit suggested Composer command for security updates. * * @hook post-command pm:security */ public function suggestComposerCommand($result, CommandData $commandData) { if (!empty($this->securityUpdates)) { $suggested_command = 'composer require '; foreach ($this->securityUpdates as $package) { $suggested_command .= $package['name'] . ':^' . $package['min-version'] . ' '; } $suggested_command .= '--update-with-dependencies'; $this->logger()->warning("One or more of your dependencies has an outstanding security update. Please apply update(s) immediately."); $this->logger()->notice("Try running: $suggested_command"); $this->logger()->notice("If that fails due to a conflict then you must update one or more root dependencies."); } } /** * Fetches the generated composer.json from drupal-security-advisories. * * @return mixed * * @throws \Exception */ protected function fetchAdvisoryComposerJson() { try { $response_body = file_get_contents('https://raw.githubusercontent.com/drupal-composer/drupal-security-advisories/8.x/composer.json'); } catch (Exception $e) { throw new Exception("Unable to fetch drupal-security-advisories information."); } $security_advisories_composer_json = json_decode($response_body, true); return $security_advisories_composer_json; } /** * Loads the contents of the local Drupal application's composer.lock file. * * @return array * * @throws \Exception */ protected function loadSiteComposerLock() { $composer_root = Drush::bootstrapManager()->getComposerRoot(); $composer_lock_file_name = getenv('COMPOSER') ? str_replace( '.json', '', getenv('COMPOSER') ) : 'composer'; $composer_lock_file_name .= '.lock'; $composer_lock_file_path = Path::join( $composer_root, $composer_lock_file_name ); if (!file_exists($composer_lock_file_path)) { throw new Exception("Cannot find $composer_lock_file_path!"); } $composer_lock_contents = file_get_contents($composer_lock_file_path); $composer_lock_data = json_decode($composer_lock_contents, true); if (!array_key_exists('packages', $composer_lock_data)) { throw new Exception("No packages were found in $composer_lock_file_path! Contents:\n $composer_lock_contents"); } return $composer_lock_data; } /** * Register all available security updates in $this->securityUpdates. * @param array $composer_lock_data * The contents of the local Drupal application's composer.lock file. * @param array $security_advisories_composer_json * The composer.json array from drupal-security-advisories. */ protected function registerAllSecurityUpdates($composer_lock_data, $security_advisories_composer_json) { $both = $composer_lock_data['packages-dev'] + $composer_lock_data['packages']; foreach ($both as $package) { $name = $package['name']; $this->registerPackageSecurityUpdates($security_advisories_composer_json, $name, $package); } } /** * Determines if update is available based on a conflict constraint. * * @param string $conflict_constraint * The constraint for the conflicting, insecure package version. * E.g., <1.0.0. * @param array $package * The package to be evaluated. * @param string $name * The human readable display name for the package. * * @return array * An associative array containing name, version, and min-version keys. */ public static function determineUpdatesFromConstraint( $conflict_constraint, $package, $name ) { // Only parse constraints that follow pattern like "<1.0.0". if (substr($conflict_constraint, 0, 1) == '<') { $min_version = substr($conflict_constraint, 1); if (Comparator::lessThan( $package['version'], $min_version )) { return [ 'name' => $name, 'version' => $package['version'], // Assume that conflict constraint of <1.0.0 indicates that // 1.0.0 is the available, secure version. 'min-version' => $min_version, ]; } } // Compare exact versions that are insecure. elseif (preg_match( '/^[[:digit:]](?![-*><=~ ])/', $conflict_constraint )) { $exact_version = $conflict_constraint; if (Comparator::equalTo( $package['version'], $exact_version )) { $version_parts = explode('.', $package['version']); if (count($version_parts) == 3) { $version_parts[2]++; $min_version = implode('.', $version_parts); return [ 'name' => $name, 'version' => $package['version'], // Assume that conflict constraint of 1.0.0 indicates that // 1.0.1 is the available, secure version. 'min-version' => $min_version, ]; } } } return []; } /** * Registers available security updates for a given package. * * @param array $security_advisories_composer_json * The composer.json array from drupal-security-advisories. * @param string $name * The human readable display name for the package. * @param array $package * The package to be evaluated. */ protected function registerPackageSecurityUpdates( $security_advisories_composer_json, $name, $package ) { if (empty($this->securityUpdates[$name]) && !empty($security_advisories_composer_json['conflict'][$name])) { $conflict_constraints = explode( ',', $security_advisories_composer_json['conflict'][$name] ); foreach ($conflict_constraints as $conflict_constraint) { $available_update = $this->determineUpdatesFromConstraint( $conflict_constraint, $package, $name ); if ($available_update) { $this->securityUpdates[$name] = $available_update; } } } } }