1
0
Fork 0

Use a bitmask to produce deterministic exit codes for the "audit" command (#12203)

* Use a bitmask to produce deterministic exit codes for the "audit" command

* Rename consts, small cleanups

---------

Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>
pull/12211/head
Javier Spagnoletti 2024-11-25 10:30:31 -03:00 committed by GitHub
parent 38cb4bfe71
commit e468b73cb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 81 additions and 17 deletions

View File

@ -53,6 +53,11 @@ class Auditor
self::ABANDONED_FAIL, self::ABANDONED_FAIL,
]; ];
/** Values to determine the audit result. */
public const STATUS_OK = 0;
public const STATUS_VULNERABLE = 1;
public const STATUS_ABANDONED = 2;
/** /**
* @param PackageInterface[] $packages * @param PackageInterface[] $packages
* @param self::FORMAT_* $format The format that will be used to output audit results. * @param self::FORMAT_* $format The format that will be used to output audit results.
@ -61,7 +66,7 @@ class Auditor
* @param self::ABANDONED_* $abandoned * @param self::ABANDONED_* $abandoned
* @param array<string> $ignoredSeverities List of ignored severity levels * @param array<string> $ignoredSeverities List of ignored severity levels
* *
* @return int Amount of packages with vulnerabilities found * @return int-mask<self::STATUS_*> A bitmask of STATUS_* constants or 0 on success
* @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, array $ignoredSeverities = []): 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
@ -75,7 +80,7 @@ class Auditor
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities); ['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities);
$abandonedCount = 0; $abandonedCount = 0;
$affectedPackagesCount = 0; $affectedPackagesCount = count($advisories);
if ($abandoned === self::ABANDONED_IGNORE) { if ($abandoned === self::ABANDONED_IGNORE) {
$abandonedPackages = []; $abandonedPackages = [];
} else { } else {
@ -85,6 +90,8 @@ class Auditor
} }
} }
$auditBitmask = $this->calculateBitmask(0 < $affectedPackagesCount, 0 < $abandonedCount);
if (self::FORMAT_JSON === $format) { if (self::FORMAT_JSON === $format) {
$json = ['advisories' => $advisories]; $json = ['advisories' => $advisories];
if ($ignoredAdvisories !== []) { if ($ignoredAdvisories !== []) {
@ -98,23 +105,22 @@ class Auditor
$io->write(JsonFile::encode($json)); $io->write(JsonFile::encode($json));
return count($advisories) + $abandonedCount; return $auditBitmask;
} }
$errorOrWarn = $warningOnly ? 'warning' : 'error'; $errorOrWarn = $warningOnly ? 'warning' : 'error';
if (count($advisories) > 0 || count($ignoredAdvisories) > 0) { if ($affectedPackagesCount > 0 || count($ignoredAdvisories) > 0) {
$passes = [ $passes = [
[$ignoredAdvisories, "<info>Found %d ignored security vulnerability advisor%s affecting %d package%s%s</info>"], [$ignoredAdvisories, "<info>Found %d ignored security vulnerability advisor%s affecting %d package%s%s</info>"],
// this has to run last to allow $affectedPackagesCount in the return statement to be correct
[$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s</$errorOrWarn>"], [$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s</$errorOrWarn>"],
]; ];
foreach ($passes as [$advisoriesToOutput, $message]) { foreach ($passes as [$advisoriesToOutput, $message]) {
[$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); [$pkgCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
if ($affectedPackagesCount > 0) { if ($pkgCount > 0) {
$plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies'; $plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies';
$pkgPlurality = $affectedPackagesCount === 1 ? '' : 's'; $pkgPlurality = $pkgCount === 1 ? '' : 's';
$punctuation = $format === 'summary' ? '.' : ':'; $punctuation = $format === 'summary' ? '.' : ':';
$io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $affectedPackagesCount, $pkgPlurality, $punctuation)); $io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $pkgCount, $pkgPlurality, $punctuation));
$this->outputAdvisories($io, $advisoriesToOutput, $format); $this->outputAdvisories($io, $advisoriesToOutput, $format);
} }
} }
@ -130,7 +136,7 @@ class Auditor
$this->outputAbandonedPackages($io, $abandonedPackages, $format); $this->outputAbandonedPackages($io, $abandonedPackages, $format);
} }
return $affectedPackagesCount + $abandonedCount; return $auditBitmask;
} }
/** /**
@ -139,7 +145,7 @@ class Auditor
*/ */
private function filterAbandonedPackages(array $packages): array private function filterAbandonedPackages(array $packages): array
{ {
return array_filter($packages, static function (PackageInterface $pkg) { return array_filter($packages, static function (PackageInterface $pkg): bool {
return $pkg instanceof CompletePackageInterface && $pkg->isAbandoned(); return $pkg instanceof CompletePackageInterface && $pkg->isAbandoned();
}); });
} }
@ -401,4 +407,22 @@ class Auditor
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>'; return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
} }
/**
* @return int-mask<self::STATUS_*>
*/
private function calculateBitmask(bool $hasVulnerablePackages, bool $hasAbandonedPackages): int
{
$bitmask = self::STATUS_OK;
if ($hasVulnerablePackages) {
$bitmask |= self::STATUS_VULNERABLE;
}
if ($hasAbandonedPackages) {
$bitmask |= self::STATUS_ABANDONED;
}
return $bitmask;
}
} }

