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, 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 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 A bitmask calculation of vulnerable and abandoned packages 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, 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,11 +105,11 @@ 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 // 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); $this->outputAbandonedPackages($io, $abandonedPackages, $format);
} }
return $affectedPackagesCount + $abandonedCount; return $auditBitmask;
} }
/** /**
@ -139,7 +146,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 +408,22 @@ class Auditor
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>'; 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, 'warningOnly' => true,
], ],
'expected' => 0, 'expected' => Auditor::BIT_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::BIT_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::BIT_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::BIT_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::BIT_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::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' => [ 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::BIT_ABANDONED,
'output' => '{ 'output' => '{
"advisories": [], "advisories": [],
"abandoned": { "abandoned": {