From 11f852683a97a47a31b226f59b6aa3dd0b54f39d Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Sun, 17 Nov 2024 19:09:07 -0300 Subject: [PATCH] Use a bitmask to produce deterministic exit codes for the "audit" command --- src/Composer/Advisory/Auditor.php | 37 +++++++++++--- tests/Composer/Test/Advisory/AuditorTest.php | 52 +++++++++++++++++--- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php index de8034bad..bc495c200 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 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 $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, "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 @@ -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 '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; + } } diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php index cc9efb985..ad5b95d98 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::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' => '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::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": {