View File

@ -37,7 +37,7 @@ class AuditorTest extends TestCase
], ],
'warningOnly' => true, 'warningOnly' => true,
], ],
'expected' => 0, 'expected' => Auditor::STATUS_OK,
'output' => 'No security vulnerability advisories found.', 'output' => 'No security vulnerability advisories found.',
]; ];
@ -50,7 +50,7 @@ class AuditorTest extends TestCase
], ],
'warningOnly' => true, 'warningOnly' => true,
], ],
'expected' => 1, 'expected' => Auditor::STATUS_VULNERABLE,
'output' => '<warning>Found 2 security vulnerability advisories affecting 1 package:</warning> 'output' => '<warning>Found 2 security vulnerability advisories affecting 1 package:</warning>
Package: vendor1/package1 Package: vendor1/package1
Severity: high Severity: high
@ -83,7 +83,7 @@ Reported at: 2022-05-25T13:21:00+00:00',
'warningOnly' => false, 'warningOnly' => false,
'abandoned' => Auditor::ABANDONED_IGNORE, 'abandoned' => Auditor::ABANDONED_IGNORE,
], ],
'expected' => 0, 'expected' => Auditor::STATUS_OK,
'output' => 'No security vulnerability advisories found.', 'output' => 'No security vulnerability advisories found.',
]; ];
@ -96,7 +96,7 @@ Reported at: 2022-05-25T13:21:00+00:00',
'warningOnly' => true, 'warningOnly' => true,
'abandoned' => Auditor::ABANDONED_REPORT, 'abandoned' => Auditor::ABANDONED_REPORT,
], ],
'expected' => 0, 'expected' => Auditor::STATUS_OK,
'output' => 'No security vulnerability advisories found. 'output' => 'No security vulnerability advisories found.
Found 2 abandoned packages: Found 2 abandoned packages:
vendor/abandoned is abandoned. Use foo/bar instead. vendor/abandoned is abandoned. Use foo/bar instead.
@ -113,7 +113,7 @@ vendor/abandoned2 is abandoned. No replacement was suggested.',
'abandoned' => Auditor::ABANDONED_FAIL, 'abandoned' => Auditor::ABANDONED_FAIL,
'format' => Auditor::FORMAT_TABLE, 'format' => Auditor::FORMAT_TABLE,
], ],
'expected' => 2, 'expected' => Auditor::STATUS_ABANDONED,
'output' => 'No security vulnerability advisories found. 'output' => 'No security vulnerability advisories found.
Found 2 abandoned packages: Found 2 abandoned packages:
+-------------------+----------------------------------------------------------------------------------+ +-------------------+----------------------------------------------------------------------------------+
@ -124,6 +124,46 @@ Found 2 abandoned packages:
+-------------------+----------------------------------------------------------------------------------+', +-------------------+----------------------------------------------------------------------------------+',
]; ];
yield 'vulnerable and abandoned packages fails' => [
'data' => [
'packages' => [
new Package('vendor1/package1', '8.2.1', '8.2.1'),
$abandonedWithReplacement,
$abandonedNoReplacement,
],
'warningOnly' => false,
'abandoned' => Auditor::ABANDONED_FAIL,
'format' => Auditor::FORMAT_TABLE,
],
'expected' => Auditor::STATUS_VULNERABLE | Auditor::STATUS_ABANDONED,
'output' => 'Found 2 security vulnerability advisories affecting 1 package:
+-------------------+----------------------------------------------------------------------------------+
| Package | vendor1/package1 |
| Severity | high |
| CVE | CVE3 |
| Title | advisory4 |
| URL | https://advisory.example.com/advisory4 |
| Affected versions | >=8,<8.2.2|>=1,<2.5.6 |
| Reported at | 2022-05-25T13:21:00+00:00 |
+-------------------+----------------------------------------------------------------------------------+
+-------------------+----------------------------------------------------------------------------------+
| Package | vendor1/package1 |
| Severity | medium |
| CVE | |
| Title | advisory5 |
| URL | https://advisory.example.com/advisory5 |
| Affected versions | >=8,<8.2.2|>=1,<2.5.6 |
| Reported at | 2022-05-25T13:21:00+00:00 |
+-------------------+----------------------------------------------------------------------------------+
Found 2 abandoned packages:
+-------------------+----------------------------------------------------------------------------------+
| Abandoned Package | Suggested Replacement |
+-------------------+----------------------------------------------------------------------------------+
| vendor/abandoned | foo/bar |
| vendor/abandoned2 | none |
+-------------------+----------------------------------------------------------------------------------+',
];
yield 'abandoned packages fails with json format' => [ yield 'abandoned packages fails with json format' => [
'data' => [ 'data' => [
'packages' => [ 'packages' => [
@ -134,7 +174,7 @@ Found 2 abandoned packages:
'abandoned' => Auditor::ABANDONED_FAIL, 'abandoned' => Auditor::ABANDONED_FAIL,
'format' => Auditor::FORMAT_JSON, 'format' => Auditor::FORMAT_JSON,
], ],
'expected' => 2, 'expected' => Auditor::STATUS_ABANDONED,
'output' => '{ 'output' => '{
"advisories": [], "advisories": [],
"abandoned": { "abandoned": {