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",
|
* **--abandoned:** Behavior on abandoned packages. Must be "ignore", "report",
|
||||||
or "fail". See also [audit.abandoned](06-config.md#abandoned). Passing this
|
or "fail". See also [audit.abandoned](06-config.md#abandoned). Passing this
|
||||||
flag will override the config value and the environment variable.
|
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
|
## help
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ use Composer\Package\CompletePackageInterface;
|
||||||
use Composer\Package\PackageInterface;
|
use Composer\Package\PackageInterface;
|
||||||
use Composer\Repository\RepositorySet;
|
use Composer\Repository\RepositorySet;
|
||||||
use Composer\Util\PackageInfo;
|
use Composer\Util\PackageInfo;
|
||||||
use Composer\Util\Platform;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
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 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 string[] $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities.
|
||||||
* @param self::ABANDONED_* $abandoned
|
* @param self::ABANDONED_* $abandoned
|
||||||
|
* @param array<string> $ignoredSeverities List of ignored severity levels
|
||||||
*
|
*
|
||||||
* @return int Amount of packages with vulnerabilities found
|
* @return int Amount of packages with vulnerabilities found
|
||||||
* @throws InvalidArgumentException If no packages are passed in
|
* @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);
|
$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
|
// 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) {
|
if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) {
|
||||||
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
|
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
|
||||||
}
|
}
|
||||||
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList);
|
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities);
|
||||||
|
|
||||||
$abandonedCount = 0;
|
$abandonedCount = 0;
|
||||||
$affectedPackagesCount = 0;
|
$affectedPackagesCount = 0;
|
||||||
|
@ -90,8 +90,9 @@ class Auditor
|
||||||
if ($ignoredAdvisories !== []) {
|
if ($ignoredAdvisories !== []) {
|
||||||
$json['ignored-advisories'] = $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();
|
$carry[$package->getPrettyName()] = $package->getReplacementPackage();
|
||||||
|
|
||||||
return $carry;
|
return $carry;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -146,11 +147,12 @@ class Auditor
|
||||||
/**
|
/**
|
||||||
* @phpstan-param array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> $allAdvisories
|
* @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>|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>>}
|
* @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' => []];
|
return ['advisories' => $allAdvisories, 'ignoredAdvisories' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +176,11 @@ class Auditor
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($advisory instanceof SecurityAdvisory) {
|
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)) {
|
if (in_array($advisory->cve, $ignoredIds, true)) {
|
||||||
$isActive = false;
|
$isActive = false;
|
||||||
$ignoreReason = $ignoreList[$advisory->cve] ?? null;
|
$ignoreReason = $ignoreList[$advisory->cve] ?? null;
|
||||||
|
@ -394,5 +401,4 @@ class Auditor
|
||||||
|
|
||||||
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
|
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('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('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('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(
|
->setHelp(
|
||||||
<<<EOT
|
<<<EOT
|
||||||
|
@ -73,6 +74,8 @@ EOT
|
||||||
|
|
||||||
$abandoned = $abandoned ?? $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL;
|
$abandoned = $abandoned ?? $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL;
|
||||||
|
|
||||||
|
$ignoreSeverities = $input->getOption('ignore-severity') ?? [];
|
||||||
|
|
||||||
return min(255, $auditor->audit(
|
return min(255, $auditor->audit(
|
||||||
$this->getIO(),
|
$this->getIO(),
|
||||||
$repoSet,
|
$repoSet,
|
||||||
|
@ -80,8 +83,10 @@ EOT
|
||||||
$this->getAuditFormat($input, 'format'),
|
$this->getAuditFormat($input, 'format'),
|
||||||
false,
|
false,
|
||||||
$auditConfig['ignore'] ?? [],
|
$auditConfig['ignore'] ?? [],
|
||||||
$abandoned
|
$abandoned,
|
||||||
|
$ignoreSeverities
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,7 +15,6 @@ namespace Composer\Test\Advisory;
|
||||||
use Composer\Advisory\PartialSecurityAdvisory;
|
use Composer\Advisory\PartialSecurityAdvisory;
|
||||||
use Composer\Advisory\SecurityAdvisory;
|
use Composer\Advisory\SecurityAdvisory;
|
||||||
use Composer\IO\BufferIO;
|
use Composer\IO\BufferIO;
|
||||||
use Composer\IO\NullIO;
|
|
||||||
use Composer\Package\CompletePackage;
|
use Composer\Package\CompletePackage;
|
||||||
use Composer\Package\Package;
|
use Composer\Package\Package;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
|
@ -23,7 +22,6 @@ use Composer\Repository\ComposerRepository;
|
||||||
use Composer\Repository\RepositorySet;
|
use Composer\Repository\RepositorySet;
|
||||||
use Composer\Test\TestCase;
|
use Composer\Test\TestCase;
|
||||||
use Composer\Advisory\Auditor;
|
use Composer\Advisory\Auditor;
|
||||||
use Composer\Util\Platform;
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
class AuditorTest extends TestCase
|
class AuditorTest extends TestCase
|
||||||
|
@ -162,7 +160,8 @@ Found 2 abandoned packages:
|
||||||
self::assertSame($output, trim(str_replace("\r", '', $io->getOutput())));
|
self::assertSame($output, trim(str_replace("\r", '', $io->getOutput())));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ignoredIdsProvider(): \Generator {
|
public function ignoredIdsProvider(): \Generator
|
||||||
|
{
|
||||||
yield 'ignore by CVE' => [
|
yield 'ignore by CVE' => [
|
||||||
[
|
[
|
||||||
new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
|
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' => 'URL: https://advisory.example.com/advisory1'],
|
||||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
yield 'ignore by CVE with reasoning' => [
|
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' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||||
['text' => 'Ignore reason: A good reason'],
|
['text' => 'Ignore reason: A good reason'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
yield 'ignore by advisory id' => [
|
yield 'ignore by advisory id' => [
|
||||||
[
|
[
|
||||||
|
@ -213,7 +212,7 @@ Found 2 abandoned packages:
|
||||||
['text' => 'URL: https://advisory.example.com/advisory2'],
|
['text' => 'URL: https://advisory.example.com/advisory2'],
|
||||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
yield 'ignore by remote id' => [
|
yield 'ignore by remote id' => [
|
||||||
[
|
[
|
||||||
|
@ -230,7 +229,7 @@ Found 2 abandoned packages:
|
||||||
['text' => 'URL: https://advisory.example.com/advisory17'],
|
['text' => 'URL: https://advisory.example.com/advisory17'],
|
||||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
yield '1 vulnerability, 0 ignored' => [
|
yield '1 vulnerability, 0 ignored' => [
|
||||||
[
|
[
|
||||||
|
@ -247,7 +246,7 @@ Found 2 abandoned packages:
|
||||||
['text' => 'URL: https://advisory.example.com/advisory1'],
|
['text' => 'URL: https://advisory.example.com/advisory1'],
|
||||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
yield '1 vulnerability, 3 ignored affecting 2 packages' => [
|
yield '1 vulnerability, 3 ignored affecting 2 packages' => [
|
||||||
[
|
[
|
||||||
|
@ -295,7 +294,7 @@ Found 2 abandoned packages:
|
||||||
['text' => 'URL: https://advisory.example.com/advisory7'],
|
['text' => 'URL: https://advisory.example.com/advisory7'],
|
||||||
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
|
||||||
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,6 +313,55 @@ Found 2 abandoned packages:
|
||||||
self::assertSame($exitCode, $result);
|
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
|
private function getRepoSet(): RepositorySet
|
||||||
{
|
{
|
||||||
$repo = $this
|
$repo = $this
|
||||||
|
|
Loading…
Reference in New Issue