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,
|
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 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-mask<self::STATUS_*> A bitmask of STATUS_* constants or 0 on success
|
||||||
* @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,23 +105,22 @@ 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
|
|
||||||
[$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s</$errorOrWarn>"],
|
[$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s</$errorOrWarn>"],
|
||||||
];
|
];
|
||||||
foreach ($passes as [$advisoriesToOutput, $message]) {
|
foreach ($passes as [$advisoriesToOutput, $message]) {
|
||||||
[$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
|
[$pkgCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
|
||||||
if ($affectedPackagesCount > 0) {
|
if ($pkgCount > 0) {
|
||||||
$plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies';
|
$plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies';
|
||||||
$pkgPlurality = $affectedPackagesCount === 1 ? '' : 's';
|
$pkgPlurality = $pkgCount === 1 ? '' : 's';
|
||||||
$punctuation = $format === 'summary' ? '.' : ':';
|
$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);
|
$this->outputAdvisories($io, $advisoriesToOutput, $format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +136,7 @@ class Auditor
|
||||||
$this->outputAbandonedPackages($io, $abandonedPackages, $format);
|
$this->outputAbandonedPackages($io, $abandonedPackages, $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $affectedPackagesCount + $abandonedCount;
|
return $auditBitmask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,7 +145,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 +407,22 @@ class Auditor
|
||||||
|
|
||||||
return '<href='.OutputFormatter::escape($advisory->link).'>'.OutputFormatter::escape($advisory->link).'</>';
|
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,
|
'warningOnly' => true,
|
||||||
],
|
],
|
||||||
'expected' => 0,
|
'expected' => Auditor::STATUS_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::STATUS_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::STATUS_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::STATUS_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::STATUS_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::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' => [
|
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::STATUS_ABANDONED,
|
||||||
'output' => '{
|
'output' => '{
|
||||||
"advisories": [],
|
"advisories": [],
|
||||||
"abandoned": {
|
"abandoned": {
|
||||||
|
|
Loading…
Reference in New Issue