1
0
Fork 0

Add `composer audit --ignore-severity` option (#12132)

Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
pull/12143/head
Johnson Page 2024-10-02 22:14:53 +10:00 committed by GitHub
parent 5b256070b7
commit 31d83b2c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 78 additions and 18 deletions

View File

@ -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

View File

@ -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).'</>';
} }
} }

View File

@ -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
)); ));
} }
/** /**

View File

@ -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