public function security()
{
$this->securityUpdates = [];
- 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);
- $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");
- }
- foreach ($composer_lock_data['packages'] as $key => $package) {
- $name = $package['name'];
- if (!empty($security_advisories_composer_json['conflict'][$name])) {
- $conflict_constraints = explode(',', $security_advisories_composer_json['conflict'][$name]);
- foreach ($conflict_constraints as $conflict_constraint) {
- // 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)) {
- $this->securityUpdates[$name] = [
- 'name' => $name,
- 'version' => $package['version'],
- 'min-version' => $min_version,
- ];
- }
- } else {
- $this->logger()->warning("Could not parse drupal-security-advisories conflicting version constraint $conflict_constraint for package $name.");
- }
- }
- }
- }
+ $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);
$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;
+ }
+ }
+ }
+ }
}