parent
d17c724f23
commit
d93239ddd9
|
@ -109,6 +109,8 @@ resolution.
|
||||||
* **--no-autoloader:** Skips autoloader generation.
|
* **--no-autoloader:** Skips autoloader generation.
|
||||||
* **--no-progress:** Removes the progress display that can mess with some
|
* **--no-progress:** Removes the progress display that can mess with some
|
||||||
terminals or scripts which don't handle backspace characters.
|
terminals or scripts which don't handle backspace characters.
|
||||||
|
* **--no-audit:** Does not run the audit step after installation is complete.
|
||||||
|
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
||||||
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
|
* **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster
|
||||||
autoloader. This is recommended especially for production, but can take
|
autoloader. This is recommended especially for production, but can take
|
||||||
a bit of time to run so it is currently not done by default.
|
a bit of time to run so it is currently not done by default.
|
||||||
|
@ -188,6 +190,8 @@ and this feature is only available for your root package dependencies.
|
||||||
* **--dev:** Install packages listed in `require-dev` (this is the default behavior).
|
* **--dev:** Install packages listed in `require-dev` (this is the default behavior).
|
||||||
* **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules.
|
* **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules.
|
||||||
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
||||||
|
* **--no-audit:** Does not run the audit steps after updating the composer.lock file.
|
||||||
|
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
||||||
* **--lock:** Only updates the lock file hash to suppress warning about the
|
* **--lock:** Only updates the lock file hash to suppress warning about the
|
||||||
lock file being out of date.
|
lock file being out of date.
|
||||||
* **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0
|
* **--with:** Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0
|
||||||
|
@ -259,6 +263,8 @@ If you do not specify a package, Composer will prompt you to search for a packag
|
||||||
terminals or scripts which don't handle backspace characters.
|
terminals or scripts which don't handle backspace characters.
|
||||||
* **--no-update:** Disables the automatic update of the dependencies (implies --no-install).
|
* **--no-update:** Disables the automatic update of the dependencies (implies --no-install).
|
||||||
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
||||||
|
* **--no-audit:** Does not run the audit steps after updating the composer.lock file.
|
||||||
|
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
||||||
* **--update-no-dev:** Run the dependency update with the `--no-dev` option.
|
* **--update-no-dev:** Run the dependency update with the `--no-dev` option.
|
||||||
* **--update-with-dependencies (-w):** Also update dependencies of the newly required packages, except those that are root requirements.
|
* **--update-with-dependencies (-w):** Also update dependencies of the newly required packages, except those that are root requirements.
|
||||||
* **--update-with-all-dependencies (-W):** Also update dependencies of the newly required packages, including those that are root requirements.
|
* **--update-with-all-dependencies (-W):** Also update dependencies of the newly required packages, including those that are root requirements.
|
||||||
|
@ -301,6 +307,8 @@ uninstalled.
|
||||||
terminals or scripts which don't handle backspace characters.
|
terminals or scripts which don't handle backspace characters.
|
||||||
* **--no-update:** Disables the automatic update of the dependencies (implies --no-install).
|
* **--no-update:** Disables the automatic update of the dependencies (implies --no-install).
|
||||||
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
* **--no-install:** Does not run the install step after updating the composer.lock file.
|
||||||
|
* **--no-audit:** Does not run the audit steps after installation is complete.
|
||||||
|
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
||||||
* **--update-no-dev:** Run the dependency update with the --no-dev option.
|
* **--update-no-dev:** Run the dependency update with the --no-dev option.
|
||||||
* **--update-with-dependencies (-w):** Also update dependencies of the removed packages.
|
* **--update-with-dependencies (-w):** Also update dependencies of the removed packages.
|
||||||
(Deprecated, is now default behavior)
|
(Deprecated, is now default behavior)
|
||||||
|
@ -880,6 +888,8 @@ By default the command checks for the packages on packagist.org.
|
||||||
mode.
|
mode.
|
||||||
* **--remove-vcs:** Force-remove the VCS metadata without prompting.
|
* **--remove-vcs:** Force-remove the VCS metadata without prompting.
|
||||||
* **--no-install:** Disables installation of the vendors.
|
* **--no-install:** Disables installation of the vendors.
|
||||||
|
* **--no-audit:** Does not run the audit steps after installation is complete.
|
||||||
|
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
||||||
* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`,
|
* **--ignore-platform-reqs:** ignore all platform requirements (`php`, `hhvm`,
|
||||||
`lib-*` and `ext-*`) and force the installation even if the local machine does
|
`lib-*` and `ext-*`) and force the installation even if the local machine does
|
||||||
not fulfill these.
|
not fulfill these.
|
||||||
|
@ -991,6 +1001,23 @@ php composer.phar archive vendor/package 2.0.21 --format=zip
|
||||||
* **--dir:** Write the archive to this directory (default: ".")
|
* **--dir:** Write the archive to this directory (default: ".")
|
||||||
* **--file:** Write the archive with the given file name.
|
* **--file:** Write the archive with the given file name.
|
||||||
|
|
||||||
|
## audit
|
||||||
|
|
||||||
|
This command is used to audit the packages you have installed
|
||||||
|
for possible security issues. Currently this only checks for and
|
||||||
|
lists security vulnerability advisories according to the
|
||||||
|
[Packagist.org api](https://packagist.org/apidoc#list-security-advisories).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
php composer.phar audit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
* **--no-dev:** Disables auditing of require-dev packages.
|
||||||
|
* **--format (-f):** Audit output format. Must be "table" (default), "plain", or "summary".
|
||||||
|
* **--locked:** Audit packages from the lock file, regardless of what is currently in vendor dir.
|
||||||
|
|
||||||
## help
|
## help
|
||||||
|
|
||||||
To get more information about a certain command, you can use `help`.
|
To get more information about a certain command, you can use `help`.
|
||||||
|
|
|
@ -4518,6 +4518,11 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/SelfUpdate/Versions.php
|
path: ../src/Composer/SelfUpdate/Versions.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Composer/Util/Auditor.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#"
|
message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Composer\Command;
|
||||||
|
|
||||||
|
use Composer\Composer;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Composer\Package\PackageInterface;
|
||||||
|
use Composer\Repository\InstalledRepository;
|
||||||
|
use Composer\Repository\RepositoryInterface;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
|
class AuditCommand extends BaseCommand
|
||||||
|
{
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('audit')
|
||||||
|
->setDescription('Checks for security vulnerability advisories for installed packages.')
|
||||||
|
->setDefinition(array(
|
||||||
|
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables auditing of require-dev packages.'),
|
||||||
|
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_TABLE, Auditor::FORMATS),
|
||||||
|
new InputOption('locked', null, InputOption::VALUE_NONE, 'Audit based on the lock file instead of the installed packages.'),
|
||||||
|
))
|
||||||
|
->setHelp(
|
||||||
|
<<<EOT
|
||||||
|
The <info>audit</info> command checks for security vulnerability advisories for installed packages.
|
||||||
|
|
||||||
|
If you do not want to include dev dependencies in the audit you can omit them with --no-dev
|
||||||
|
|
||||||
|
Read more at https://getcomposer.org/doc/03-cli.md#audit
|
||||||
|
EOT
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$composer = $this->requireComposer();
|
||||||
|
$packages = $this->getPackages($composer, $input);
|
||||||
|
$httpDownloader = $composer->getLoop()->getHttpDownloader();
|
||||||
|
|
||||||
|
if (count($packages) === 0) {
|
||||||
|
$this->getIO()->writeError('No packages - skipping audit.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$auditor = new Auditor($httpDownloader);
|
||||||
|
return $auditor->audit($this->getIO(), $packages, $input->getOption('format'), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InputInterface $input
|
||||||
|
* @return PackageInterface[]
|
||||||
|
*/
|
||||||
|
private function getPackages(Composer $composer, InputInterface $input): array
|
||||||
|
{
|
||||||
|
if ($input->getOption('locked')) {
|
||||||
|
if (!$composer->getLocker()->isLocked()) {
|
||||||
|
throw new \UnexpectedValueException('Valid composer.json and composer.lock files are required to run this command with --locked');
|
||||||
|
}
|
||||||
|
$locker = $composer->getLocker();
|
||||||
|
return $locker->getLockedRepository(!$input->getOption('no-dev'))->getPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootPkg = $composer->getPackage();
|
||||||
|
$installedRepo = new InstalledRepository(array($composer->getRepositoryManager()->getLocalRepository()));
|
||||||
|
|
||||||
|
if ($input->getOption('no-dev')) {
|
||||||
|
return $this->filterRequiredPackages($installedRepo, $rootPkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installedRepo->getPackages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find package requires and child requires.
|
||||||
|
* Effectively filters out dev dependencies.
|
||||||
|
*
|
||||||
|
* @param PackageInterface[] $bucket
|
||||||
|
* @return PackageInterface[]
|
||||||
|
*/
|
||||||
|
private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, array $bucket = array()): array
|
||||||
|
{
|
||||||
|
$requires = $package->getRequires();
|
||||||
|
|
||||||
|
foreach ($repo->getPackages() as $candidate) {
|
||||||
|
foreach ($candidate->getNames() as $name) {
|
||||||
|
if (isset($requires[$name])) {
|
||||||
|
if (!in_array($candidate, $bucket, true)) {
|
||||||
|
$bucket[] = $candidate;
|
||||||
|
$bucket = $this->filterRequiredPackages($repo, $candidate, $bucket);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bucket;
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ use Composer\Util\Filesystem;
|
||||||
use Composer\Util\Platform;
|
use Composer\Util\Platform;
|
||||||
use Composer\Util\ProcessExecutor;
|
use Composer\Util\ProcessExecutor;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a package as new project into new directory.
|
* Install a package as new project into new directory.
|
||||||
|
@ -90,6 +91,8 @@ class CreateProjectCommand extends BaseCommand
|
||||||
new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deleting the vcs folder.'),
|
new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deleting the vcs folder.'),
|
||||||
new InputOption('remove-vcs', null, InputOption::VALUE_NONE, 'Whether to force deletion of the vcs folder without prompting.'),
|
new InputOption('remove-vcs', null, InputOption::VALUE_NONE, 'Whether to force deletion of the vcs folder without prompting.'),
|
||||||
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Whether to skip installation of the package dependencies.'),
|
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Whether to skip installation of the package dependencies.'),
|
||||||
|
new InputOption('no-audit', null, InputOption::VALUE_NONE, 'Whether to skip auditing of the installed package dependencies.'),
|
||||||
|
new InputOption('audit-format', null, InputOption::VALUE_REQUIRED, 'Audit output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_SUMMARY, Auditor::FORMATS),
|
||||||
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'),
|
new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'),
|
||||||
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
|
new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'),
|
||||||
new InputOption('ask', null, InputOption::VALUE_NONE, 'Whether to ask for project directory.'),
|
new InputOption('ask', null, InputOption::VALUE_NONE, 'Whether to ask for project directory.'),
|
||||||
|
@ -259,7 +262,9 @@ EOT
|
||||||
->setSuggestedPackagesReporter($this->suggestedPackagesReporter)
|
->setSuggestedPackagesReporter($this->suggestedPackagesReporter)
|
||||||
->setOptimizeAutoloader($config->get('optimize-autoloader'))
|
->setOptimizeAutoloader($config->get('optimize-autoloader'))
|
||||||
->setClassMapAuthoritative($config->get('classmap-authoritative'))
|
->setClassMapAuthoritative($config->get('classmap-authoritative'))
|
||||||
->setApcuAutoloader($config->get('apcu-autoloader'));
|
->setApcuAutoloader($config->get('apcu-autoloader'))
|
||||||
|
->setAudit(!$input->getOption('no-audit'))
|
||||||
|
->setAuditFormat($input->getOption('audit-format'));
|
||||||
|
|
||||||
if (!$composer->getLocker()->isLocked()) {
|
if (!$composer->getLocker()->isLocked()) {
|
||||||
$installer->setUpdate(true);
|
$installer->setUpdate(true);
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace Composer\Command;
|
||||||
use Composer\Installer;
|
use Composer\Installer;
|
||||||
use Composer\Plugin\CommandEvent;
|
use Composer\Plugin\CommandEvent;
|
||||||
use Composer\Plugin\PluginEvents;
|
use Composer\Plugin\PluginEvents;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
use Composer\Util\HttpDownloader;
|
use Composer\Util\HttpDownloader;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Composer\Console\Input\InputOption;
|
use Composer\Console\Input\InputOption;
|
||||||
|
@ -51,6 +52,8 @@ class InstallCommand extends BaseCommand
|
||||||
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
|
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
|
||||||
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
||||||
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Do not use, only defined here to catch misuse of the install command.'),
|
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Do not use, only defined here to catch misuse of the install command.'),
|
||||||
|
new InputOption('no-audit', null, InputOption::VALUE_NONE, 'Skip the audit step after installation is complete.'),
|
||||||
|
new InputOption('audit-format', null, InputOption::VALUE_REQUIRED, 'Audit output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_SUMMARY, Auditor::FORMATS),
|
||||||
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
|
new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'),
|
||||||
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
|
new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
|
||||||
new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
|
new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'),
|
||||||
|
@ -130,6 +133,8 @@ EOT
|
||||||
->setClassMapAuthoritative($authoritative)
|
->setClassMapAuthoritative($authoritative)
|
||||||
->setApcuAutoloader($apcu, $apcuPrefix)
|
->setApcuAutoloader($apcu, $apcuPrefix)
|
||||||
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
||||||
|
->setAudit(!$input->getOption('no-audit'))
|
||||||
|
->setAuditFormat($input->getOption('audit-format'))
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($input->getOption('no-plugins')) {
|
if ($input->getOption('no-plugins')) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ use Composer\Console\Input\InputOption;
|
||||||
use Composer\Console\Input\InputArgument;
|
use Composer\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Composer\Package\BasePackage;
|
use Composer\Package\BasePackage;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pierre du Plessis <pdples@gmail.com>
|
* @author Pierre du Plessis <pdples@gmail.com>
|
||||||
|
@ -49,6 +50,8 @@ class RemoveCommand extends BaseCommand
|
||||||
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
||||||
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'),
|
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'),
|
||||||
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
||||||
|
new InputOption('no-audit', null, InputOption::VALUE_NONE, 'Skip the audit step after updating the composer.lock file.'),
|
||||||
|
new InputOption('audit-format', null, InputOption::VALUE_REQUIRED, 'Audit output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_SUMMARY, Auditor::FORMATS),
|
||||||
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
|
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
|
||||||
new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated with explicit dependencies. (Deprecrated, is now default behavior)'),
|
new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated with explicit dependencies. (Deprecrated, is now default behavior)'),
|
||||||
new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
|
new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
|
||||||
|
@ -282,6 +285,8 @@ EOT
|
||||||
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
|
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
|
||||||
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
||||||
->setDryRun($dryRun)
|
->setDryRun($dryRun)
|
||||||
|
->setAudit(!$input->getOption('no-audit'))
|
||||||
|
->setAuditFormat($input->getOption('audit-format'))
|
||||||
;
|
;
|
||||||
|
|
||||||
// if no lock is present, we do not do a partial update as
|
// if no lock is present, we do not do a partial update as
|
||||||
|
|
|
@ -31,6 +31,7 @@ use Composer\Plugin\PluginEvents;
|
||||||
use Composer\Repository\CompositeRepository;
|
use Composer\Repository\CompositeRepository;
|
||||||
use Composer\Repository\PlatformRepository;
|
use Composer\Repository\PlatformRepository;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
use Composer\Util\Silencer;
|
use Composer\Util\Silencer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,6 +80,8 @@ class RequireCommand extends BaseCommand
|
||||||
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
||||||
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'),
|
new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'),
|
||||||
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
||||||
|
new InputOption('no-audit', null, InputOption::VALUE_NONE, 'Skip the audit step after updating the composer.lock file.'),
|
||||||
|
new InputOption('audit-format', null, InputOption::VALUE_REQUIRED, 'Audit output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_SUMMARY, Auditor::FORMATS),
|
||||||
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
|
new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'),
|
||||||
new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'),
|
new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'),
|
||||||
new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
|
new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'),
|
||||||
|
@ -415,6 +418,8 @@ EOT
|
||||||
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
||||||
->setPreferStable($input->getOption('prefer-stable'))
|
->setPreferStable($input->getOption('prefer-stable'))
|
||||||
->setPreferLowest($input->getOption('prefer-lowest'))
|
->setPreferLowest($input->getOption('prefer-lowest'))
|
||||||
|
->setAudit(!$input->getOption('no-audit'))
|
||||||
|
->setAuditFormat($input->getOption('audit-format'))
|
||||||
;
|
;
|
||||||
|
|
||||||
// if no lock is present, or the file is brand new, we do not do a
|
// if no lock is present, or the file is brand new, we do not do a
|
||||||
|
|
|
@ -25,6 +25,7 @@ use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
use Composer\Util\HttpDownloader;
|
use Composer\Util\HttpDownloader;
|
||||||
use Composer\Semver\Constraint\MultiConstraint;
|
use Composer\Semver\Constraint\MultiConstraint;
|
||||||
use Composer\Package\Link;
|
use Composer\Package\Link;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
use Symfony\Component\Console\Helper\Table;
|
use Symfony\Component\Console\Helper\Table;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Composer\Console\Input\InputOption;
|
use Composer\Console\Input\InputOption;
|
||||||
|
@ -60,6 +61,8 @@ class UpdateCommand extends BaseCommand
|
||||||
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
|
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
|
||||||
new InputOption('lock', null, InputOption::VALUE_NONE, 'Overwrites the lock file hash to suppress warning about the lock file being out of date without updating package versions. Package metadata like mirrors and URLs are updated if they changed.'),
|
new InputOption('lock', null, InputOption::VALUE_NONE, 'Overwrites the lock file hash to suppress warning about the lock file being out of date without updating package versions. Package metadata like mirrors and URLs are updated if they changed.'),
|
||||||
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'),
|
||||||
|
new InputOption('no-audit', null, InputOption::VALUE_NONE, 'Skip the audit step after updating the composer.lock file.'),
|
||||||
|
new InputOption('audit-format', null, InputOption::VALUE_REQUIRED, 'Audit output format. Must be "table", "plain", or "summary".', Auditor::FORMAT_SUMMARY, Auditor::FORMATS),
|
||||||
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
|
new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'),
|
||||||
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'),
|
new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'),
|
||||||
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'),
|
||||||
|
@ -229,6 +232,8 @@ EOT
|
||||||
->setPreferStable($input->getOption('prefer-stable'))
|
->setPreferStable($input->getOption('prefer-stable'))
|
||||||
->setPreferLowest($input->getOption('prefer-lowest'))
|
->setPreferLowest($input->getOption('prefer-lowest'))
|
||||||
->setTemporaryConstraints($temporaryConstraints)
|
->setTemporaryConstraints($temporaryConstraints)
|
||||||
|
->setAudit(!$input->getOption('no-audit'))
|
||||||
|
->setAuditFormat($input->getOption('audit-format'))
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($input->getOption('no-plugins')) {
|
if ($input->getOption('no-plugins')) {
|
||||||
|
|
|
@ -529,6 +529,7 @@ class Application extends BaseApplication
|
||||||
new Command\UpdateCommand(),
|
new Command\UpdateCommand(),
|
||||||
new Command\SearchCommand(),
|
new Command\SearchCommand(),
|
||||||
new Command\ValidateCommand(),
|
new Command\ValidateCommand(),
|
||||||
|
new Command\AuditCommand(),
|
||||||
new Command\ShowCommand(),
|
new Command\ShowCommand(),
|
||||||
new Command\SuggestsCommand(),
|
new Command\SuggestsCommand(),
|
||||||
new Command\RequireCommand(),
|
new Command\RequireCommand(),
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace Composer\IO;
|
||||||
use Composer\Question\StrictConfirmationQuestion;
|
use Composer\Question\StrictConfirmationQuestion;
|
||||||
use Symfony\Component\Console\Helper\HelperSet;
|
use Symfony\Component\Console\Helper\HelperSet;
|
||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
|
use Symfony\Component\Console\Helper\Table;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
@ -342,6 +343,14 @@ class ConsoleIO extends BaseIO
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Table
|
||||||
|
*/
|
||||||
|
public function getTable(): Table
|
||||||
|
{
|
||||||
|
return new Table($this->output);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return OutputInterface
|
* @return OutputInterface
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,6 +27,7 @@ use Composer\DependencyResolver\Solver;
|
||||||
use Composer\DependencyResolver\SolverProblemsException;
|
use Composer\DependencyResolver\SolverProblemsException;
|
||||||
use Composer\DependencyResolver\PolicyInterface;
|
use Composer\DependencyResolver\PolicyInterface;
|
||||||
use Composer\Downloader\DownloadManager;
|
use Composer\Downloader\DownloadManager;
|
||||||
|
use Composer\Downloader\TransportException;
|
||||||
use Composer\EventDispatcher\EventDispatcher;
|
use Composer\EventDispatcher\EventDispatcher;
|
||||||
use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter;
|
use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter;
|
||||||
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
|
use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory;
|
||||||
|
@ -61,6 +62,7 @@ use Composer\Repository\RepositoryManager;
|
||||||
use Composer\Repository\LockArrayRepository;
|
use Composer\Repository\LockArrayRepository;
|
||||||
use Composer\Script\ScriptEvents;
|
use Composer\Script\ScriptEvents;
|
||||||
use Composer\Semver\Constraint\ConstraintInterface;
|
use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
use Composer\Util\Platform;
|
use Composer\Util\Platform;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -163,6 +165,10 @@ class Installer
|
||||||
protected $writeLock;
|
protected $writeLock;
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
protected $executeOperations = true;
|
protected $executeOperations = true;
|
||||||
|
/** @var bool */
|
||||||
|
protected $audit = true;
|
||||||
|
/** @var string */
|
||||||
|
protected $auditFormat = Auditor::FORMAT_TABLE;
|
||||||
|
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
protected $updateMirrors = false;
|
protected $updateMirrors = false;
|
||||||
|
@ -381,6 +387,23 @@ class Installer
|
||||||
gc_enable();
|
gc_enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->audit) {
|
||||||
|
$packages = $localRepo->getCanonicalPackages();
|
||||||
|
if (count($packages) > 0) {
|
||||||
|
try {
|
||||||
|
$auditor = new Auditor(Factory::createHttpDownloader($this->io, $this->config));
|
||||||
|
$auditor->audit($this->io, $packages, $this->auditFormat);
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
$this->io->error('Failed to audit installed packages.');
|
||||||
|
if ($this->io->isVerbose()) {
|
||||||
|
$this->io->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->io->writeError('No packages - skipping audit.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1071,10 +1094,13 @@ class Installer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, ConstraintInterface> $constraints
|
* @param array<string, ConstraintInterface> $constraints
|
||||||
|
* @return Installer
|
||||||
*/
|
*/
|
||||||
public function setTemporaryConstraints(array $constraints): void
|
public function setTemporaryConstraints(array $constraints): self
|
||||||
{
|
{
|
||||||
$this->temporaryConstraints = $constraints;
|
$this->temporaryConstraints = $constraints;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1418,6 +1444,32 @@ class Installer
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should an audit be run after installation is complete?
|
||||||
|
*
|
||||||
|
* @param boolean $audit
|
||||||
|
* @return Installer
|
||||||
|
*/
|
||||||
|
public function setAudit(bool $audit): self
|
||||||
|
{
|
||||||
|
$this->audit = $audit;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What format should be used for audit output?
|
||||||
|
*
|
||||||
|
* @param string $auditFormat
|
||||||
|
* @return Installer
|
||||||
|
*/
|
||||||
|
public function setAuditFormat(string $auditFormat): self
|
||||||
|
{
|
||||||
|
$this->auditFormat = $auditFormat;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables plugins.
|
* Disables plugins.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Composer\Util;
|
||||||
|
|
||||||
|
use Composer\IO\ConsoleIO;
|
||||||
|
use Composer\IO\IOInterface;
|
||||||
|
use Composer\Package\PackageInterface;
|
||||||
|
use Composer\Semver\Semver;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class Auditor
|
||||||
|
{
|
||||||
|
private const API_URL = 'https://packagist.org/api/security-advisories/';
|
||||||
|
|
||||||
|
public const FORMAT_TABLE = 'table';
|
||||||
|
|
||||||
|
public const FORMAT_PLAIN = 'plain';
|
||||||
|
|
||||||
|
public const FORMAT_SUMMARY = 'summary';
|
||||||
|
|
||||||
|
public const FORMATS = [
|
||||||
|
self::FORMAT_TABLE,
|
||||||
|
self::FORMAT_PLAIN,
|
||||||
|
self::FORMAT_SUMMARY,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var HttpDownloader */
|
||||||
|
private $httpDownloader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param HttpDownloader $httpDownloader
|
||||||
|
*/
|
||||||
|
public function __construct(HttpDownloader $httpDownloader)
|
||||||
|
{
|
||||||
|
$this->httpDownloader = $httpDownloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOInterface $io
|
||||||
|
* @param PackageInterface[] $packages
|
||||||
|
* @param self::FORMAT_* $format The format that will be used to output audit results.
|
||||||
|
* @param bool $warningOnly If true, outputs a warning. If false, outputs an error.
|
||||||
|
* @return int
|
||||||
|
* @throws InvalidArgumentException If no packages are passed in
|
||||||
|
*/
|
||||||
|
public function audit(IOInterface $io, array $packages, string $format, bool $warningOnly = true): int
|
||||||
|
{
|
||||||
|
$advisories = $this->getAdvisories($packages);
|
||||||
|
$errorOrWarn = $warningOnly ? 'warning' : 'error';
|
||||||
|
if (count($advisories) > 0) {
|
||||||
|
$numAdvisories = $this->countAdvisories($advisories);
|
||||||
|
$plurality = $numAdvisories === 1 ? 'y' : 'ies';
|
||||||
|
$io->writeError("<$errorOrWarn>Found $numAdvisories security vulnerability advisor$plurality:</$errorOrWarn>");
|
||||||
|
$this->outputAdvisories($io, $advisories, $format);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
$io->writeError('<info>No security vulnerability advisories found</info>');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get advisories from packagist.org
|
||||||
|
*
|
||||||
|
* @param PackageInterface[] $packages
|
||||||
|
* @param ?int $updatedSince Timestamp
|
||||||
|
* @param bool $filterByVersion Filter by the package versions if true
|
||||||
|
* @return string[][][]
|
||||||
|
* @throws InvalidArgumentException If no packages and no updatedSince timestamp are passed in
|
||||||
|
*/
|
||||||
|
public function getAdvisories(array $packages = [], int $updatedSince = null, bool $filterByVersion = true): array
|
||||||
|
{
|
||||||
|
if (count($packages) === 0 && $updatedSince === null) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
'At least one package or an $updatedSince timestamp must be passed in.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($packages) === 0 && $filterByVersion) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add updatedSince query to URL if passed in
|
||||||
|
$url = self::API_URL;
|
||||||
|
if ($updatedSince !== null) {
|
||||||
|
$url .= "?updatedSince=$updatedSince";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get advisories from API
|
||||||
|
$response = $this->httpDownloader->get($url, $this->createPostOptions($packages));
|
||||||
|
$advisories = $response->decodeJson()['advisories'];
|
||||||
|
|
||||||
|
if (count($advisories) > 0 && $filterByVersion) {
|
||||||
|
return $this->filterAdvisories($advisories, $packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $advisories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param PackageInterface[] $packages
|
||||||
|
* @return string[]
|
||||||
|
* @phpstan-return array<string, array<string, array<int, string>|int|string>>
|
||||||
|
*/
|
||||||
|
private function createPostOptions(array $packages): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => ['Content-type: application/x-www-form-urlencoded'],
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (count($packages) > 0) {
|
||||||
|
$content = ['packages' => []];
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
$content['packages'][] = $package->getName();
|
||||||
|
}
|
||||||
|
$options['http']['content'] = http_build_query($content);
|
||||||
|
}
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[][][] $advisories
|
||||||
|
* @param PackageInterface[] $packages
|
||||||
|
* @return string[][][]
|
||||||
|
*/
|
||||||
|
private function filterAdvisories(array $advisories, array $packages): array
|
||||||
|
{
|
||||||
|
$filteredAdvisories = [];
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
if (array_key_exists($package->getName(), $advisories)) {
|
||||||
|
foreach ($advisories[$package->getName()] as $advisory) {
|
||||||
|
if (Semver::satisfies($package->getVersion(), $advisory['affectedVersions'])) {
|
||||||
|
$filteredAdvisories[$package->getName()][] = $advisory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $filteredAdvisories;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[][][] $advisories
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
private function countAdvisories(array $advisories): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
foreach ($advisories as $packageAdvisories) {
|
||||||
|
$count += count($packageAdvisories);
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOInterface $io
|
||||||
|
* @param string[][][] $advisories
|
||||||
|
* @param self::FORMAT_* $format The format that will be used to output audit results.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function outputAdvisories(IOInterface $io, array $advisories, string $format): void
|
||||||
|
{
|
||||||
|
switch ($format) {
|
||||||
|
case self::FORMAT_TABLE:
|
||||||
|
if (!($io instanceof ConsoleIO)) {
|
||||||
|
throw new InvalidArgumentException('Cannot use table format with ' . get_class($io));
|
||||||
|
}
|
||||||
|
$this->outputAvisoriesTable($io, $advisories);
|
||||||
|
return;
|
||||||
|
case self::FORMAT_PLAIN:
|
||||||
|
$this->outputAdvisoriesPlain($io, $advisories);
|
||||||
|
return;
|
||||||
|
case self::FORMAT_SUMMARY:
|
||||||
|
// We've already output the number of advisories in audit()
|
||||||
|
$io->writeError('Run composer audit for a full list of advisories.');
|
||||||
|
default:
|
||||||
|
throw new InvalidArgumentException('Invalid format.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConsoleIO $io
|
||||||
|
* @param string[][][] $advisories
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function outputAvisoriesTable(ConsoleIO $io, array $advisories): void
|
||||||
|
{
|
||||||
|
foreach ($advisories as $package => $packageAdvisories) {
|
||||||
|
foreach ($packageAdvisories as $advisory) {
|
||||||
|
$io->getTable()
|
||||||
|
->setHorizontal()
|
||||||
|
->setHeaders([
|
||||||
|
'Package',
|
||||||
|
'CVE',
|
||||||
|
'Title',
|
||||||
|
'URL',
|
||||||
|
'Affected versions',
|
||||||
|
'Reported at',
|
||||||
|
])
|
||||||
|
->addRow([
|
||||||
|
$package,
|
||||||
|
$advisory['cve'] ?: 'NO CVE',
|
||||||
|
$advisory['title'],
|
||||||
|
$advisory['link'],
|
||||||
|
$advisory['affectedVersions'],
|
||||||
|
$advisory['reportedAt'],
|
||||||
|
])
|
||||||
|
->setColumnWidth(1, 80)
|
||||||
|
->setColumnMaxWidth(1, 80)
|
||||||
|
->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOInterface $io
|
||||||
|
* @param string[][][] $advisories
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function outputAdvisoriesPlain(IOInterface $io, array $advisories): void
|
||||||
|
{
|
||||||
|
$error = [];
|
||||||
|
$firstAdvisory = true;
|
||||||
|
foreach ($advisories as $package => $packageAdvisories) {
|
||||||
|
foreach ($packageAdvisories as $advisory) {
|
||||||
|
if (!$firstAdvisory) {
|
||||||
|
$error[] = '--------';
|
||||||
|
}
|
||||||
|
$cve = $advisory['cve'] ?: 'NO CVE';
|
||||||
|
$error[] = "Package: $package";
|
||||||
|
$error[] = "CVE: $cve";
|
||||||
|
$error[] = "Title: {$advisory['title']}";
|
||||||
|
$error[] = "URL: {$advisory['link']}";
|
||||||
|
$error[] = "Affected versions: {$advisory['affectedVersions']}";
|
||||||
|
$error[] = "Reported at: {$advisory['reportedAt']}";
|
||||||
|
$firstAdvisory = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$io->writeError($error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ class UpdateCommandTest extends TestCase
|
||||||
$this->initTempComposer($composerJson);
|
$this->initTempComposer($composerJson);
|
||||||
|
|
||||||
$appTester = $this->getApplicationTester();
|
$appTester = $this->getApplicationTester();
|
||||||
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true], $command));
|
$appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command));
|
||||||
|
|
||||||
$this->assertSame(trim($expected), trim($appTester->getDisplay(true)));
|
$this->assertSame(trim($expected), trim($appTester->getDisplay(true)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,7 @@ class InstallerTest extends TestCase
|
||||||
$autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
|
$autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock();
|
||||||
|
|
||||||
$installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);
|
$installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator);
|
||||||
|
$installer->setAudit(false);
|
||||||
$result = $installer->run();
|
$result = $installer->run();
|
||||||
|
|
||||||
$output = str_replace("\r", '', $io->getOutput());
|
$output = str_replace("\r", '', $io->getOutput());
|
||||||
|
@ -395,7 +396,8 @@ class InstallerTest extends TestCase
|
||||||
$installer
|
$installer
|
||||||
->setDevMode(!$input->getOption('no-dev'))
|
->setDevMode(!$input->getOption('no-dev'))
|
||||||
->setDryRun($input->getOption('dry-run'))
|
->setDryRun($input->getOption('dry-run'))
|
||||||
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs));
|
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs))
|
||||||
|
->setAudit(false);
|
||||||
|
|
||||||
return $installer->run();
|
return $installer->run();
|
||||||
});
|
});
|
||||||
|
@ -440,7 +442,8 @@ class InstallerTest extends TestCase
|
||||||
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
|
->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies)
|
||||||
->setPreferStable($input->getOption('prefer-stable'))
|
->setPreferStable($input->getOption('prefer-stable'))
|
||||||
->setPreferLowest($input->getOption('prefer-lowest'))
|
->setPreferLowest($input->getOption('prefer-lowest'))
|
||||||
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs));
|
->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs))
|
||||||
|
->setAudit(false);
|
||||||
|
|
||||||
return $installer->run();
|
return $installer->run();
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,399 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Composer\Test\Util;
|
||||||
|
|
||||||
|
use Composer\IO\IOInterface;
|
||||||
|
use Composer\Package\Package;
|
||||||
|
use Composer\Test\TestCase;
|
||||||
|
use Composer\Util\Auditor;
|
||||||
|
use Composer\Util\Http\Response;
|
||||||
|
use Composer\Util\HttpDownloader;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
|
||||||
|
class AuditorTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var IOInterface&\PHPUnit\Framework\MockObject\MockObject */
|
||||||
|
private $io;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->io = $this
|
||||||
|
->getMockBuilder(IOInterface::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function auditProvider()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Test no advisories returns 0
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [
|
||||||
|
new Package('vendor1/package2', '9.0.0', '9.0.0'),
|
||||||
|
new Package('vendor1/package1', '9.0.0', '9.0.0'),
|
||||||
|
new Package('vendor3/package1', '9.0.0', '9.0.0'),
|
||||||
|
],
|
||||||
|
'warningOnly' => true,
|
||||||
|
],
|
||||||
|
'expected' => 0,
|
||||||
|
'message' => 'Test no advisories returns 0',
|
||||||
|
],
|
||||||
|
// Test with advisories returns 1
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [
|
||||||
|
new Package('vendor1/package2', '9.0.0', '9.0.0'),
|
||||||
|
new Package('vendor1/package1', '8.2.1', '8.2.1'),
|
||||||
|
new Package('vendor3/package1', '9.0.0', '9.0.0'),
|
||||||
|
],
|
||||||
|
'warningOnly' => true,
|
||||||
|
],
|
||||||
|
'expected' => 1,
|
||||||
|
'message' => 'Test with advisories returns 1',
|
||||||
|
],
|
||||||
|
// Test no packages throws InvalidArgumentException
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [],
|
||||||
|
'warningOnly' => true,
|
||||||
|
],
|
||||||
|
'expected' => 1,
|
||||||
|
'message' => 'Test no packages throws InvalidArgumentException',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider auditProvider
|
||||||
|
* @phpstan-param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
public function testAudit(array $data, int $expected, string $message): void
|
||||||
|
{
|
||||||
|
if (count($data['packages']) === 0) {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
}
|
||||||
|
$auditor = new Auditor($this->getHttpDownloader());
|
||||||
|
$result = $auditor->audit($this->io, $data['packages'], Auditor::FORMAT_PLAIN, $data['warningOnly']);
|
||||||
|
$this->assertSame($expected, $result, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function advisoriesProvider()
|
||||||
|
{
|
||||||
|
$advisories = static::getMockAdvisories(null);
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [
|
||||||
|
new Package('vendor1/package1', '8.2.1', '8.2.1'),
|
||||||
|
new Package('vendor1/package2', '3.1.0', '3.1.0'),
|
||||||
|
// Check a package with no advisories at all doesn't cause any issues
|
||||||
|
new Package('vendor5/package2', '5.0.0', '5.0.0'),
|
||||||
|
],
|
||||||
|
'updatedSince' => null,
|
||||||
|
'filterByVersion' => false
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'vendor1/package1' => $advisories['vendor1/package1'],
|
||||||
|
'vendor1/package2' => $advisories['vendor1/package2'],
|
||||||
|
],
|
||||||
|
'message' => 'Check not filtering by version',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [
|
||||||
|
new Package('vendor1/package1', '8.2.1', '8.2.1'),
|
||||||
|
new Package('vendor1/package2', '3.1.0', '3.1.0'),
|
||||||
|
// Check a package with no advisories at all doesn't cause any issues
|
||||||
|
new Package('vendor5/package2', '5.0.0', '5.0.0'),
|
||||||
|
],
|
||||||
|
'updatedSince' => null,
|
||||||
|
'filterByVersion' => true
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'vendor1/package1' => [
|
||||||
|
$advisories['vendor1/package1'][1],
|
||||||
|
$advisories['vendor1/package1'][2],
|
||||||
|
],
|
||||||
|
'vendor1/package2' => [
|
||||||
|
$advisories['vendor1/package2'][0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'message' => 'Check filter by version',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [
|
||||||
|
new Package('vendor1/package1', '8.2.1', '8.2.1'),
|
||||||
|
new Package('vendor1/package2', '5.0.0', '5.0.0'),
|
||||||
|
new Package('vendor2/package1', '3.0.0', '3.0.0'),
|
||||||
|
],
|
||||||
|
'updatedSince' => 1335939007,
|
||||||
|
'filterByVersion' => false
|
||||||
|
],
|
||||||
|
'expected' => [
|
||||||
|
'vendor1/package1' => [
|
||||||
|
$advisories['vendor1/package1'][0],
|
||||||
|
$advisories['vendor1/package1'][1],
|
||||||
|
],
|
||||||
|
'vendor1/package2' => [
|
||||||
|
$advisories['vendor1/package2'][0],
|
||||||
|
],
|
||||||
|
'vendor2/package1' => [
|
||||||
|
$advisories['vendor2/package1'][0],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'message' => 'Check updatedSince is passed through to the API',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [],
|
||||||
|
'updatedSince' => 1335939007,
|
||||||
|
'filterByVersion' => true
|
||||||
|
],
|
||||||
|
'expected' => [],
|
||||||
|
'message' => 'No packages and filterByVersion === true should return 0 results',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [],
|
||||||
|
'updatedSince' => 0,
|
||||||
|
'filterByVersion' => false
|
||||||
|
],
|
||||||
|
// All advisories expected with no packages and updatedSince === 0
|
||||||
|
'expected' => $advisories,
|
||||||
|
'message' => 'No packages and updatedSince === 0 should NOT throw LogicException',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'packages' => [],
|
||||||
|
'updatedSince' => null,
|
||||||
|
'filterByVersion' => false
|
||||||
|
],
|
||||||
|
'expected' => [],
|
||||||
|
'message' => 'No packages and updatedSince === null should throw LogicException',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider advisoriesProvider
|
||||||
|
* @phpstan-param array<string, mixed> $data
|
||||||
|
* @phpstan-param string[][][] $expected
|
||||||
|
*/
|
||||||
|
public function testGetAdvisories(array $data, array $expected, string $message): void
|
||||||
|
{
|
||||||
|
if (count($data['packages']) === 0 && $data['updatedSince'] === null) {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
}
|
||||||
|
$auditor = new Auditor($this->getHttpDownloader(), Auditor::FORMAT_PLAIN);
|
||||||
|
$result = $auditor->getAdvisories($data['packages'], $data['updatedSince'], $data['filterByVersion']);
|
||||||
|
$this->assertSame($expected, $result, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HttpDownloader&MockObject
|
||||||
|
*/
|
||||||
|
private function getHttpDownloader(): MockObject
|
||||||
|
{
|
||||||
|
$httpDownloader = $this
|
||||||
|
->getMockBuilder(HttpDownloader::class)
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->onlyMethods(['get'])
|
||||||
|
->getMock();
|
||||||
|
|
||||||
|
$callback = function(string $url, array $options) {
|
||||||
|
parse_str(parse_url($url, PHP_URL_QUERY) ?? '', $query);
|
||||||
|
$updatedSince = null;
|
||||||
|
if (isset($query['updatedSince'])) {
|
||||||
|
$updatedSince = $query['updatedSince'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$advisories = AuditorTest::getMockAdvisories($updatedSince);
|
||||||
|
|
||||||
|
// If the mock API request is for specific packages, only include advisories for those packages
|
||||||
|
if (isset($options['http']['content'])) {
|
||||||
|
parse_str($options['http']['content'], $body);
|
||||||
|
$packages = $body['packages'];
|
||||||
|
foreach ($advisories as $package => $data) {
|
||||||
|
if (!in_array($package, $packages)) {
|
||||||
|
unset($advisories[$package]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(['url' => 'https://packagist.org/api/security-advisories/'], 200, [], json_encode(['advisories' => $advisories]));
|
||||||
|
};
|
||||||
|
|
||||||
|
$httpDownloader
|
||||||
|
->method('get')
|
||||||
|
->willReturnCallback($callback);
|
||||||
|
|
||||||
|
return $httpDownloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMockAdvisories(?int $updatedSince)
|
||||||
|
{
|
||||||
|
$advisories = [
|
||||||
|
'vendor1/package1' => [
|
||||||
|
[
|
||||||
|
'advisoryId' => 'ID1',
|
||||||
|
'packageName' => 'vendor1/package1',
|
||||||
|
'title' => 'advisory1',
|
||||||
|
'link' => 'https://advisory.example.com/advisory1',
|
||||||
|
'cve' => 'CVE1',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source1',
|
||||||
|
'remoteId' => 'RemoteID1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2022-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'advisoryId' => 'ID4',
|
||||||
|
'packageName' => 'vendor1/package1',
|
||||||
|
'title' => 'advisory4',
|
||||||
|
'link' => 'https://advisory.example.com/advisory4',
|
||||||
|
'cve' => 'CVE3',
|
||||||
|
'affectedVersions' => '>=8,<8.2.2|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID2',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2022-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'vendor1/package2' => [
|
||||||
|
[
|
||||||
|
'advisoryId' => 'ID2',
|
||||||
|
'packageName' => 'vendor1/package2',
|
||||||
|
'title' => 'advisory2',
|
||||||
|
'link' => 'https://advisory.example.com/advisory2',
|
||||||
|
'cve' => '',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source1',
|
||||||
|
'remoteId' => 'RemoteID2',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2022-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'vendorx/packagex' => [
|
||||||
|
[
|
||||||
|
'advisoryId' => 'IDx',
|
||||||
|
'packageName' => 'vendorx/packagex',
|
||||||
|
'title' => 'advisory7',
|
||||||
|
'link' => 'https://advisory.example.com/advisory7',
|
||||||
|
'cve' => 'CVE5',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2015-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'vendor2/package1' => [
|
||||||
|
[
|
||||||
|
'advisoryId' => 'ID3',
|
||||||
|
'packageName' => 'vendor2/package1',
|
||||||
|
'title' => 'advisory3',
|
||||||
|
'link' => 'https://advisory.example.com/advisory3',
|
||||||
|
'cve' => 'CVE2',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2022-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Intentionally allow updatedSince === 0 to include these advisories
|
||||||
|
if (!$updatedSince) {
|
||||||
|
$advisories['vendor1/package1'][] = [
|
||||||
|
'advisoryId' => 'ID5',
|
||||||
|
'packageName' => 'vendor1/package1',
|
||||||
|
'title' => 'advisory5',
|
||||||
|
'link' => 'https://advisory.example.com/advisory5',
|
||||||
|
'cve' => '',
|
||||||
|
'affectedVersions' => '>=8,<8.2.2|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source1',
|
||||||
|
'remoteId' => 'RemoteID3',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
];
|
||||||
|
$advisories['vendor2/package1'][] = [
|
||||||
|
'advisoryId' => 'ID6',
|
||||||
|
'packageName' => 'vendor2/package1',
|
||||||
|
'title' => 'advisory6',
|
||||||
|
'link' => 'https://advisory.example.com/advisory6',
|
||||||
|
'cve' => 'CVE4',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID3',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2015-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
];
|
||||||
|
$advisories['vendory/packagey'][] = [
|
||||||
|
'advisoryId' => 'IDy',
|
||||||
|
'packageName' => 'vendory/packagey',
|
||||||
|
'title' => 'advisory7',
|
||||||
|
'link' => 'https://advisory.example.com/advisory7',
|
||||||
|
'cve' => 'CVE5',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2015-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
];
|
||||||
|
$advisories['vendor3/package1'][] = [
|
||||||
|
'advisoryId' => 'ID7',
|
||||||
|
'packageName' => 'vendor3/package1',
|
||||||
|
'title' => 'advisory7',
|
||||||
|
'link' => 'https://advisory.example.com/advisory7',
|
||||||
|
'cve' => 'CVE5',
|
||||||
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
|
'sources' => [
|
||||||
|
[
|
||||||
|
'name' => 'source2',
|
||||||
|
'remoteId' => 'RemoteID4',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'reportedAt' => '2015-05-25 13:21:00',
|
||||||
|
'composerRepository' => 'https://packagist.org',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $advisories;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue