diff --git a/doc/06-config.md b/doc/06-config.md
index 615fd0d28..61391e276 100644
--- a/doc/06-config.md
+++ b/doc/06-config.md
@@ -105,15 +105,31 @@ optionally be an object with package name patterns for keys for more granular in
Security audit configuration options
-### ignored
+### ignore
-A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.
+A list of advisory ids, remote ids or CVE ids that are reported but let the audit command pass.
```json
{
"config": {
"audit": {
- "ignored": ["CVE-1234", "GHSA-xx", "PKSA-yy"]
+ "ignore": {
+ "CVE-1234": "The affected component is not in use.",
+ "GHSA-xx": "The security fix was applied as a patch.",
+ "PKSA-yy": "Due to mitigations in place the update can be delayed."
+ }
+ }
+ }
+}
+```
+
+or
+
+```json
+{
+ "config": {
+ "audit": {
+ "ignore": ["CVE-1234", "GHSA-xx", "PKSA-yy"]
}
}
}
diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon
index cad1d17ff..73f87c388 100644
--- a/phpstan/baseline.neon
+++ b/phpstan/baseline.neon
@@ -5,6 +5,11 @@ parameters:
count: 1
path: ../src/Composer/Advisory/Auditor.php
+ -
+ message: "#^Variable \\$affectedPackagesCount might not be defined\\.$#"
+ count: 1
+ path: ../src/Composer/Advisory/Auditor.php
+
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 10
@@ -3575,7 +3580,7 @@ parameters:
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
- count: 6
+ count: 5
path: ../src/Composer/Repository/Vcs/GitHubDriver.php
-
@@ -3610,7 +3615,7 @@ parameters:
-
message: "#^Only booleans are allowed in a negated boolean, string given\\.$#"
- count: 2
+ count: 1
path: ../src/Composer/Repository/Vcs/GitHubDriver.php
-
diff --git a/res/composer-schema.json b/res/composer-schema.json
index 9757c8950..fd469d51c 100644
--- a/res/composer-schema.json
+++ b/res/composer-schema.json
@@ -329,12 +329,23 @@
"type": "object",
"description": "Security audit configuration options",
"properties": {
- "ignored": {
- "type": "array",
- "description": "A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.",
- "items": {
- "type": "string"
- }
+ "ignore": {
+ "anyOf": [
+ {
+ "type": "object",
+ "description": "A list of advisory ids, remote ids or CVE ids (keys) and the explanations (values) for why they're being ignored. The listed items are reported but let the audit command pass.",
+ "additionalProperties": {
+ "type": ["string", "string"]
+ }
+ },
+ {
+ "type": "array",
+ "description": "A set of advisory ids, remote ids or CVE ids that are reported but let the audit command pass.",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
}
}
},
diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php
index c48f54d47..5a8397c64 100644
--- a/src/Composer/Advisory/Auditor.php
+++ b/src/Composer/Advisory/Auditor.php
@@ -44,34 +44,55 @@ class Auditor
* @param PackageInterface[] $packages
* @param self::FORMAT_* $format The format that will be used to output audit results.
* @param bool $warningOnly If true, outputs a warning. If false, outputs an error.
- * @param string[] $ignoredIds Ignored advisory IDs, remote IDs or CVE IDs
+ * @param string[] $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities.
+ *
* @return int Amount of packages with vulnerabilities found
* @throws InvalidArgumentException If no packages are passed in
*/
- public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoredIds = []): int
+ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = []): int
{
- $advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
-
- if (\count($ignoredIds) > 0) {
- $advisories = $this->filterIgnoredAdvisories($advisories, $ignoredIds);
+ $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
+ // we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above
+ // and ignores are set then we need to query again the full data to make sure it can be filtered
+ if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) {
+ $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
}
+ ['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList);
if (self::FORMAT_JSON === $format) {
- $io->write(JsonFile::encode(['advisories' => $advisories]));
+ $json = ['advisories' => $advisories];
+ if ($ignoredAdvisories !== []) {
+ $json['ignored-advisories'] = $ignoredAdvisories;
+ }
+
+ $io->write(JsonFile::encode($json));
return count($advisories);
}
$errorOrWarn = $warningOnly ? 'warning' : 'error';
- if (count($advisories) > 0) {
- [$affectedPackages, $totalAdvisories] = $this->countAdvisories($advisories);
- $plurality = $totalAdvisories === 1 ? 'y' : 'ies';
- $pkgPlurality = $affectedPackages === 1 ? '' : 's';
- $punctuation = $format === 'summary' ? '.' : ':';
- $io->writeError("<$errorOrWarn>Found $totalAdvisories security vulnerability advisor{$plurality} affecting $affectedPackages package{$pkgPlurality}{$punctuation}$errorOrWarn>");
- $this->outputAdvisories($io, $advisories, $format);
+ if (count($advisories) > 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$errorOrWarn>"],
+ ];
+ foreach ($passes as [$advisoriesToOutput, $message]) {
+ [$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput);
+ if ($affectedPackagesCount > 0) {
+ $plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies';
+ $pkgPlurality = $affectedPackagesCount === 1 ? '' : 's';
+ $punctuation = $format === 'summary' ? '.' : ':';
+ $io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $affectedPackagesCount, $pkgPlurality, $punctuation));
+ $this->outputAdvisories($io, $advisoriesToOutput, $format);
+ }
+ }
- return $affectedPackages;
+ if ($format === self::FORMAT_SUMMARY) {
+ $io->writeError('Run "composer audit" for a full list of advisories.');
+ }
+
+ return $affectedPackagesCount;
}
$io->writeError('No security vulnerability advisories found');
@@ -80,37 +101,66 @@ class Auditor
}
/**
- * @phpstan-param array> $advisories
- * @param array $ignoredIds
- * @phpstan-return array>
+ * @phpstan-param array> $allAdvisories
+ * @param array|array $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities.
+ * @phpstan-return array{advisories: array>, ignoredAdvisories: array>}
*/
- private function filterIgnoredAdvisories(array $advisories, array $ignoredIds): array
+ private function processAdvisories(array $allAdvisories, array $ignoreList): array
{
- foreach ($advisories as $package => $pkgAdvisories) {
- $advisories[$package] = array_filter($pkgAdvisories, static function (PartialSecurityAdvisory $advisory) use ($ignoredIds) {
+ if ($ignoreList === []) {
+ return ['advisories' => $allAdvisories, 'ignoredAdvisories' => []];
+ }
+
+ if (\count($ignoreList) > 0 && !\array_is_list($ignoreList)) {
+ $ignoredIds = array_keys($ignoreList);
+ } else {
+ $ignoredIds = $ignoreList;
+ }
+
+ $advisories = [];
+ $ignored = [];
+ $ignoreReason = null;
+
+ foreach ($allAdvisories as $package => $pkgAdvisories) {
+ foreach ($pkgAdvisories as $advisory) {
+ $isActive = true;
+
if (in_array($advisory->advisoryId, $ignoredIds, true)) {
- return false;
+ $isActive = false;
+ $ignoreReason = $ignoreList[$advisory->advisoryId] ?? null;
}
+
if ($advisory instanceof SecurityAdvisory) {
if (in_array($advisory->cve, $ignoredIds, true)) {
- return false;
+ $isActive = false;
+ $ignoreReason = $ignoreList[$advisory->cve] ?? null;
}
foreach ($advisory->sources as $source) {
if (in_array($source['remoteId'], $ignoredIds, true)) {
- return false;
+ $isActive = false;
+ $ignoreReason = $ignoreList[$source['remoteId']] ?? null;
+ break;
}
}
}
- return true;
- });
- if (\count($advisories[$package]) === 0) {
- unset($advisories[$package]);
+ if ($isActive) {
+ $advisories[$package][] = $advisory;
+ continue;
+ }
+
+ // Partial security advisories only used in summary mode
+ // and in that case we do not need to cast the object.
+ if ($advisory instanceof SecurityAdvisory) {
+ $advisory = $advisory->toIgnoredAdvisory($ignoreReason);
+ }
+
+ $ignored[$package][] = $advisory;
}
}
- return $advisories;
+ return ['advisories' => $advisories, 'ignoredAdvisories' => $ignored];
}
/**
@@ -146,8 +196,6 @@ class Auditor
return;
case self::FORMAT_SUMMARY:
- // We've already output the number of advisories in audit()
- $io->writeError('Run composer audit for a full list of advisories.');
return;
default:
@@ -162,24 +210,30 @@ class Auditor
{
foreach ($advisories as $packageAdvisories) {
foreach ($packageAdvisories as $advisory) {
+ $headers = [
+ 'Package',
+ 'CVE',
+ 'Title',
+ 'URL',
+ 'Affected versions',
+ 'Reported at',
+ ];
+ $row = [
+ $advisory->packageName,
+ $this->getCVE($advisory),
+ $advisory->title,
+ $this->getURL($advisory),
+ $advisory->affectedVersions->getPrettyString(),
+ $advisory->reportedAt->format(DATE_ATOM),
+ ];
+ if ($advisory instanceof IgnoredSecurityAdvisory) {
+ $headers[] = 'Ignore reason';
+ $row[] = $advisory->ignoreReason ?? 'None specified';
+ }
$io->getTable()
->setHorizontal()
- ->setHeaders([
- 'Package',
- 'CVE',
- 'Title',
- 'URL',
- 'Affected versions',
- 'Reported at',
- ])
- ->addRow([
- $advisory->packageName,
- $this->getCVE($advisory),
- $advisory->title,
- $this->getURL($advisory),
- $advisory->affectedVersions->getPrettyString(),
- $advisory->reportedAt->format(DATE_ATOM),
- ])
+ ->setHeaders($headers)
+ ->addRow($row)
->setColumnWidth(1, 80)
->setColumnMaxWidth(1, 80)
->render();
@@ -205,6 +259,9 @@ class Auditor
$error[] = "URL: ".$this->getURL($advisory);
$error[] = "Affected versions: ".OutputFormatter::escape($advisory->affectedVersions->getPrettyString());
$error[] = "Reported at: ".$advisory->reportedAt->format(DATE_ATOM);
+ if ($advisory instanceof IgnoredSecurityAdvisory) {
+ $error[] = "Ignore reason: ".($advisory->ignoreReason ?? 'None specified');
+ }
$firstAdvisory = false;
}
}
@@ -228,4 +285,5 @@ class Auditor
return 'link).'>'.OutputFormatter::escape($advisory->link).'>';
}
+
}
diff --git a/src/Composer/Advisory/IgnoredSecurityAdvisory.php b/src/Composer/Advisory/IgnoredSecurityAdvisory.php
new file mode 100644
index 000000000..ba9079287
--- /dev/null
+++ b/src/Composer/Advisory/IgnoredSecurityAdvisory.php
@@ -0,0 +1,50 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Advisory;
+
+use Composer\Semver\Constraint\ConstraintInterface;
+use DateTimeImmutable;
+
+class IgnoredSecurityAdvisory extends SecurityAdvisory
+{
+ /**
+ * @var string|null
+ * @readonly
+ */
+ public $ignoreReason;
+
+ /**
+ * @param non-empty-array $sources
+ */
+ public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null, ?string $ignoreReason = null)
+ {
+ parent::__construct($packageName, $advisoryId, $affectedVersions, $title, $sources, $reportedAt, $cve, $link);
+
+ $this->ignoreReason = $ignoreReason;
+ }
+
+ /**
+ * @return mixed
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ $data = parent::jsonSerialize();
+ if ($this->ignoreReason === NULL) {
+ unset($data['ignoreReason']);
+ }
+
+ return $data;
+ }
+
+}
diff --git a/src/Composer/Advisory/SecurityAdvisory.php b/src/Composer/Advisory/SecurityAdvisory.php
index 8fdf4dd55..e88228d60 100644
--- a/src/Composer/Advisory/SecurityAdvisory.php
+++ b/src/Composer/Advisory/SecurityAdvisory.php
@@ -42,14 +42,13 @@ class SecurityAdvisory extends PartialSecurityAdvisory
public $reportedAt;
/**
- * @var array
+ * @var non-empty-array
* @readonly
*/
public $sources;
/**
* @param non-empty-array $sources
- * @readonly
*/
public function __construct(string $packageName, string $advisoryId, ConstraintInterface $affectedVersions, string $title, array $sources, DateTimeImmutable $reportedAt, ?string $cve = null, ?string $link = null)
{
@@ -62,6 +61,24 @@ class SecurityAdvisory extends PartialSecurityAdvisory
$this->link = $link;
}
+ /**
+ * @internal
+ */
+ public function toIgnoredAdvisory(?string $ignoreReason): IgnoredSecurityAdvisory
+ {
+ return new IgnoredSecurityAdvisory(
+ $this->packageName,
+ $this->advisoryId,
+ $this->affectedVersions,
+ $this->title,
+ $this->sources,
+ $this->reportedAt,
+ $this->cve,
+ $this->link,
+ $ignoreReason
+ );
+ }
+
/**
* @return mixed
*/
diff --git a/src/Composer/Command/AuditCommand.php b/src/Composer/Command/AuditCommand.php
index ed97bbc94..3c58d7feb 100644
--- a/src/Composer/Command/AuditCommand.php
+++ b/src/Composer/Command/AuditCommand.php
@@ -63,7 +63,7 @@ EOT
$repoSet->addRepository($repo);
}
- return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $composer->getConfig()->get('audit')['ignored'] ?? []));
+ return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $composer->getConfig()->get('audit')['ignore'] ?? []));
}
/**
diff --git a/src/Composer/Config.php b/src/Composer/Config.php
index 0523bbb2a..0c11ab505 100644
--- a/src/Composer/Config.php
+++ b/src/Composer/Config.php
@@ -37,7 +37,7 @@ class Config
'allow-plugins' => [],
'use-parent-dir' => 'prompt',
'preferred-install' => 'dist',
- 'audit' => ['ignored' => []],
+ 'audit' => ['ignore' => []],
'notify-on-install' => true,
'github-protocols' => ['https', 'ssh', 'git'],
'gitlab-protocol' => null,
@@ -209,10 +209,10 @@ class Config
$this->setSourceOfConfigValue($val, $key, $source);
}
} elseif ('audit' === $key) {
- $currentIgnores = $this->config['audit']['ignored'];
+ $currentIgnores = $this->config['audit']['ignore'];
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
- $this->config['audit']['ignored'] = array_merge($currentIgnores, $val['ignored'] ?? []);
+ $this->config['audit']['ignore'] = array_merge($currentIgnores, $val['ignore'] ?? []);
} else {
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php
index 8f40cfb06..eaee35f9d 100644
--- a/src/Composer/Installer.php
+++ b/src/Composer/Installer.php
@@ -402,7 +402,7 @@ class Installer
$repoSet->addRepository($repo);
}
- return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat, true, $this->config->get('audit')['ignored'] ?? []) > 0 ? self::ERROR_AUDIT_FAILED : 0;
+ return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat, true, $this->config->get('audit')['ignore'] ?? []) > 0 ? self::ERROR_AUDIT_FAILED : 0;
} catch (TransportException $e) {
$this->io->error('Failed to audit '.$target.' packages.');
if ($this->io->isVerbose()) {
diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php
index ca87e8e9a..84b8693cf 100644
--- a/tests/Composer/Test/Advisory/AuditorTest.php
+++ b/tests/Composer/Test/Advisory/AuditorTest.php
@@ -14,7 +14,6 @@ namespace Composer\Test\Advisory;
use Composer\Advisory\PartialSecurityAdvisory;
use Composer\Advisory\SecurityAdvisory;
-use Composer\IO\BufferIO;
use Composer\IO\NullIO;
use Composer\Package\Package;
use Composer\Package\Version\VersionParser;
@@ -72,33 +71,147 @@ class AuditorTest extends TestCase
$this->assertSame($expected, $result, $message);
}
- public function testAuditIgnoredIDs(): void
- {
- $packages = [
- new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
- new Package('vendor1/package2', '3.0.0.0', '3.0.0'),
- new Package('vendorx/packagex', '3.0.0.0', '3.0.0'),
- new Package('vendor3/package1', '3.0.0.0', '3.0.0'),
+ public function ignoredIdsProvider(): \Generator {
+ yield 'ignore by CVE' => [
+ [
+ new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
+ ],
+ ['CVE1'],
+ 0,
+ [
+ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendor1/package1'],
+ ['text' => 'CVE: CVE1'],
+ ['text' => 'Title: advisory1'],
+ ['text' => 'URL: https://advisory.example.com/advisory1'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
+ ]
];
+ yield 'ignore by CVE with reasoning' => [
+ [
+ new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
+ ],
+ ['CVE1' => 'A good reason'],
+ 0,
+ [
+ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendor1/package1'],
+ ['text' => 'CVE: CVE1'],
+ ['text' => 'Title: advisory1'],
+ ['text' => 'URL: https://advisory.example.com/advisory1'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
+ ['text' => 'Ignore reason: A good reason'],
+ ]
+ ];
+ yield 'ignore by advisory id' => [
+ [
+ new Package('vendor1/package2', '3.0.0.0', '3.0.0'),
+ ],
+ ['ID2'],
+ 0,
+ [
+ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendor1/package2'],
+ ['text' => 'CVE: '],
+ ['text' => 'Title: advisory2'],
+ ['text' => 'URL: https://advisory.example.com/advisory2'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
+ ]
+ ];
+ yield 'ignore by remote id' => [
+ [
+ new Package('vendorx/packagex', '3.0.0.0', '3.0.0'),
+ ],
+ ['RemoteIDx'],
+ 0,
+ [
+ ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendorx/packagex'],
+ ['text' => 'CVE: CVE5'],
+ ['text' => 'Title: advisory17'],
+ ['text' => 'URL: https://advisory.example.com/advisory17'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
+ ]
+ ];
+ yield '1 vulnerability, 0 ignored' => [
+ [
+ new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
+ ],
+ [],
+ 1,
+ [
+ ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendor1/package1'],
+ ['text' => 'CVE: CVE1'],
+ ['text' => 'Title: advisory1'],
+ ['text' => 'URL: https://advisory.example.com/advisory1'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
+ ]
+ ];
+ yield '1 vulnerability, 3 ignored affecting 2 packages' => [
+ [
+ new Package('vendor3/package1', '3.0.0.0', '3.0.0'),
+ // RemoteIDx
+ new Package('vendorx/packagex', '3.0.0.0', '3.0.0'),
+ // ID3, ID6
+ new Package('vendor2/package1', '3.0.0.0', '3.0.0'),
+ ],
+ ['RemoteIDx', 'ID3', 'ID6'],
+ 1,
+ [
+ ['text' => 'Found 3 ignored security vulnerability advisories affecting 2 packages:'],
+ ['text' => 'Package: vendor2/package1'],
+ ['text' => 'CVE: CVE2'],
+ ['text' => 'Title: advisory3'],
+ ['text' => 'URL: https://advisory.example.com/advisory3'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'],
+ ['text' => 'Ignore reason: None specified'],
+ ['text' => '--------'],
+ ['text' => 'Package: vendor2/package1'],
+ ['text' => 'CVE: CVE4'],
+ ['text' => 'Title: advisory6'],
+ ['text' => 'URL: https://advisory.example.com/advisory6'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
+ ['text' => 'Ignore reason: None specified'],
+ ['text' => '--------'],
+ ['text' => 'Package: vendorx/packagex'],
+ ['text' => 'CVE: CVE5'],
+ ['text' => 'Title: advisory17'],
+ ['text' => 'URL: https://advisory.example.com/advisory17'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
+ ['text' => 'Ignore reason: None specified'],
+ ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
+ ['text' => 'Package: vendor3/package1'],
+ ['text' => 'CVE: CVE5'],
+ ['text' => 'Title: advisory7'],
+ ['text' => 'URL: https://advisory.example.com/advisory7'],
+ ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
+ ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
+ ]
+ ];
+ }
- $ignoredIds = ['CVE1', 'ID2', 'RemoteIDx'];
-
+ /**
+ * @dataProvider ignoredIdsProvider
+ * @phpstan-param array<\Composer\Package\Package> $packages
+ * @phpstan-param array|array $ignoredIds
+ * @phpstan-param 0|positive-int $exitCode
+ * @phpstan-param list $expectedOutput
+ */
+ public function testAuditWithIgnore($packages, $ignoredIds, $exitCode, $expectedOutput): void
+ {
$auditor = new Auditor();
$result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, $ignoredIds);
- $io->expects([
- ['text' => 'Found 1 security vulnerability advisory affecting 1 package:'],
- ['text' => 'Package: vendor3/package1'],
- ['text' => 'CVE: CVE5'],
- ['text' => 'Title: advisory7'],
- ['text' => 'URL: https://advisory.example.com/advisory7'],
- ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'],
- ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'],
- ], true);
- $this->assertSame(1, $result);
-
- // without ignored IDs, we should get all 4
- $result = $auditor->audit($io, $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false);
- $this->assertSame(4, $result);
+ $io->expects($expectedOutput, true);
+ $this->assertSame($exitCode, $result);
}
private function getRepoSet(): RepositorySet