
| Current Path : /var/www/html/vendor/drush/drush/src/Commands/pm/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/html/vendor/drush/drush/src/Commands/pm/SecurityUpdateCommands.php |
<?php
namespace Drush\Commands\pm;
use GuzzleHttp\Client;
use Composer\Semver\Semver;
use Consolidation\AnnotatedCommand\CommandResult;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Consolidation\OutputFormatters\StructuredData\UnstructuredData;
use Drush\Commands\DrushCommands;
use Drush\Drush;
use Enlightn\SecurityChecker\SecurityChecker;
use Exception;
use Symfony\Component\Filesystem\Path;
/**
* Check Drupal Composer packages for security updates.
*/
class SecurityUpdateCommands extends DrushCommands
{
/**
* Return path to composer.lock
*
* @return string
* @throws \Exception
*/
public static function composerLockPath(): string
{
$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!");
}
return $composer_lock_file_path;
}
/**
* Check Drupal Composer packages for pending security updates.
*
* This uses the [Drupal security advisories package](https://github.com/drupal-composer/drupal-security-advisories) to determine if updates
* are available. An exit code of 3 indicates that the check completed, and insecure packages were found.
*
* @command pm:security
* @aliases sec,pm-security
* @option no-dev Only check production dependencies.
* @usage drush pm:security --format=json
* Get security data in JSON format.
* @usage HTTP_PROXY=tcp://localhost:8125 pm:security
* Proxy Guzzle requests through an http proxy.
* @bootstrap none
* @table-style default
* @field-labels
* name: Name
* version: Installed Version
* @default-fields name,version
*
* @filter-default-field name
* @return RowsOfFields
*
* @throws \Exception
*/
public function security(array $options = ['no-dev' => false])
{
$security_advisories_composer_json = $this->fetchAdvisoryComposerJson();
$composer_lock_data = $this->loadSiteComposerLock();
$updates = $this->calculateSecurityUpdates($composer_lock_data, $security_advisories_composer_json, $options['no-dev']);
if ($updates) {
$this->suggestComposerCommand($updates);
return CommandResult::dataWithExitCode(new RowsOfFields($updates), self::EXIT_FAILURE_WITH_CLARITY);
}
$this->logger()->success("<info>There are no outstanding security updates for Drupal projects.</info>");
if ($options['format'] === 'table') {
return null;
}
return new RowsOfFields([]);
}
/**
* Emit suggested Composer command for security updates.
*/
public function suggestComposerCommand($updates): void
{
$suggested_command = 'composer require ';
foreach ($updates as $package) {
$suggested_command .= $package['name'] . ' ';
}
$suggested_command .= '--update-with-dependencies';
$this->logger()->warning('One or more of your dependencies has an outstanding security update.');
$this->logger()->notice("Try running: <comment>$suggested_command</comment>");
$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.
*
* This function fetches the generated composer.json from the
* drupal-security-advisories repository or fetches it from another source
* if the environment variable DRUSH_SECURITY_ADVISORIES_URL is set. The
* environment variable is not a supported API.
*
* @return mixed
*
* @throws \Exception
*/
protected function fetchAdvisoryComposerJson()
{
$client = new Client(['handler' => $this->getStack()]);
$security_advisories_composer_url = getenv('DRUSH_SECURITY_ADVISORIES_URL') ?: 'https://raw.githubusercontent.com/drupal-composer/drupal-security-advisories/9.x/composer.json';
$response = $client->get($security_advisories_composer_url);
$security_advisories_composer_json = json_decode($response->getBody(), true);
return $security_advisories_composer_json;
}
/**
* Loads the contents of the local Drupal application's composer.lock file.
*
*
* @throws \Exception
*/
protected function loadSiteComposerLock(): array
{
$composer_lock_file_path = self::composerLockPath();
$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;
}
/**
* Return available security updates.
*
* @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 calculateSecurityUpdates(array $composer_lock_data, array $security_advisories_composer_json, bool $excludeDev = false): array
{
$updates = [];
$packages = $composer_lock_data['packages'];
if (!$excludeDev) {
$packages = array_merge($composer_lock_data['packages-dev'], $packages);
}
$conflict = $security_advisories_composer_json['conflict'];
foreach ($packages as $package) {
$name = $package['name'];
if (!empty($conflict[$name]) && Semver::satisfies($package['version'], $security_advisories_composer_json['conflict'][$name])) {
$updates[$name] = [
'name' => $name,
'version' => $package['version'],
];
}
}
return $updates;
}
/**
* Check non-Drupal PHP packages for pending security updates.
*
* Packages are discovered via composer.lock file. An exit code of 3
* indicates that the check completed, and insecure packages were found.
*
* @param array $options
*
* @return UnstructuredData
* @throws \Exception
* @command pm:security-php
* @validate-php-extension json
* @aliases sec-php,pm-security-php
* @option no-dev Only check production dependencies.
* @bootstrap none
*
* @usage drush pm:security-php --format=json
* Get security data in JSON format.
* @usage HTTP_PROXY=tcp://localhost:8125 pm:security
* Proxy Guzzle requests through an http proxy.
*/
public function securityPhp(array $options = ['format' => 'yaml', 'no-dev' => false])
{
$result = (new SecurityChecker())->check(self::composerLockPath(), $options['no-dev']);
if ($result) {
$suggested_command = "composer why " . implode(' && composer why ', array_keys($result));
$this->logger()->warning('One or more of your dependencies has an outstanding security update.');
$this->logger()->notice("Run <comment>$suggested_command</comment> to learn what module requires the package.");
return CommandResult::dataWithExitCode(new UnstructuredData($result), self::EXIT_FAILURE_WITH_CLARITY);
}
$this->logger()->success("There are no outstanding security updates for your dependencies.");
if ($options['format'] === 'table') {
return null;
}
return new RowsOfFields([]);
}
}