Skip to content

Commit

Permalink
Add audit.ignored config setting to ignore security advisories by id …
Browse files Browse the repository at this point in the history
…or CVE id, fixes #11298 (#11556)
  • Loading branch information
Seldaek committed Jul 21, 2023
1 parent 7f78dec commit 0cdabcc
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 7 deletions.
18 changes: 18 additions & 0 deletions doc/06-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ optionally be an object with package name patterns for keys for more granular in
> configuration in global and package configurations the string notation
> is translated to a `*` package pattern.
## audit

Security audit configuration options

### ignored

A set of advisory ids, remote ids or CVE ids that should be ignored and not reported as part of an audit.

```json
{
"config": {
"audit": {
"ignored": ["CVE-1234", "GHSA-xx", "PKSA-yy"]
}
}
}
```

## use-parent-dir

When running Composer in a directory where there is no composer.json, if there
Expand Down
13 changes: 13 additions & 0 deletions res/composer-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,19 @@
"type": ["string"]
}
},
"audit": {
"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"
}
}
}
},
"notify-on-install": {
"type": "boolean",
"description": "Composer allows repositories to define a notification URL, so that they get notified whenever a package from that repository is installed. This option allows you to disable that behaviour, defaults to true."
Expand Down
42 changes: 41 additions & 1 deletion src/Composer/Advisory/Auditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,18 @@ 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
* @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): int
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoredIds = []): int
{
$advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);

if (\count($ignoredIds) > 0) {
$advisories = $this->filterIgnoredAdvisories($advisories, $ignoredIds);
}

if (self::FORMAT_JSON === $format) {
$io->write(JsonFile::encode(['advisories' => $advisories]));

Expand All @@ -73,6 +79,40 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages,
return 0;
}

/**
* @phpstan-param array<string, array<PartialSecurityAdvisory|SecurityAdvisory>> $advisories
* @param array<string> $ignoredIds
* @phpstan-return array<string, array<PartialSecurityAdvisory|SecurityAdvisory>>
*/
private function filterIgnoredAdvisories(array $advisories, array $ignoredIds): array
{
foreach ($advisories as $package => $pkgAdvisories) {
$advisories[$package] = array_filter($pkgAdvisories, static function (PartialSecurityAdvisory $advisory) use ($ignoredIds) {
if (in_array($advisory->advisoryId, $ignoredIds, true)) {
return false;
}
if ($advisory instanceof SecurityAdvisory) {
if (in_array($advisory->cve, $ignoredIds, true)) {
return false;
}

foreach ($advisory->sources as $source) {
if (in_array($source['remoteId'], $ignoredIds, true)) {
return false;
}
}
}

return true;
});
if (\count($advisories[$package]) === 0) {
unset($advisories[$package]);
}
}

return $advisories;
}

/**
* @param array<string, array<PartialSecurityAdvisory>> $advisories
* @return array{int, int} Count of affected packages and total count of advisories
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Command/AuditCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$repoSet->addRepository($repo);
}

return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false));
return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $composer->getConfig()->get('audit')['ignored'] ?? []));
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/Composer/Command/ConfigCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,27 @@ static function ($vals) {
return $vals;
},
],
'audit.ignore' => [
static function ($vals) {
if (!is_array($vals)) {
return 'array expected';
}

return true;
},
static function ($vals) {
return $vals;
},
],
];

// allow unsetting audit config entirely
if ($input->getOption('unset') && $settingKey === 'audit') {
$this->configSource->removeConfigSetting($settingKey);

return 0;
}

if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) {
if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) {
$this->getIO()->writeError('<info>You are now running Composer with SSL/TLS protection enabled.</info>');
Expand Down
6 changes: 6 additions & 0 deletions src/Composer/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Config
'allow-plugins' => [],
'use-parent-dir' => 'prompt',
'preferred-install' => 'dist',
'audit' => ['ignored' => []],
'notify-on-install' => true,
'github-protocols' => ['https', 'ssh', 'git'],
'gitlab-protocol' => null,
Expand Down Expand Up @@ -207,6 +208,11 @@ public function merge(array $config, string $source = self::SOURCE_UNKNOWN): voi
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
}
} elseif ('audit' === $key) {
$currentIgnores = $this->config['audit']['ignored'];
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
$this->config['audit']['ignored'] = array_merge($currentIgnores, $val['ignored']);
} else {
$this->config[$key] = $val;
$this->setSourceOfConfigValue($val, $key, $source);
Expand Down
2 changes: 1 addition & 1 deletion src/Composer/Installer.php
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ public function run(): int
$repoSet->addRepository($repo);
}

return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat) > 0 ? self::ERROR_AUDIT_FAILED : 0;
return $auditor->audit($this->io, $repoSet, $packages, $this->auditFormat, true, $this->config->get('audit')['ignored'] ?? []) > 0 ? self::ERROR_AUDIT_FAILED : 0;
} catch (TransportException $e) {
$this->io->error('Failed to audit '.$target.' packages.');
if ($this->io->isVerbose()) {
Expand Down
38 changes: 34 additions & 4 deletions tests/Composer/Test/Advisory/AuditorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

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;
Expand Down Expand Up @@ -71,6 +72,35 @@ public function testAudit(array $data, int $expected, string $message): void
$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'),
];

$ignoredIds = ['CVE1', 'ID2', 'RemoteIDx'];

$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);
}

private function getRepoSet(): RepositorySet
{
$repo = $this
Expand Down Expand Up @@ -160,7 +190,7 @@ public static function getMockAdvisories(): array
'sources' => [
[
'name' => 'source2',
'remoteId' => 'RemoteID2',
'remoteId' => 'RemoteID4',
],
],
'reportedAt' => '2022-05-25 13:21:00',
Expand Down Expand Up @@ -205,14 +235,14 @@ public static function getMockAdvisories(): array
[
'advisoryId' => 'IDx',
'packageName' => 'vendorx/packagex',
'title' => 'advisory7',
'link' => 'https://advisory.example.com/advisory7',
'title' => 'advisory17',
'link' => 'https://advisory.example.com/advisory17',
'cve' => 'CVE5',
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
'sources' => [
[
'name' => 'source2',
'remoteId' => 'RemoteID4',
'remoteId' => 'RemoteIDx',
],
],
'reportedAt' => '2015-05-25 13:21:00',
Expand Down

0 comments on commit 0cdabcc

Please sign in to comment.