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,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
Loading…
Reference in New Issue