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",
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

View File

@ -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;
@ -92,6 +92,7 @@ class Auditor
}
$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).'</>';
}
}

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

View File

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