Skip to content

Commit

Permalink
Add audit.ignore config setting to ignore security advisories by id o…
Browse files Browse the repository at this point in the history
…r CVE id, fixes #11298
  • Loading branch information
Seldaek committed Jul 18, 2023
1 parent 196ac10 commit d7458f5
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 d7458f5

Please sign in to comment.