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
parent
38cb4bfe71
commit
e468b73cb2
|
@ -53,6 +53,11 @@ class Auditor
|
|||
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 self::FORMAT_* $format The format that will be used to output audit results.
|
||||
|
@ -61,7 +66,7 @@ class Auditor
|
|||
* @param self::ABANDONED_* $abandoned
|
||||
* @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
|
||||
*/
|
||||
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);
|
||||
|
||||
$abandonedCount = 0;
|
||||
$affectedPackagesCount = 0;
|
||||
$affectedPackagesCount = count($advisories);
|
||||
if ($abandoned === self::ABANDONED_IGNORE) {
|
||||
$abandonedPackages = [];
|
||||
} else {
|
||||
|
@ -85,6 +90,8 @@ class Auditor
|
|||
}
|
||||
}
|
||||
|
||||
$auditBitmask = $this->calculateBitmask(0 < $affectedPackagesCount, 0 < $abandonedCount);
|
||||
|
||||
if (self::FORMAT_JSON === $format) {
|
||||
$json = ['advisories' => $advisories];
|
||||
if ($ignoredAdvisories !== []) {
|
||||
|
@ -98,23 +105,22 @@ class Auditor
|
|||
|
||||
$io->write(JsonFile::encode($json));
|
||||
|
||||
return count($advisories) + $abandonedCount;
|
||||
return $auditBitmask;
|
||||
}
|
||||
|
||||
$errorOrWarn = $warningOnly ? 'warning' : 'error';
|
||||
if (count($advisories) > 0 || count($ignoredAdvisories) > 0) {
|
||||
if ($affectedPackagesCount > 0 || count($ignoredAdvisories) > 0) {
|
||||
$passes = [
|
||||
[$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>"],
|
||||
];
|
||||
foreach ($passes as [$advisoriesToOutput, $message]) {
|
||||
[$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
|
||||
if ($affectedPackagesCount > 0) {
|
||||
[$pkgCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
|
||||
if ($pkgCount > 0) {
|
||||
$plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies';
|
||||
$pkgPlurality = $affectedPackagesCount === 1 ? '' : 's';
|
||||
$pkgPlurality = $pkgCount === 1 ? '' : 's';
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +136,7 @@ class Auditor
|
|||
$this->outputAbandonedPackages($io, $abandonedPackages, $format);
|
||||
}
|
||||
|
||||
return $affectedPackagesCount + $abandonedCount;
|
||||
return $auditBitmask;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,7 +145,7 @@ class Auditor
|
|||
*/
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
@ -401,4 +407,22 @@ class Auditor
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ class AuditorTest extends TestCase
|
|||
],
|
||||
'warningOnly' => true,
|
||||
],
|
||||
'expected' => 0,
|
||||
'expected' => Auditor::STATUS_OK,
|
||||
'output' => 'No security vulnerability advisories found.',
|
||||
];
|
||||
|
||||
|
@ -50,7 +50,7 @@ class AuditorTest extends TestCase
|
|||
],
|
||||
'warningOnly' => true,
|
||||
],
|
||||
'expected' => 1,
|
||||
'expected' => Auditor::STATUS_VULNERABLE,
|
||||
'output' => '<warning>Found 2 security vulnerability advisories affecting 1 package:</warning>
|
||||
Package: vendor1/package1
|
||||
Severity: high
|
||||
|
@ -83,7 +83,7 @@ Reported at: 2022-05-25T13:21:00+00:00',
|
|||
'warningOnly' => false,
|
||||
'abandoned' => Auditor::ABANDONED_IGNORE,
|
||||
],
|
||||
'expected' => 0,
|
||||
'expected' => Auditor::STATUS_OK,
|
||||
'output' => 'No security vulnerability advisories found.',
|
||||
];
|
||||
|
||||
|
@ -96,7 +96,7 @@ Reported at: 2022-05-25T13:21:00+00:00',
|
|||
'warningOnly' => true,
|
||||
'abandoned' => Auditor::ABANDONED_REPORT,
|
||||
],
|
||||
'expected' => 0,
|
||||
'expected' => Auditor::STATUS_OK,
|
||||
'output' => 'No security vulnerability advisories found.
|
||||
Found 2 abandoned packages:
|
||||
vendor/abandoned is abandoned. Use foo/bar instead.
|
||||
|
@ -113,7 +113,7 @@ vendor/abandoned2 is abandoned. No replacement was suggested.',
|
|||
'abandoned' => Auditor::ABANDONED_FAIL,
|
||||
'format' => Auditor::FORMAT_TABLE,
|
||||
],
|
||||
'expected' => 2,
|
||||
'expected' => Auditor::STATUS_ABANDONED,
|
||||
'output' => 'No security vulnerability advisories found.
|
||||
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' => [
|
||||
'data' => [
|
||||
'packages' => [
|
||||
|
@ -134,7 +174,7 @@ Found 2 abandoned packages:
|
|||
'abandoned' => Auditor::ABANDONED_FAIL,
|
||||
'format' => Auditor::FORMAT_JSON,
|
||||
],
|
||||
'expected' => 2,
|
||||
'expected' => Auditor::STATUS_ABANDONED,
|
||||
'output' => '{
|
||||
"advisories": [],
|
||||
"abandoned": {
|
||||
|
|
Loading…
Reference in New Issue