Merge pull request #10898 from Seldaek/meta_advisories
Move security advisory loading to repositoriespull/10902/head
commit
f9db69ae1f
|
@ -109,7 +109,7 @@ resolution.
|
|||
* **--no-autoloader:** Skips autoloader generation.
|
||||
* **--no-progress:** Removes the progress display that can mess with some
|
||||
terminals or scripts which don't handle backspace characters.
|
||||
* **--no-audit:** Does not run the audit step after installation is complete.
|
||||
* **--audit:** Run an audit 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
|
||||
autoloader. This is recommended especially for production, but can take
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#^Binary operation \"\\.\" between non\\-empty\\-string and array\\|string\\|null results in an error\\.$#"
|
||||
message: "#^Parameter \\#2 \\$advisories of method Composer\\\\Advisory\\\\Auditor\\:\\:outputAdvisories\\(\\) expects array\\<string, array\\<Composer\\\\Advisory\\\\SecurityAdvisory\\>\\>, array\\<string, array\\<Composer\\\\Advisory\\\\PartialSecurityAdvisory\\>\\> given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
path: ../src/Composer/Advisory/Auditor.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method writeError\\(\\) on Composer\\\\IO\\\\IOInterface\\|null\\.$#"
|
||||
count: 2
|
||||
message: "#^Binary operation \"\\.\" between non\\-empty\\-string and array\\|string\\|null results in an error\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
|
@ -20,11 +20,6 @@ parameters:
|
|||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in &&, Composer\\\\IO\\\\IOInterface\\|null given on the left side\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in &&, array\\<string, array\\<int\\|string, array\\<string\\>\\|int\\|string\\>\\|bool\\|string\\>\\|bool\\|string\\|null given on the left side\\.$#"
|
||||
count: 1
|
||||
|
@ -55,21 +50,11 @@ parameters:
|
|||
count: 3
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a ternary operator condition, array\\<int, string\\> given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a ternary operator condition, mixed given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, array\\<int, string\\>\\|null given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||
count: 1
|
||||
|
@ -115,11 +100,6 @@ parameters:
|
|||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$excluded of static method Composer\\\\Autoload\\\\ClassMapGenerator\\:\\:createMap\\(\\) expects string\\|null, array\\|string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:isMatch\\(\\) expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
|
@ -165,66 +145,6 @@ parameters:
|
|||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassLoader.php
|
||||
|
||||
-
|
||||
message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getPathname\\(\\) on SplFileInfo\\|string\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||
count: 2
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Method Composer\\\\Autoload\\\\ClassMapGenerator\\:\\:findClasses\\(\\) should return array\\<int, class\\-string\\> but returns array\\<int, string\\>\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in &&, Composer\\\\IO\\\\IOInterface\\|null given on the left side\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in &&, string\\|null given on the left side\\.$#"
|
||||
count: 2
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a negated boolean, string given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, Composer\\\\IO\\\\IOInterface\\|null given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, array\\<int, class\\-string\\> given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$str of function strtr expects string, string\\|false given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#3 \\$baseNamespace of static method Composer\\\\Autoload\\\\ClassMapGenerator\\:\\:filterByNamespace\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#5 \\$basePath of static method Composer\\\\Autoload\\\\ClassMapGenerator\\:\\:filterByNamespace\\(\\) expects string, array\\<string\\>\\|string\\|Traversable\\<mixed, SplFileInfo\\> given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Autoload/ClassMapGenerator.php
|
||||
|
||||
-
|
||||
message: "#^Casting to bool something that's already bool\\.$#"
|
||||
count: 2
|
||||
|
@ -717,7 +637,7 @@ parameters:
|
|||
|
||||
-
|
||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||
count: 3
|
||||
count: 2
|
||||
path: ../src/Composer/Command/RequireCommand.php
|
||||
|
||||
-
|
||||
|
@ -735,11 +655,6 @@ parameters:
|
|||
count: 1
|
||||
path: ../src/Composer/Command/RequireCommand.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a negated boolean, mixed given\\.$#"
|
||||
count: 1
|
||||
path: ../src/Composer/Command/RequireCommand.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
||||
count: 2
|
||||
|
@ -1842,7 +1757,7 @@ parameters:
|
|||
|
||||
-
|
||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||
count: 2
|
||||
count: 1
|
||||
path: ../src/Composer/DependencyResolver/SolverProblemsException.php
|
||||
|
||||
-
|
||||
|
@ -3725,11 +3640,6 @@ parameters:
|
|||
count: 2
|
||||
path: ../src/Composer/Repository/ComposerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a negated boolean, string\\|null given\\.$#"
|
||||
count: 2
|
||||
path: ../src/Composer/Repository/ComposerRepository.php
|
||||
|
||||
-
|
||||
message: "#^Only booleans are allowed in a ternary operator condition, string\\|null given\\.$#"
|
||||
count: 1
|
||||
|
@ -4518,11 +4428,6 @@ parameters:
|
|||
count: 1
|
||||
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\\.$#"
|
||||
count: 1
|
||||
|
@ -5598,21 +5503,6 @@ parameters:
|
|||
count: 2
|
||||
path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Dynamic call to static method Composer\\\\Test\\\\TestCase\\:\\:ensureDirectoryExistsAndClear\\(\\)\\.$#"
|
||||
count: 1
|
||||
path: ../tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$expected of method Composer\\\\Test\\\\Autoload\\\\ClassMapGeneratorTest\\:\\:assertEqualsNormalized\\(\\) expects array\\<class\\-string\\>, array\\<string, string\\> given\\.$#"
|
||||
count: 3
|
||||
path: ../tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$actual of method Composer\\\\Test\\\\Autoload\\\\ClassMapGeneratorTest\\:\\:assertEqualsNormalized\\(\\) expects array\\<class\\-string\\>, array\\<class\\-string, string\\> given\\.$#"
|
||||
count: 3
|
||||
path: ../tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
|
||||
|
||||
-
|
||||
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
|
||||
count: 1
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Advisory;
|
||||
|
||||
use Composer\IO\ConsoleIO;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Package\PackageInterface;
|
||||
use Composer\Repository\RepositorySet;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Auditor
|
||||
{
|
||||
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,
|
||||
];
|
||||
|
||||
/**
|
||||
* @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 Amount of packages with vulnerabilities found
|
||||
* @throws InvalidArgumentException If no packages are passed in
|
||||
*/
|
||||
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true): int
|
||||
{
|
||||
$advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
|
||||
$errorOrWarn = $warningOnly ? 'warning' : 'error';
|
||||
if (count($advisories) > 0) {
|
||||
[$affectedPackages, $totalAdvisories] = $this->countAdvisories($advisories);
|
||||
$plurality = $totalAdvisories === 1 ? 'y' : 'ies';
|
||||
$pkgPlurality = $affectedPackages === 1 ? '' : 's';
|
||||
$punctuation = $format === 'summary' ? '.' : ':';
|
||||
$io->writeError("<$errorOrWarn>Found $totalAdvisories security vulnerability advisor{$plurality} affecting $affectedPackages package{$pkgPlurality}{$punctuation}</$errorOrWarn>");
|
||||
$this->outputAdvisories($io, $advisories, $format);
|
||||
|
||||
return $affectedPackages;
|
||||
}
|
||||
|
||||
$io->writeError('<info>No security vulnerability advisories found</info>');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<PartialSecurityAdvisory>> $advisories
|
||||
* @return array{int, int} Count of affected packages and total count of advisories
|
||||
*/
|
||||
private function countAdvisories(array $advisories): array
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($advisories as $packageAdvisories) {
|
||||
$count += count($packageAdvisories);
|
||||
}
|
||||
return [count($advisories), $count];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOInterface $io
|
||||
* @param array<string, array<SecurityAdvisory>> $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.');
|
||||
return;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid format "'.$format.'".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ConsoleIO $io
|
||||
* @param array<string, array<SecurityAdvisory>> $advisories
|
||||
* @return void
|
||||
*/
|
||||
private function outputAvisoriesTable(ConsoleIO $io, array $advisories): void
|
||||
{
|
||||
foreach ($advisories as $packageAdvisories) {
|
||||
foreach ($packageAdvisories as $advisory) {
|
||||
$io->getTable()
|
||||
->setHorizontal()
|
||||
->setHeaders([
|
||||
'Package',
|
||||
'CVE',
|
||||
'Title',
|
||||
'URL',
|
||||
'Affected versions',
|
||||
'Reported at',
|
||||
])
|
||||
->addRow([
|
||||
$advisory->packageName,
|
||||
$this->getCVE($advisory),
|
||||
$advisory->title,
|
||||
$this->getURL($advisory),
|
||||
$advisory->affectedVersions->getPrettyString(),
|
||||
$advisory->reportedAt->format(DATE_ATOM),
|
||||
])
|
||||
->setColumnWidth(1, 80)
|
||||
->setColumnMaxWidth(1, 80)
|
||||
->render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IOInterface $io
|
||||
* @param array<string, array<SecurityAdvisory>> $advisories
|
||||
* @return void
|
||||
*/
|
||||
private function outputAdvisoriesPlain(IOInterface $io, array $advisories): void
|
||||
{
|
||||
$error = [];
|
||||
$firstAdvisory = true;
|
||||
foreach ($advisories as $packageAdvisories) {
|
||||
foreach ($packageAdvisories as $advisory) {
|
||||
if (!$firstAdvisory) {
|
||||
$error[] = '--------';
|
||||
}
|
||||
$error[] = "Package: ".$advisory->packageName;
|
||||
$error[] = "CVE: ".$this->getCVE($advisory);
|
||||
$error[] = "Title: ".OutputFormatter::escape($advisory->title);
|
||||
$error[] = "URL: ".$this->getURL($advisory);
|
||||
$error[] = "Affected versions: ".OutputFormatter::escape($advisory->affectedVersions->getPrettyString());
|
||||
$error[] = "Reported at: ".$advisory->reportedAt->format(DATE_ATOM);
|
||||
$firstAdvisory = false;
|
||||
}
|
||||
}
|
||||
$io->writeError($error);
|
||||
}
|
||||
|
||||
private function getCVE(SecurityAdvisory $advisory): string
|
||||
{
|
||||
if ($advisory->cve === null) {
|
||||
return 'NO CVE';
|
||||
}
|
||||
|
||||
return '<href=https://cve.mitre.org/cgi-bin/cvename.cgi?name='.$advisory->cve.'>'.$advisory->cve.'</>';
|
||||
}
|
||||
|
||||
private function getURL(SecurityAdvisory $advisory): string
|
||||
{
|
||||
if ($advisory->link === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Advisory;
|
||||
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
class PartialSecurityAdvisory
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $advisoryId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $packageName;
|
||||
|
||||
/**
|
||||
* @var ConstraintInterface
|
||||
* @readonly
|
||||
*/
|
||||
public $affectedVersions;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @return SecurityAdvisory|PartialSecurityAdvisory
|
||||
*/
|
||||
public static function create(string $packageName, array $data, VersionParser $parser): self
|
||||
{
|
||||
$constraint = $parser->parseConstraints($data['affectedVersions']);
|
||||
if (isset($data['title'], $data['sources'], $data['reportedAt'])) {
|
||||
return new SecurityAdvisory($packageName, $data['advisoryId'], $constraint, $data['title'], $data['sources'], new \DateTimeImmutable($data['reportedAt'], new \DateTimeZone('UTC')), $data['cve'] ?? null, $data['link'] ?? null);
|
||||
}
|
||||
|
||||
return new self($packageName, $data['advisoryId'], $constraint);
|
||||
}
|
||||
|
||||
public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions)
|
||||
{
|
||||
$this->advisoryId = $advisoryId;
|
||||
$this->packageName = $packageName;
|
||||
$this->affectedVersions = $affectedVersions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Advisory;
|
||||
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class SecurityAdvisory extends PartialSecurityAdvisory
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @readonly
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* @readonly
|
||||
*/
|
||||
public $cve;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* @readonly
|
||||
*/
|
||||
public $link;
|
||||
|
||||
/**
|
||||
* @var DateTimeImmutable
|
||||
* @readonly
|
||||
*/
|
||||
public $reportedAt;
|
||||
|
||||
/**
|
||||
* @var array<array{name: string, remoteId: string}>
|
||||
* @readonly
|
||||
*/
|
||||
public $sources;
|
||||
|
||||
/**
|
||||
* @param non-empty-array<array{name: string, remoteId: string}> $sources
|
||||
* @readonly
|
||||
*/
|
||||
public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, \DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null)
|
||||
{
|
||||
parent::__construct($packageName, $advisoryId, $affectedVersions);
|
||||
|
||||
$this->title = $title;
|
||||
$this->sources = $sources;
|
||||
$this->reportedAt = $reportedAt;
|
||||
$this->cve = $cve;
|
||||
$this->link = $link;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Command;
|
||||
|
||||
use Composer\Composer;
|
||||
use Composer\Repository\RepositorySet;
|
||||
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 Composer\Advisory\Auditor;
|
||||
use Composer\Console\Input\InputOption;
|
||||
|
||||
class AuditCommand extends BaseCommand
|
||||
|
@ -39,16 +40,19 @@ EOT
|
|||
{
|
||||
$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);
|
||||
$auditor = new Auditor();
|
||||
$repoSet = new RepositorySet();
|
||||
foreach ($composer->getRepositoryManager()->getRepositories() as $repo) {
|
||||
$repoSet->addRepository($repo);
|
||||
}
|
||||
|
||||
return min(255, $auditor->audit($this->getIO(), $packages, $this->getAuditFormat($input, 'format'), false));
|
||||
return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ use Composer\IO\NullIO;
|
|||
use Composer\Plugin\PreCommandRunEvent;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Plugin\PluginEvents;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\Platform;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
|
|
|
@ -44,7 +44,7 @@ use Composer\Util\Filesystem;
|
|||
use Composer\Util\Platform;
|
||||
use Composer\Util\ProcessExecutor;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
|
||||
/**
|
||||
* Install a package as new project into new directory.
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Composer\Command;
|
|||
use Composer\Installer;
|
||||
use Composer\Plugin\CommandEvent;
|
||||
use Composer\Plugin\PluginEvents;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\HttpDownloader;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Composer\Console\Input\InputOption;
|
||||
|
@ -52,7 +52,7 @@ class InstallCommand extends BaseCommand
|
|||
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-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', null, InputOption::VALUE_NONE, 'Run an audit 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('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'),
|
||||
|
@ -133,7 +133,7 @@ EOT
|
|||
->setClassMapAuthoritative($authoritative)
|
||||
->setApcuAutoloader($apcu, $apcuPrefix)
|
||||
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
||||
->setAudit(!$input->getOption('no-audit'))
|
||||
->setAudit($input->getOption('audit'))
|
||||
->setAuditFormat($this->getAuditFormat($input))
|
||||
;
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ use Composer\Console\Input\InputOption;
|
|||
use Composer\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
|
||||
/**
|
||||
* @author Pierre du Plessis <pdples@gmail.com>
|
||||
|
|
|
@ -31,7 +31,7 @@ use Composer\Plugin\PluginEvents;
|
|||
use Composer\Repository\CompositeRepository;
|
||||
use Composer\Repository\PlatformRepository;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\Silencer;
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,7 @@ use Composer\Semver\Constraint\ConstraintInterface;
|
|||
use Composer\Util\HttpDownloader;
|
||||
use Composer\Semver\Constraint\MultiConstraint;
|
||||
use Composer\Package\Link;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Composer\Console\Input\InputOption;
|
||||
|
|
|
@ -62,7 +62,7 @@ use Composer\Repository\RepositoryManager;
|
|||
use Composer\Repository\LockArrayRepository;
|
||||
use Composer\Script\ScriptEvents;
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use Composer\Util\Auditor;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\Platform;
|
||||
|
||||
/**
|
||||
|
@ -397,8 +397,12 @@ class Installer
|
|||
}
|
||||
if (count($packages) > 0) {
|
||||
try {
|
||||
$auditor = new Auditor(Factory::createHttpDownloader($this->io, $this->config));
|
||||
$auditor->audit($this->io, $packages, $this->auditFormat);
|
||||
$auditor = new Auditor();
|
||||
$repoSet = new RepositorySet();
|
||||
foreach ($this->repositoryManager->getRepositories() as $repo) {
|
||||
$repoSet->addRepository($repo);
|
||||
}
|
||||
$auditor->audit($this->io, $repoSet, $packages, $this->auditFormat);
|
||||
} catch (TransportException $e) {
|
||||
$this->io->error('Failed to audit '.$target.' packages.');
|
||||
if ($this->io->isVerbose()) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Repository;
|
||||
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use Composer\Advisory\PartialSecurityAdvisory;
|
||||
use Composer\Advisory\SecurityAdvisory;
|
||||
|
||||
/**
|
||||
* Repositories that allow fetching security advisory data
|
||||
*
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @internal
|
||||
*/
|
||||
interface AdvisoryProviderInterface
|
||||
{
|
||||
public function hasSecurityAdvisories(): bool;
|
||||
|
||||
/**
|
||||
* @param array<string, ConstraintInterface> $packageConstraintMap Map of package name to constraint (can be MatchAllConstraint to fetch all advisories)
|
||||
* @return ($allowPartialAdvisories is true ? array{namesFound: string[], advisories: array<string, array<PartialSecurityAdvisory|SecurityAdvisory>>} : array{namesFound: string[], advisories: array<string, array<SecurityAdvisory>>})
|
||||
*/
|
||||
public function getSecurityAdvisories(array $packageConstraintMap, bool $allowPartialAdvisories = false): array;
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
namespace Composer\Repository;
|
||||
|
||||
use Composer\Advisory\PartialSecurityAdvisory;
|
||||
use Composer\Advisory\SecurityAdvisory;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\Loader\ArrayLoader;
|
||||
use Composer\Package\PackageInterface;
|
||||
|
@ -40,11 +42,12 @@ use Composer\Util\Http\Response;
|
|||
use Composer\MetadataMinifier\MetadataMinifier;
|
||||
use Composer\Util\Url;
|
||||
use React\Promise\PromiseInterface;
|
||||
use function React\Promise\resolve;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface
|
||||
class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface, AdvisoryProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
|
@ -107,6 +110,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
private $partialPackagesByName = null;
|
||||
/** @var bool */
|
||||
private $displayedWarningAboutNonMatchingPackageIndex = false;
|
||||
/** @var array{metadata: bool, query-all: bool, api-url: string|null}|null */
|
||||
private $securityAdvisoryConfig = null;
|
||||
|
||||
/**
|
||||
* @var array list of package names which are fresh and can be loaded from the cache directly in case loadPackage is called several times
|
||||
|
@ -472,7 +477,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
// this call initializes loadRootServerFile which is needed for the rest below to work
|
||||
$hasProviders = $this->hasProviders();
|
||||
|
||||
if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) {
|
||||
if (!$hasProviders && !$this->hasPartialPackages() && null === $this->lazyProvidersUrl) {
|
||||
return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded);
|
||||
}
|
||||
|
||||
|
@ -604,6 +609,102 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
return parent::search($query, $mode);
|
||||
}
|
||||
|
||||
public function hasSecurityAdvisories(): bool
|
||||
{
|
||||
$this->loadRootServerFile(600);
|
||||
return $this->securityAdvisoryConfig !== null && ($this->securityAdvisoryConfig['metadata'] || $this->securityAdvisoryConfig['api-url'] !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getSecurityAdvisories(array $packageConstraintMap, bool $allowPartialAdvisories = false): array
|
||||
{
|
||||
$this->loadRootServerFile(600);
|
||||
if (null === $this->securityAdvisoryConfig) {
|
||||
return ['namesFound' => [], 'advisories' => []];
|
||||
}
|
||||
|
||||
$advisories = [];
|
||||
$namesFound = [];
|
||||
|
||||
$apiUrl = $this->securityAdvisoryConfig['api-url'];
|
||||
|
||||
$parser = new VersionParser();
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param string $name
|
||||
* @return ($allowPartialAdvisories is false ? SecurityAdvisory|null : PartialSecurityAdvisory|SecurityAdvisory|null)
|
||||
*/
|
||||
$create = function (array $data, string $name) use ($parser, $allowPartialAdvisories, &$packageConstraintMap): ?PartialSecurityAdvisory {
|
||||
$advisory = PartialSecurityAdvisory::create($name, $data, $parser);
|
||||
if (!$allowPartialAdvisories && !$advisory instanceof SecurityAdvisory) {
|
||||
throw new \RuntimeException('Advisory for '.$name.' could not be loaded as a full advisory from '.$this->getRepoName() . PHP_EOL . var_export($data, true));
|
||||
}
|
||||
if (!$advisory->affectedVersions->matches($packageConstraintMap[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $advisory;
|
||||
};
|
||||
|
||||
if ($this->securityAdvisoryConfig['metadata'] && ($allowPartialAdvisories || $apiUrl === null)) {
|
||||
$promises = [];
|
||||
foreach ($packageConstraintMap as $name => $constraint) {
|
||||
$name = strtolower($name);
|
||||
|
||||
// skip platform packages, root package and composer-plugin-api
|
||||
if (PlatformRepository::isPlatformPackage($name) || '__root__' === $name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$promises[] = $this->startCachedAsyncDownload($name, $name)
|
||||
->then(function (array $spec) use (&$advisories, &$namesFound, &$packageConstraintMap, $name, $create): void {
|
||||
list($response, ) = $spec;
|
||||
|
||||
if (!isset($response['security-advisories']) || !is_array($response['security-advisories'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$namesFound[$name] = true;
|
||||
if (count($response['security-advisories']) > 0) {
|
||||
$advisories[$name] = array_filter(array_map(
|
||||
function ($data) use ($name, $create) { return $create($data, $name); },
|
||||
$response['security-advisories']
|
||||
));
|
||||
}
|
||||
unset($packageConstraintMap[$name]);
|
||||
});
|
||||
}
|
||||
|
||||
$this->loop->wait($promises);
|
||||
}
|
||||
|
||||
if ($apiUrl !== null && count($packageConstraintMap) > 0) {
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => ['Content-type: application/x-www-form-urlencoded'],
|
||||
'timeout' => 10,
|
||||
'content' => http_build_query(['packages' => array_keys($packageConstraintMap)]),
|
||||
],
|
||||
];
|
||||
$response = $this->httpDownloader->get($apiUrl, $options);
|
||||
/** @var string $name */
|
||||
foreach ($response->decodeJson()['advisories'] as $name => $list) {
|
||||
if (count($list) > 0) {
|
||||
$advisories[$name] = array_filter(array_map(
|
||||
function ($data) use ($name, $create) { return $create($data, $name); },
|
||||
$list
|
||||
));
|
||||
}
|
||||
$namesFound[$name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ['namesFound' => array_keys($namesFound), 'advisories' => array_filter($advisories)];
|
||||
}
|
||||
|
||||
public function getProviders(string $packageName)
|
||||
{
|
||||
$this->loadRootServerFile();
|
||||
|
@ -863,7 +964,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ConstraintInterface|null> $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded
|
||||
* @param array<string, ConstraintInterface|null> $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only
|
||||
* packages matching it will be loaded
|
||||
* @param array<string, int>|null $acceptableStabilities
|
||||
* @phpstan-param array<string, BasePackage::STABILITY_*>|null $acceptableStabilities
|
||||
* @param array<string, int>|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value
|
||||
|
@ -880,7 +982,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
$namesFound = array();
|
||||
$promises = array();
|
||||
|
||||
if (!$this->lazyProvidersUrl) {
|
||||
if (null === $this->lazyProvidersUrl) {
|
||||
throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url');
|
||||
}
|
||||
|
||||
|
@ -904,25 +1006,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
continue;
|
||||
}
|
||||
|
||||
$url = str_replace('%package%', $name, $this->lazyProvidersUrl);
|
||||
$cacheKey = 'provider-'.strtr($name, '/', '~').'.json';
|
||||
|
||||
$lastModified = null;
|
||||
if ($contents = $this->cache->read($cacheKey)) {
|
||||
$contents = json_decode($contents, true);
|
||||
$lastModified = $contents['last-modified'] ?? null;
|
||||
}
|
||||
|
||||
$promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified)
|
||||
->then(function ($response) use (&$packages, &$namesFound, $url, $cacheKey, $contents, $realName, $constraint, $acceptableStabilities, $stabilityFlags, $alreadyLoaded): void {
|
||||
$packagesSource = 'downloaded file ('.Url::sanitize($url).')';
|
||||
|
||||
if (true === $response) {
|
||||
$packagesSource = 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')';
|
||||
$response = $contents;
|
||||
}
|
||||
|
||||
if (!isset($response['packages'][$realName])) {
|
||||
$promises[] = $this->startCachedAsyncDownload($name, $realName)
|
||||
->then(function (array $spec) use (&$packages, &$namesFound, $realName, $constraint, $acceptableStabilities, $stabilityFlags, $alreadyLoaded): void {
|
||||
list($response, $packagesSource) = $spec;
|
||||
if (null === $response) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -968,7 +1055,41 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
$this->loop->wait($promises);
|
||||
|
||||
return array('namesFound' => $namesFound, 'packages' => $packages);
|
||||
// RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed
|
||||
}
|
||||
|
||||
private function startCachedAsyncDownload(string $fileName, string $packageName = null): PromiseInterface
|
||||
{
|
||||
if (null === $this->lazyProvidersUrl) {
|
||||
throw new \LogicException('startCachedAsyncDownload only supports v2 protocol composer repos with a metadata-url');
|
||||
}
|
||||
|
||||
$name = strtolower($fileName);
|
||||
$packageName = $packageName ?? $name;
|
||||
|
||||
$url = str_replace('%package%', $name, $this->lazyProvidersUrl);
|
||||
$cacheKey = 'provider-'.strtr($name, '/', '~').'.json';
|
||||
|
||||
$lastModified = null;
|
||||
if ($contents = $this->cache->read($cacheKey)) {
|
||||
$contents = json_decode($contents, true);
|
||||
$lastModified = $contents['last-modified'] ?? null;
|
||||
}
|
||||
|
||||
return $this->asyncFetchFile($url, $cacheKey, $lastModified)
|
||||
->then(function ($response) use ($url, $cacheKey, $contents, $packageName): array {
|
||||
$packagesSource = 'downloaded file ('.Url::sanitize($url).')';
|
||||
|
||||
if (true === $response) {
|
||||
$packagesSource = 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')';
|
||||
$response = $contents;
|
||||
}
|
||||
|
||||
if (!isset($response['packages'][$packageName])) {
|
||||
return [null, $packagesSource];
|
||||
}
|
||||
|
||||
return [$response, $packagesSource];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1113,6 +1234,14 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
|||
// Remove legacy keys as most repos need to be compatible with Composer v1
|
||||
// as well but we are not interested in the old format anymore at this point
|
||||
unset($data['providers-url'], $data['providers'], $data['providers-includes']);
|
||||
|
||||
if (isset($data['security-advisories']) && is_array($data['security-advisories'])) {
|
||||
$this->securityAdvisoryConfig = [
|
||||
'metadata' => $data['security-advisories']['metadata'] ?? false,
|
||||
'api-url' => $data['security-advisories']['api-url'] ?? null,
|
||||
'query-all' => $data['security-advisories']['query-all'] ?? false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->allowSslDowngrade) {
|
||||
|
|
|
@ -17,14 +17,19 @@ use Composer\DependencyResolver\Pool;
|
|||
use Composer\DependencyResolver\PoolBuilder;
|
||||
use Composer\DependencyResolver\Request;
|
||||
use Composer\EventDispatcher\EventDispatcher;
|
||||
use Composer\Advisory\SecurityAdvisory;
|
||||
use Composer\Advisory\PartialSecurityAdvisory;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\IO\NullIO;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\AliasPackage;
|
||||
use Composer\Package\CompleteAliasPackage;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\PackageInterface;
|
||||
use Composer\Semver\Constraint\Constraint;
|
||||
use Composer\Semver\Constraint\ConstraintInterface;
|
||||
use Composer\Package\Version\StabilityFilter;
|
||||
use Composer\Semver\Constraint\MatchAllConstraint;
|
||||
|
||||
/**
|
||||
* @author Nils Adermann <naderman@naderman.de>
|
||||
|
@ -226,6 +231,57 @@ class RepositorySet
|
|||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $packageNames
|
||||
* @return ($allowPartialAdvisories is true ? array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> : array<string, array<SecurityAdvisory>>)
|
||||
*/
|
||||
public function getSecurityAdvisories(array $packageNames, bool $allowPartialAdvisories = false): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($packageNames as $name) {
|
||||
$map[$name] = new MatchAllConstraint();
|
||||
}
|
||||
|
||||
return $this->getSecurityAdvisoriesForConstraints($map, $allowPartialAdvisories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PackageInterface[] $packages
|
||||
* @return ($allowPartialAdvisories is true ? array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> : array<string, array<SecurityAdvisory>>)
|
||||
*/
|
||||
public function getMatchingSecurityAdvisories(array $packages, bool $allowPartialAdvisories = false): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($packages as $package) {
|
||||
$map[$package->getName()] = new Constraint('=', $package->getVersion());
|
||||
}
|
||||
|
||||
return $this->getSecurityAdvisoriesForConstraints($map, $allowPartialAdvisories);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ConstraintInterface> $packageConstraintMap
|
||||
* @return ($allowPartialAdvisories is true ? array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> : array<string, array<SecurityAdvisory>>)
|
||||
*/
|
||||
private function getSecurityAdvisoriesForConstraints(array $packageConstraintMap, bool $allowPartialAdvisories): array
|
||||
{
|
||||
$advisories = [];
|
||||
foreach ($this->repositories as $repository) {
|
||||
if (!$repository instanceof AdvisoryProviderInterface || !$repository->hasSecurityAdvisories()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $repository->getSecurityAdvisories($packageConstraintMap, $allowPartialAdvisories);
|
||||
foreach ($result['namesFound'] as $nameFound) {
|
||||
unset($packageConstraintMap[$nameFound]);
|
||||
}
|
||||
|
||||
$advisories = array_merge($advisories, $result['advisories']);
|
||||
}
|
||||
|
||||
return $advisories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
*
|
||||
|
|
|
@ -1,250 +0,0 @@
|
|||
<?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 Amount of advisories found
|
||||
* @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';
|
||||
$punctuation = $format === 'summary' ? '.' : ':';
|
||||
$io->writeError("<$errorOrWarn>Found $numAdvisories security vulnerability advisor{$plurality}{$punctuation}</$errorOrWarn>");
|
||||
$this->outputAdvisories($io, $advisories, $format);
|
||||
|
||||
return count($advisories);
|
||||
}
|
||||
|
||||
$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.');
|
||||
return;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid format "'.$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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,289 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Composer\Test\Advisory;
|
||||
|
||||
use Composer\Advisory\PartialSecurityAdvisory;
|
||||
use Composer\Advisory\SecurityAdvisory;
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\IO\NullIO;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Package\Package;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Repository\ComposerRepository;
|
||||
use Composer\Repository\RepositorySet;
|
||||
use Composer\Test\TestCase;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\Http\Response;
|
||||
use Composer\Util\HttpDownloader;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class AuditorTest extends TestCase
|
||||
{
|
||||
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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
$result = $auditor->audit(new NullIO(), $this->getRepoSet(), $data['packages'], Auditor::FORMAT_PLAIN, $data['warningOnly']);
|
||||
$this->assertSame($expected, $result, $message);
|
||||
}
|
||||
|
||||
private function getRepoSet(): RepositorySet
|
||||
{
|
||||
$repo = $this
|
||||
->getMockBuilder(ComposerRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['hasSecurityAdvisories', 'getSecurityAdvisories'])
|
||||
->getMock();
|
||||
|
||||
$repoSet = new RepositorySet();
|
||||
$repoSet->addRepository($repo);
|
||||
|
||||
$repo
|
||||
->method('hasSecurityAdvisories')
|
||||
->willReturn(true);
|
||||
|
||||
$repo
|
||||
->method('getSecurityAdvisories')
|
||||
->willReturnCallback(function (array $packageConstraintMap, bool $allowPartialAdvisories) {
|
||||
$advisories = [];
|
||||
|
||||
$parser = new VersionParser();
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param string $name
|
||||
* @return ($allowPartialAdvisories is false ? SecurityAdvisory|null : PartialSecurityAdvisory|SecurityAdvisory|null)
|
||||
*/
|
||||
$create = function (array $data, string $name) use ($parser, $allowPartialAdvisories, $packageConstraintMap): ?PartialSecurityAdvisory {
|
||||
$advisory = PartialSecurityAdvisory::create($name, $data, $parser);
|
||||
if (!$allowPartialAdvisories && !$advisory instanceof SecurityAdvisory) {
|
||||
throw new \RuntimeException('Advisory for '.$name.' could not be loaded as a full advisory from test repo');
|
||||
}
|
||||
if (!$advisory->affectedVersions->matches($packageConstraintMap[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $advisory;
|
||||
};
|
||||
|
||||
foreach (self::getMockAdvisories() as $package => $list) {
|
||||
if (!isset($packageConstraintMap[$package])) {
|
||||
continue;
|
||||
}
|
||||
$advisories[$package] = array_filter(array_map(
|
||||
function ($data) use ($package, $create) { return $create($data, $package); },
|
||||
$list
|
||||
));
|
||||
}
|
||||
|
||||
return ['namesFound' => array_keys($packageConstraintMap), 'advisories' => array_filter($advisories)];
|
||||
});
|
||||
|
||||
return $repoSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function getMockAdvisories(): array
|
||||
{
|
||||
$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',
|
||||
],
|
||||
[
|
||||
'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',
|
||||
]
|
||||
],
|
||||
'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',
|
||||
],
|
||||
[
|
||||
'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',
|
||||
]
|
||||
],
|
||||
'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',
|
||||
],
|
||||
],
|
||||
'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;
|
||||
}
|
||||
}
|
|
@ -1,395 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Composer\Test\Util;
|
||||
|
||||
use Composer\IO\IOInterface;
|
||||
use Composer\IO\NullIO;
|
||||
use Composer\Json\JsonFile;
|
||||
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
|
||||
{
|
||||
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(new NullIO(), $data['packages'], Auditor::FORMAT_PLAIN, $data['warningOnly']);
|
||||
$this->assertSame($expected, $result, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function advisoriesProvider(): array
|
||||
{
|
||||
$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());
|
||||
$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((string) 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, true)) {
|
||||
unset($advisories[$package]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(['url' => 'https://packagist.org/api/security-advisories/'], 200, [], JsonFile::encode(['advisories' => $advisories]));
|
||||
};
|
||||
|
||||
$httpDownloader
|
||||
->method('get')
|
||||
->willReturnCallback($callback);
|
||||
|
||||
return $httpDownloader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function getMockAdvisories(?int $updatedSince): array
|
||||
{
|
||||
$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',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (0 === $updatedSince || null === $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