Add `composer audit --ignore-severity` option (#12132)
Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>pull/12143/head
parent
5b256070b7
commit
31d83b2c0f
|
@ -1087,7 +1087,8 @@ php composer.phar audit
|
|||
* **--abandoned:** Behavior on abandoned packages. Must be "ignore", "report",
|
||||
or "fail". See also [audit.abandoned](06-config.md#abandoned). Passing this
|
||||
flag will override the config value and the environment variable.
|
||||
|
||||
* **--ignore-severity:** Ignore advisories of a certain severity level. Can be passed one or more
|
||||
time to ignore multiple severities.
|
||||
|
||||
## help
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ use Composer\Package\CompletePackageInterface;
|
|||
use Composer\Package\PackageInterface;
|
||||
use Composer\Repository\RepositorySet;
|
||||
use Composer\Util\PackageInfo;
|
||||
use Composer\Util\Platform;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
|
@ -60,11 +59,12 @@ class Auditor
|
|||
* @param bool $warningOnly If true, outputs a warning. If false, outputs an error.
|
||||
* @param string[] $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities.
|
||||
* @param self::ABANDONED_* $abandoned
|
||||
* @param array<string> $ignoredSeverities List of ignored severity levels
|
||||
*
|
||||
* @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, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL): int
|
||||
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL, array $ignoredSeverities = []): int
|
||||
{
|
||||
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
|
||||
// we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above
|
||||
|
@ -72,7 +72,7 @@ class Auditor
|
|||
if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) {
|
||||
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
|
||||
}
|
||||
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList);
|
||||
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities);
|
||||
|
||||
$abandonedCount = 0;
|
||||
$affectedPackagesCount = 0;
|
||||
|
@ -90,8 +90,9 @@ class Auditor
|
|||
if ($ignoredAdvisories !== []) {
|
||||
$json['ignored-advisories'] = $ignoredAdvisories;
|
||||
}
|
||||
$json['abandoned'] = array_reduce($abandonedPackages, static function(array $carry, CompletePackageInterface $package): array {
|
||||
$json['abandoned'] = array_reduce($abandonedPackages, static function (array $carry, CompletePackageInterface $package): array {
|
||||
$carry[$package->getPrettyName()] = $package->getReplacementPackage();
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
|
@ -146,11 +147,12 @@ class Auditor
|
|||
/**
|
||||
* @phpstan-param array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> $allAdvisories
|
||||
* @param array<string>|array<string,string> $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities.
|
||||
* @param array<string> $ignoredSeverities List of ignored severity levels
|
||||
* @phpstan-return array{advisories: array<string, array<PartialSecurityAdvisory|SecurityAdvisory>>, ignoredAdvisories: array<string, array<PartialSecurityAdvisory|SecurityAdvisory>>}
|
||||
*/
|
||||
private function processAdvisories(array $allAdvisories, array $ignoreList): array
|
||||
private function processAdvisories(array $allAdvisories, array $ignoreList, array $ignoredSeverities): array
|
||||
{
|
||||
if ($ignoreList === []) {
|
||||
if ($ignoreList === [] && $ignoredSeverities === []) {
|
||||
return ['advisories' => $allAdvisories, 'ignoredAdvisories' => []];
|
||||
}
|
||||
|
||||
|
@ -174,6 +176,11 @@ class Auditor
|
|||
}
|
||||
|
||||
if ($advisory instanceof SecurityAdvisory) {
|
||||
if (in_array($advisory->severity, $ignoredSeverities, true)) {
|
||||
$isActive = false;
|
||||
$ignoreReason = "Ignored via --ignore-severity={$advisory->severity}";
|
||||
}
|
||||
|
||||
if (in_array($advisory->cve, $ignoredIds, true)) {
|
||||
$isActive = false;
|
||||
$ignoreReason = $ignoreList[$advisory->cve] ?? null;
|
||||
|
@ -394,5 +401,4 @@ class Auditor
|
|||
|
||||
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ class AuditCommand extends BaseCommand
|
|||
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format. Must be "table", "plain", "json", or "summary".', Auditor::FORMAT_TABLE, Auditor::FORMATS),
|
||||
new InputOption('locked', null, InputOption::VALUE_NONE, 'Audit based on the lock file instead of the installed packages.'),
|
||||
new InputOption('abandoned', null, InputOption::VALUE_REQUIRED, 'Behavior on abandoned packages. Must be "ignore", "report", or "fail".', null, Auditor::ABANDONEDS),
|
||||
new InputOption('ignore-severity', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Ignore advisories of a certain severity level.', [], ['low', 'medium', 'high', 'critical']),
|
||||
])
|
||||
->setHelp(
|
||||
<<<EOT
|
||||
|
@ -73,6 +74,8 @@ EOT
|
|||
|
||||
$abandoned = $abandoned ?? $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL;
|
||||
|
||||
$ignoreSeverities = $input->getOption('ignore-severity') ?? [];
|
||||
|
||||
return min(255, $auditor->audit(
|
||||
$this->getIO(),
|
||||
$repoSet,
|
||||
|
@ -80,8 +83,10 @@ EOT
|
|||
$this->getAuditFormat($input, 'format'),
|
||||
false,
|
||||
$auditConfig['ignore'] ?? [],
|
||||
$abandoned
|
||||
$abandoned,
|
||||
$ignoreSeverities
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,6 @@ namespace Composer\Test\Advisory;
|
|||
use Composer\Advisory\PartialSecurityAdvisory;
|
||||
use Composer\Advisory\SecurityAdvisory;
|
||||
use Composer\IO\BufferIO;
|
||||
use Composer\IO\NullIO;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\Package;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
|
@ -23,7 +22,6 @@ use Composer\Repository\ComposerRepository;
|
|||
use Composer\Repository\RepositorySet;
|
||||
use Composer\Test\TestCase;
|
||||
use Composer\Advisory\Auditor;
|
||||
use Composer\Util\Platform;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class AuditorTest extends TestCase
|
||||
|
@ -162,7 +160,8 @@ Found 2 abandoned packages:
|
|||
self::assertSame($output, trim(str_replace("\r", '', $io->getOutput())));
|
||||
}
|
||||
|
||||
public function ignoredIdsProvider(): \Generator {
|
||||
public function ignoredIdsProvider(): \Generator
|
||||
{
|
||||
yield 'ignore by CVE' => [
|
||||
[
|
||||
new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
|
||||
|
@ -178,7 +177,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'URL: https://advisory.example.com/advisory1'],
|
||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||
]
|
||||
],
|
||||
];
|
||||
yield 'ignore by CVE with reasoning' => [
|
||||
[
|
||||
|
@ -196,7 +195,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||
['text' => 'Ignore reason: A good reason'],
|
||||
]
|
||||
],
|
||||
];
|
||||
yield 'ignore by advisory id' => [
|
||||
[
|
||||
|
@ -213,7 +212,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'URL: https://advisory.example.com/advisory2'],
|
||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||
]
|
||||
],
|
||||
];
|
||||
yield 'ignore by remote id' => [
|
||||
[
|
||||
|
@ -230,7 +229,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'URL: https://advisory.example.com/advisory17'],
|
||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
||||
]
|
||||
],
|
||||
];
|
||||
yield '1 vulnerability, 0 ignored' => [
|
||||
[
|
||||
|
@ -247,7 +246,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'URL: https://advisory.example.com/advisory1'],
|
||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||
]
|
||||
],
|
||||
];
|
||||
yield '1 vulnerability, 3 ignored affecting 2 packages' => [
|
||||
[
|
||||
|
@ -295,7 +294,7 @@ Found 2 abandoned packages:
|
|||
['text' => 'URL: https://advisory.example.com/advisory7'],
|
||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -314,6 +313,55 @@ Found 2 abandoned packages:
|
|||
self::assertSame($exitCode, $result);
|
||||
}
|
||||
|
||||
public function ignoreSeverityProvider(): \Generator
|
||||
{
|
||||
yield 'ignore medium' => [
|
||||
[
|
||||
new Package('vendor1/package1', '2.0.0.0', '2.0.0'),
|
||||
],
|
||||
['medium'],
|
||||
1,
|
||||
[
|
||||
['text' => 'Found 2 ignored security vulnerability advisories affecting 1 package:'],
|
||||
],
|
||||
];
|
||||
yield 'ignore high' => [
|
||||
[
|
||||
new Package('vendor1/package1', '2.0.0.0', '2.0.0'),
|
||||
],
|
||||
['high'],
|
||||
1,
|
||||
[
|
||||
['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
|
||||
],
|
||||
];
|
||||
yield 'ignore high and medium' => [
|
||||
[
|
||||
new Package('vendor1/package1', '2.0.0.0', '2.0.0'),
|
||||
],
|
||||
['high', 'medium'],
|
||||
0,
|
||||
[
|
||||
['text' => 'Found 3 ignored security vulnerability advisories affecting 1 package:'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider ignoreSeverityProvider
|
||||
* @phpstan-param array<\Composer\Package\Package> $packages
|
||||
* @phpstan-param array<string> $ignoredSeverities
|
||||
* @phpstan-param 0|positive-int $exitCode
|
||||
* @phpstan-param list<array{text: string, verbosity?: \Composer\IO\IOInterface::*, regex?: true}|array{ask: string, reply: string}|array{auth: array{string, string, string|null}}> $expectedOutput
|
||||
*/
|
||||
public function testAuditWithIgnoreSeverity($packages, $ignoredSeverities, $exitCode, $expectedOutput): void
|
||||
{
|
||||
$auditor = new Auditor();
|
||||
$result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, [], Auditor::ABANDONED_IGNORE, $ignoredSeverities);
|
||||
$io->expects($expectedOutput, true);
|
||||
self::assertSame($exitCode, $result);
|
||||
}
|
||||
|
||||
private function getRepoSet(): RepositorySet
|
||||
{
|
||||
$repo = $this
|
||||
|
|
Loading…
Reference in New Issue