Add audit.ignored config setting to ignore security advisories by id or CVE id, fixes #11298 (#11556)
parent
7f78decad7
commit
0cdabcc4ee
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'] ?? []));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue