Use a bitmask to produce deterministic exit codes for the "audit" command
parent
9fb833f97e
commit
11f852683a
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in New Issue