From e468b73cb225dde1d28fa805502ffa752f1eb7b3 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Mon, 25 Nov 2024 10:30:31 -0300 Subject: [PATCH] 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 --- src/Composer/Advisory/Auditor.php | 46 ++++++++++++----- tests/Composer/Test/Advisory/AuditorTest.php | 52 +++++++++++++++++--- 2 files changed, 81 insertions(+), 17 deletions(-) diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php index de8034bad..485b33267 100644 --- a/src/Composer/Advisory/Auditor.php +++ b/src/Composer/Advisory/Auditor.php @@ -53,6 +53,11 @@ class Auditor 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 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 $ignoredSeverities List of ignored severity levels * - * @return int Amount of packages with vulnerabilities found + * @return int-mask A bitmask of STATUS_* constants or 0 on success * @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,23 +105,22 @@ 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, "Found %d ignored security vulnerability advisor%s affecting %d package%s%s"], - // 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"], ]; foreach ($passes as [$advisoriesToOutput, $message]) { - [$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); - if ($affectedPackagesCount > 0) { + [$pkgCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); + if ($pkgCount > 0) { $plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies'; - $pkgPlurality = $affectedPackagesCount === 1 ? '' : 's'; + $pkgPlurality = $pkgCount === 1 ? '' : 's'; $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); } } @@ -130,7 +136,7 @@ class Auditor $this->outputAbandonedPackages($io, $abandonedPackages, $format); } - return $affectedPackagesCount + $abandonedCount; + return $auditBitmask; } /** @@ -139,7 +145,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 +407,22 @@ class Auditor return 'link).'>'.OutputFormatter::escape($advisory->link).''; } + + /** + * @return int-mask + */ + 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; + } } diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php index cc9efb985..a5472f0d8 100644 --- a/tests/Composer/Test/Advisory/AuditorTest.php +++ b/tests/Composer/Test/Advisory/AuditorTest.php @@ -37,7 +37,7 @@ class AuditorTest extends TestCase ], 'warningOnly' => true, ], - 'expected' => 0, + 'expected' => Auditor::STATUS_OK, 'output' => 'No security vulnerability advisories found.', ]; @@ -50,7 +50,7 @@ class AuditorTest extends TestCase ], 'warningOnly' => true, ], - 'expected' => 1, + 'expected' => Auditor::STATUS_VULNERABLE, 'output' => 'Found 2 security vulnerability advisories affecting 1 package: 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::STATUS_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::STATUS_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::STATUS_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::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' => [ 'data' => [ 'packages' => [ @@ -134,7 +174,7 @@ Found 2 abandoned packages: 'abandoned' => Auditor::ABANDONED_FAIL, 'format' => Auditor::FORMAT_JSON, ], - 'expected' => 2, + 'expected' => Auditor::STATUS_ABANDONED, 'output' => '{ "advisories": [], "abandoned": {