1
0
Fork 0

Add audit.ignored config setting to ignore security advisories by id or CVE id, fixes #11298 (#11556)

pull/11517/head^2
Jordi Boggiano 2023-07-21 14:36:38 +02:00 committed by GitHub
parent 7f78decad7
commit 0cdabcc4ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 133 additions and 7 deletions

View File

@ -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

View File

@ -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."

View File

@ -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]));
@ -73,6 +79,40 @@ class Auditor
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

View File

@ -63,7 +63,7 @@ EOT
$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'] ?? []));
}
/**

View File

@ -556,8 +556,27 @@ EOT
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>');

View File

@ -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,
@ -207,6 +208,11 @@ class Config
$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);

View File

@ -402,7 +402,7 @@ class Installer
$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()) {

View File

@ -14,6 +14,7 @@ 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;
@ -71,6 +72,35 @@ 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'),
];
$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
@ -160,7 +190,7 @@ class AuditorTest extends TestCase
'sources' => [
[
'name' => 'source2',
'remoteId' => 'RemoteID2',
'remoteId' => 'RemoteID4',
],
],
'reportedAt' => '2022-05-25 13:21:00',
@ -205,14 +235,14 @@ class AuditorTest extends TestCase
[
'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',