1
0
Fork 0

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

pull/12203/head
Javier Spagnoletti 2024-11-17 19:09:07 -03:00
parent 9fb833f97e
commit 11f852683a
2 changed files with 77 additions and 12 deletions

View File

@ -53,6 +53,11 @@ class Auditor
self::ABANDONED_FAIL,
];
/** Values to determine the audit result. */
public const BIT_OK = 0;
public const BIT_VULNERABLE = 1;
public const BIT_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 A bitmask calculation of vulnerable and abandoned packages 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, 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,11 +105,11 @@ 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
@ -130,7 +137,7 @@ class Auditor
$this->outputAbandonedPackages($io, $abandonedPackages, $format);
}
return $affectedPackagesCount + $abandonedCount;
return $auditBitmask;
}
/**
@ -139,7 +146,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 +408,22 @@ class Auditor
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
}
/**
* Produces a bitmask from the audit results.
*/
private function calculateBitmask(bool $hasVulnerablePackages, bool $hasabandonedPackages): int
{
$bitmask = self::BIT_OK;
if ($hasVulnerablePackages) {
$bitmask |= self::BIT_VULNERABLE;
}
if ($hasabandonedPackages) {
$bitmask |= self::BIT_ABANDONED;
}
return $bitmask;
}
}

View File

@ -37,7 +37,7 @@ class AuditorTest extends TestCase
],
'warningOnly' => true,
],
'expected' => 0,
'expected' => Auditor::BIT_OK,
'output' => 'No security vulnerability advisories found.',
];
@ -50,7 +50,7 @@ class AuditorTest extends TestCase
],
'warningOnly' => true,
],
'expected' => 1,
'expected' => Auditor::BIT_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::BIT_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::BIT_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::BIT_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::BIT_VULNERABLE | Auditor::BIT_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::BIT_ABANDONED,
'output' => '{
"advisories": [],
"abandoned": {