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-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:** Run an audit after installation is complete.
|
||||||
* **--audit-format:** Audit output format. Must be "table", "plain", or "summary" (default).
|
* **--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
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
parameters:
|
parameters:
|
||||||
ignoreErrors:
|
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
|
count: 1
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
path: ../src/Composer/Advisory/Auditor.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method writeError\\(\\) on Composer\\\\IO\\\\IOInterface\\|null\\.$#"
|
message: "#^Binary operation \"\\.\" between non\\-empty\\-string and array\\|string\\|null results in an error\\.$#"
|
||||||
count: 2
|
count: 1
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
||||||
|
|
||||||
-
|
-
|
||||||
|
@ -20,11 +20,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
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\\.$#"
|
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
|
count: 1
|
||||||
|
@ -55,21 +50,11 @@ parameters:
|
||||||
count: 3
|
count: 3
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
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\\.$#"
|
message: "#^Only booleans are allowed in a ternary operator condition, mixed given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
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\\.$#"
|
message: "#^Only booleans are allowed in an if condition, mixed given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
@ -115,11 +100,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Autoload/AutoloadGenerator.php
|
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\\.$#"
|
message: "#^Parameter \\#2 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:isMatch\\(\\) expects string, string\\|false given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
@ -165,66 +145,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Autoload/ClassLoader.php
|
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\\.$#"
|
message: "#^Casting to bool something that's already bool\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
|
@ -717,7 +637,7 @@ parameters:
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||||
count: 3
|
count: 2
|
||||||
path: ../src/Composer/Command/RequireCommand.php
|
path: ../src/Composer/Command/RequireCommand.php
|
||||||
|
|
||||||
-
|
-
|
||||||
|
@ -735,11 +655,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Command/RequireCommand.php
|
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\\.$#"
|
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
|
@ -1842,7 +1757,7 @@ parameters:
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||||
count: 2
|
count: 1
|
||||||
path: ../src/Composer/DependencyResolver/SolverProblemsException.php
|
path: ../src/Composer/DependencyResolver/SolverProblemsException.php
|
||||||
|
|
||||||
-
|
-
|
||||||
|
@ -3725,11 +3640,6 @@ parameters:
|
||||||
count: 2
|
count: 2
|
||||||
path: ../src/Composer/Repository/ComposerRepository.php
|
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\\.$#"
|
message: "#^Only booleans are allowed in a ternary operator condition, string\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
@ -4518,11 +4428,6 @@ 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
|
||||||
|
@ -5598,21 +5503,6 @@ parameters:
|
||||||
count: 2
|
count: 2
|
||||||
path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
|
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\\.$#"
|
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
|
||||||
count: 1
|
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;
|
namespace Composer\Command;
|
||||||
|
|
||||||
use Composer\Composer;
|
use Composer\Composer;
|
||||||
|
use Composer\Repository\RepositorySet;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Composer\Package\PackageInterface;
|
use Composer\Package\PackageInterface;
|
||||||
use Composer\Repository\InstalledRepository;
|
use Composer\Repository\InstalledRepository;
|
||||||
use Composer\Repository\RepositoryInterface;
|
use Composer\Repository\RepositoryInterface;
|
||||||
use Composer\Util\Auditor;
|
use Composer\Advisory\Auditor;
|
||||||
use Composer\Console\Input\InputOption;
|
use Composer\Console\Input\InputOption;
|
||||||
|
|
||||||
class AuditCommand extends BaseCommand
|
class AuditCommand extends BaseCommand
|
||||||
|
@ -39,16 +40,19 @@ EOT
|
||||||
{
|
{
|
||||||
$composer = $this->requireComposer();
|
$composer = $this->requireComposer();
|
||||||
$packages = $this->getPackages($composer, $input);
|
$packages = $this->getPackages($composer, $input);
|
||||||
$httpDownloader = $composer->getLoop()->getHttpDownloader();
|
|
||||||
|
|
||||||
if (count($packages) === 0) {
|
if (count($packages) === 0) {
|
||||||
$this->getIO()->writeError('No packages - skipping audit.');
|
$this->getIO()->writeError('No packages - skipping audit.');
|
||||||
return 0;
|
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\Plugin\PreCommandRunEvent;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
use Composer\Plugin\PluginEvents;
|
use Composer\Plugin\PluginEvents;
|
||||||
use Composer\Util\Auditor;
|
use Composer\Advisory\Auditor;
|
||||||
use Composer\Util\Platform;
|
use Composer\Util\Platform;
|
||||||
use Symfony\Component\Console\Completion\CompletionInput;
|
use Symfony\Component\Console\Completion\CompletionInput;
|
||||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||||
|
|
|
@ -44,7 +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;
|
use Composer\Advisory\Auditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a package as new project into new directory.
|
* Install a package as new project into new directory.
|
||||||
|
|
|
@ -15,7 +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\Advisory\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;
|
||||||
|
@ -52,7 +52,7 @@ 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', 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('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'),
|
||||||
|
@ -133,7 +133,7 @@ EOT
|
||||||
->setClassMapAuthoritative($authoritative)
|
->setClassMapAuthoritative($authoritative)
|
||||||
->setApcuAutoloader($apcu, $apcuPrefix)
|
->setApcuAutoloader($apcu, $apcuPrefix)
|
||||||
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
->setPlatformRequirementFilter($this->getPlatformRequirementFilter($input))
|
||||||
->setAudit(!$input->getOption('no-audit'))
|
->setAudit($input->getOption('audit'))
|
||||||
->setAuditFormat($this->getAuditFormat($input))
|
->setAuditFormat($this->getAuditFormat($input))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +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;
|
use Composer\Advisory\Auditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pierre du Plessis <pdples@gmail.com>
|
* @author Pierre du Plessis <pdples@gmail.com>
|
||||||
|
|
|
@ -31,7 +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\Advisory\Auditor;
|
||||||
use Composer\Util\Silencer;
|
use Composer\Util\Silencer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,7 +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 Composer\Advisory\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;
|
||||||
|
|
|
@ -62,7 +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\Advisory\Auditor;
|
||||||
use Composer\Util\Platform;
|
use Composer\Util\Platform;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -397,8 +397,12 @@ class Installer
|
||||||
}
|
}
|
||||||
if (count($packages) > 0) {
|
if (count($packages) > 0) {
|
||||||
try {
|
try {
|
||||||
$auditor = new Auditor(Factory::createHttpDownloader($this->io, $this->config));
|
$auditor = new Auditor();
|
||||||
$auditor->audit($this->io, $packages, $this->auditFormat);
|
$repoSet = new RepositorySet();
|
||||||
|
foreach ($this->repositoryManager->getRepositories() as $repo) {
|
||||||
|
$repoSet->addRepository($repo);
|
||||||
|
}
|
||||||
|
$auditor->audit($this->io, $repoSet, $packages, $this->auditFormat);
|
||||||
} catch (TransportException $e) {
|
} catch (TransportException $e) {
|
||||||
$this->io->error('Failed to audit '.$target.' packages.');
|
$this->io->error('Failed to audit '.$target.' packages.');
|
||||||
if ($this->io->isVerbose()) {
|
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;
|
namespace Composer\Repository;
|
||||||
|
|
||||||
|
use Composer\Advisory\PartialSecurityAdvisory;
|
||||||
|
use Composer\Advisory\SecurityAdvisory;
|
||||||
use Composer\Package\BasePackage;
|
use Composer\Package\BasePackage;
|
||||||
use Composer\Package\Loader\ArrayLoader;
|
use Composer\Package\Loader\ArrayLoader;
|
||||||
use Composer\Package\PackageInterface;
|
use Composer\Package\PackageInterface;
|
||||||
|
@ -40,11 +42,12 @@ use Composer\Util\Http\Response;
|
||||||
use Composer\MetadataMinifier\MetadataMinifier;
|
use Composer\MetadataMinifier\MetadataMinifier;
|
||||||
use Composer\Util\Url;
|
use Composer\Util\Url;
|
||||||
use React\Promise\PromiseInterface;
|
use React\Promise\PromiseInterface;
|
||||||
|
use function React\Promise\resolve;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
*/
|
*/
|
||||||
class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface
|
class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface, AdvisoryProviderInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var mixed[]
|
* @var mixed[]
|
||||||
|
@ -107,6 +110,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
||||||
private $partialPackagesByName = null;
|
private $partialPackagesByName = null;
|
||||||
/** @var bool */
|
/** @var bool */
|
||||||
private $displayedWarningAboutNonMatchingPackageIndex = false;
|
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
|
* @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
|
// this call initializes loadRootServerFile which is needed for the rest below to work
|
||||||
$hasProviders = $this->hasProviders();
|
$hasProviders = $this->hasProviders();
|
||||||
|
|
||||||
if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) {
|
if (!$hasProviders && !$this->hasPartialPackages() && null === $this->lazyProvidersUrl) {
|
||||||
return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded);
|
return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,6 +609,102 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
||||||
return parent::search($query, $mode);
|
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)
|
public function getProviders(string $packageName)
|
||||||
{
|
{
|
||||||
$this->loadRootServerFile();
|
$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
|
* @param array<string, int>|null $acceptableStabilities
|
||||||
* @phpstan-param array<string, BasePackage::STABILITY_*>|null $acceptableStabilities
|
* @phpstan-param array<string, BasePackage::STABILITY_*>|null $acceptableStabilities
|
||||||
* @param array<string, int>|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value
|
* @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();
|
$namesFound = array();
|
||||||
$promises = array();
|
$promises = array();
|
||||||
|
|
||||||
if (!$this->lazyProvidersUrl) {
|
if (null === $this->lazyProvidersUrl) {
|
||||||
throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url');
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = str_replace('%package%', $name, $this->lazyProvidersUrl);
|
$promises[] = $this->startCachedAsyncDownload($name, $realName)
|
||||||
$cacheKey = 'provider-'.strtr($name, '/', '~').'.json';
|
->then(function (array $spec) use (&$packages, &$namesFound, $realName, $constraint, $acceptableStabilities, $stabilityFlags, $alreadyLoaded): void {
|
||||||
|
list($response, $packagesSource) = $spec;
|
||||||
$lastModified = null;
|
if (null === $response) {
|
||||||
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])) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -968,7 +1055,41 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
|
||||||
$this->loop->wait($promises);
|
$this->loop->wait($promises);
|
||||||
|
|
||||||
return array('namesFound' => $namesFound, 'packages' => $packages);
|
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
|
// 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
|
// as well but we are not interested in the old format anymore at this point
|
||||||
unset($data['providers-url'], $data['providers'], $data['providers-includes']);
|
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) {
|
if ($this->allowSslDowngrade) {
|
||||||
|
|
|
@ -17,14 +17,19 @@ use Composer\DependencyResolver\Pool;
|
||||||
use Composer\DependencyResolver\PoolBuilder;
|
use Composer\DependencyResolver\PoolBuilder;
|
||||||
use Composer\DependencyResolver\Request;
|
use Composer\DependencyResolver\Request;
|
||||||
use Composer\EventDispatcher\EventDispatcher;
|
use Composer\EventDispatcher\EventDispatcher;
|
||||||
|
use Composer\Advisory\SecurityAdvisory;
|
||||||
|
use Composer\Advisory\PartialSecurityAdvisory;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
use Composer\IO\NullIO;
|
use Composer\IO\NullIO;
|
||||||
use Composer\Package\BasePackage;
|
use Composer\Package\BasePackage;
|
||||||
use Composer\Package\AliasPackage;
|
use Composer\Package\AliasPackage;
|
||||||
use Composer\Package\CompleteAliasPackage;
|
use Composer\Package\CompleteAliasPackage;
|
||||||
use Composer\Package\CompletePackage;
|
use Composer\Package\CompletePackage;
|
||||||
|
use Composer\Package\PackageInterface;
|
||||||
|
use Composer\Semver\Constraint\Constraint;
|
||||||
use Composer\Semver\Constraint\ConstraintInterface;
|
use Composer\Semver\Constraint\ConstraintInterface;
|
||||||
use Composer\Package\Version\StabilityFilter;
|
use Composer\Package\Version\StabilityFilter;
|
||||||
|
use Composer\Semver\Constraint\MatchAllConstraint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Nils Adermann <naderman@naderman.de>
|
* @author Nils Adermann <naderman@naderman.de>
|
||||||
|
@ -226,6 +231,57 @@ class RepositorySet
|
||||||
return $result;
|
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
|
* @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