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
|
> configuration in global and package configurations the string notation
|
||||||
> is translated to a `*` package pattern.
|
> 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
|
## use-parent-dir
|
||||||
|
|
||||||
When running Composer in a directory where there is no composer.json, if there
|
When running Composer in a directory where there is no composer.json, if there
|
||||||
|
|
|
@ -325,6 +325,19 @@
|
||||||
"type": ["string"]
|
"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": {
|
"notify-on-install": {
|
||||||
"type": "boolean",
|
"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."
|
"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 PackageInterface[] $packages
|
||||||
* @param self::FORMAT_* $format The format that will be used to output audit results.
|
* @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 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
|
* @return int Amount of packages with vulnerabilities found
|
||||||
* @throws InvalidArgumentException If no packages are passed in
|
* @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);
|
$advisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
|
||||||
|
|
||||||
|
if (\count($ignoredIds) > 0) {
|
||||||
|
$advisories = $this->filterIgnoredAdvisories($advisories, $ignoredIds);
|
||||||
|
}
|
||||||
|
|
||||||
if (self::FORMAT_JSON === $format) {
|
if (self::FORMAT_JSON === $format) {
|
||||||
$io->write(JsonFile::encode(['advisories' => $advisories]));
|
$io->write(JsonFile::encode(['advisories' => $advisories]));
|
||||||
|
|
||||||
|
@ -73,6 +79,40 @@ class Auditor
|
||||||
return 0;
|
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
|
* @param array<string, array<PartialSecurityAdvisory>> $advisories
|
||||||
* @return array{int, int} Count of affected packages and total count of advisories
|
* @return array{int, int} Count of affected packages and total count of advisories
|
||||||
|
|
|
@ -63,7 +63,7 @@ EOT
|
||||||
$repoSet->addRepository($repo);
|
$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;
|
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 ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) {
|
||||||
if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) {
|
if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) {
|
||||||
$this->getIO()->writeError('<info>You are now running Composer with SSL/TLS protection enabled.</info>');
|
$this->getIO()->writeError('<info>You are now running Composer with SSL/TLS protection enabled.</info>');
|
||||||
|
|
|
@ -37,6 +37,7 @@ class Config
|
||||||
'allow-plugins' => [],
|
'allow-plugins' => [],
|
||||||
'use-parent-dir' => 'prompt',
|
'use-parent-dir' => 'prompt',
|
||||||
'preferred-install' => 'dist',
|
'preferred-install' => 'dist',
|
||||||
|
'audit' => ['ignored' => []],
|
||||||
'notify-on-install' => true,
|
'notify-on-install' => true,
|
||||||
'github-protocols' => ['https', 'ssh', 'git'],
|
'github-protocols' => ['https', 'ssh', 'git'],
|
||||||
'gitlab-protocol' => null,
|
'gitlab-protocol' => null,
|
||||||
|
@ -207,6 +208,11 @@ class Config
|
||||||
$this->config[$key] = $val;
|
$this->config[$key] = $val;
|
||||||
$this->setSourceOfConfigValue($val, $key, $source);
|
$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 {
|
} else {
|
||||||
$this->config[$key] = $val;
|
$this->config[$key] = $val;
|
||||||
$this->setSourceOfConfigValue($val, $key, $source);
|
$this->setSourceOfConfigValue($val, $key, $source);
|
||||||
|
|
|
@ -402,7 +402,7 @@ class Installer
|
||||||
$repoSet->addRepository($repo);
|
$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) {
|
} catch (TransportException $e) {
|
||||||
$this->io->error('Failed to audit '.$target.' packages.');
|
$this->io->error('Failed to audit '.$target.' packages.');
|
||||||
if ($this->io->isVerbose()) {
|
if ($this->io->isVerbose()) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Composer\Test\Advisory;
|
||||||
|
|
||||||
use Composer\Advisory\PartialSecurityAdvisory;
|
use Composer\Advisory\PartialSecurityAdvisory;
|
||||||
use Composer\Advisory\SecurityAdvisory;
|
use Composer\Advisory\SecurityAdvisory;
|
||||||
|
use Composer\IO\BufferIO;
|
||||||
use Composer\IO\NullIO;
|
use Composer\IO\NullIO;
|
||||||
use Composer\Package\Package;
|
use Composer\Package\Package;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
|
@ -71,6 +72,35 @@ class AuditorTest extends TestCase
|
||||||
$this->assertSame($expected, $result, $message);
|
$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
|
private function getRepoSet(): RepositorySet
|
||||||
{
|
{
|
||||||
$repo = $this
|
$repo = $this
|
||||||
|
@ -160,7 +190,7 @@ class AuditorTest extends TestCase
|
||||||
'sources' => [
|
'sources' => [
|
||||||
[
|
[
|
||||||
'name' => 'source2',
|
'name' => 'source2',
|
||||||
'remoteId' => 'RemoteID2',
|
'remoteId' => 'RemoteID4',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'reportedAt' => '2022-05-25 13:21:00',
|
'reportedAt' => '2022-05-25 13:21:00',
|
||||||
|
@ -205,14 +235,14 @@ class AuditorTest extends TestCase
|
||||||
[
|
[
|
||||||
'advisoryId' => 'IDx',
|
'advisoryId' => 'IDx',
|
||||||
'packageName' => 'vendorx/packagex',
|
'packageName' => 'vendorx/packagex',
|
||||||
'title' => 'advisory7',
|
'title' => 'advisory17',
|
||||||
'link' => 'https://advisory.example.com/advisory7',
|
'link' => 'https://advisory.example.com/advisory17',
|
||||||
'cve' => 'CVE5',
|
'cve' => 'CVE5',
|
||||||
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
'affectedVersions' => '>=3,<3.4.3|>=1,<2.5.6',
|
||||||
'sources' => [
|
'sources' => [
|
||||||
[
|
[
|
||||||
'name' => 'source2',
|
'name' => 'source2',
|
||||||
'remoteId' => 'RemoteID4',
|
'remoteId' => 'RemoteIDx',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'reportedAt' => '2015-05-25 13:21:00',
|
'reportedAt' => '2015-05-25 13:21:00',
|
||||||
|
|
Loading…
Reference in New Issue