Skip to content

Commit 2120e1d

Browse files
committed
feat: Added --ignore-unreachable flag to audit command for private/unreachable repositories.
1 parent ce8dda6 commit 2120e1d

File tree

3 files changed

+77
-7
lines changed

3 files changed

+77
-7
lines changed

src/Composer/Advisory/Auditor.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,22 @@ class Auditor
6969
* @return int-mask<self::STATUS_*> A bitmask of STATUS_* constants or 0 on success
7070
* @throws InvalidArgumentException If no packages are passed in
7171
*/
72-
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL, array $ignoredSeverities = []): int
72+
public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL, array $ignoredSeverities = [], bool $ignoreUnreachable = false): int
7373
{
74-
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
75-
// we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above
76-
// and ignores are set then we need to query again the full data to make sure it can be filtered
77-
if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) {
78-
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
74+
$unreachableRepos = [];
75+
try {
76+
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY);
77+
// we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above
78+
// and ignores are set then we need to query again the full data to make sure it can be filtered
79+
if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) {
80+
$allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false);
81+
}
82+
} catch (\Composer\Downloader\TransportException $e) {
83+
if (!$ignoreUnreachable) {
84+
throw $e;
85+
}
86+
$unreachableRepos[] = $e->getMessage();
87+
$allAdvisories = [];
7988
}
8089
['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities);
8190

@@ -97,6 +106,9 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages,
97106
if ($ignoredAdvisories !== []) {
98107
$json['ignored-advisories'] = $ignoredAdvisories;
99108
}
109+
if ($unreachableRepos !== []) {
110+
$json['unreachable-repositories'] = $unreachableRepos;
111+
}
100112
$json['abandoned'] = array_reduce($abandonedPackages, static function (array $carry, CompletePackageInterface $package): array {
101113
$carry[$package->getPrettyName()] = $package->getReplacementPackage();
102114

@@ -132,6 +144,13 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages,
132144
$io->writeError('<info>No security vulnerability advisories found.</info>');
133145
}
134146

147+
if (count($unreachableRepos) > 0) {
148+
$io->writeError('<warning>The following repositories were unreachable:</warning>');
149+
foreach ($unreachableRepos as $repo) {
150+
$io->writeError(' - ' . $repo);
151+
}
152+
}
153+
135154
if (count($abandonedPackages) > 0 && $format !== self::FORMAT_SUMMARY) {
136155
$this->outputAbandonedPackages($io, $abandonedPackages, $format);
137156
}

src/Composer/Command/AuditCommand.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ protected function configure(): void
3535
new InputOption('locked', null, InputOption::VALUE_NONE, 'Audit based on the lock file instead of the installed packages.'),
3636
new InputOption('abandoned', null, InputOption::VALUE_REQUIRED, 'Behavior on abandoned packages. Must be "ignore", "report", or "fail".', null, Auditor::ABANDONEDS),
3737
new InputOption('ignore-severity', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Ignore advisories of a certain severity level.', [], ['low', 'medium', 'high', 'critical']),
38+
new InputOption('ignore-unreachable', null, InputOption::VALUE_NONE, 'Ignore repositories that are unreachable or return a non-200 status code.'),
3839
])
3940
->setHelp(
4041
<<<EOT
4142
The <info>audit</info> command checks for security vulnerability advisories for installed packages.
4243
4344
If you do not want to include dev dependencies in the audit you can omit them with --no-dev
4445
46+
If you want to ignore repositories that are unreachable or return a non-200 status code, use --ignore-unreachable
47+
4548
Read more at https://getcomposer.org/doc/03-cli.md#audit
4649
EOT
4750
)
@@ -75,6 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7578
$abandoned = $abandoned ?? $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL;
7679

7780
$ignoreSeverities = $input->getOption('ignore-severity') ?? [];
81+
$ignoreUnreachable = $input->getOption('ignore-unreachable');
7882

7983
return min(255, $auditor->audit(
8084
$this->getIO(),
@@ -84,7 +88,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8488
false,
8589
$auditConfig['ignore'] ?? [],
8690
$abandoned,
87-
$ignoreSeverities
91+
$ignoreSeverities,
92+
$ignoreUnreachable
8893
));
8994

9095
}

tests/Composer/Test/Advisory/AuditorTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,52 @@ public function ignoreSeverityProvider(): \Generator
387387
];
388388
}
389389

390+
public function testAuditWithIgnoreUnreachable(): void
391+
{
392+
$packages = [
393+
new Package('vendor1/package1', '3.0.0.0', '3.0.0'),
394+
];
395+
396+
// Create a mock RepositorySet that throws a TransportException
397+
$repoSet = $this->getMockBuilder(RepositorySet::class)
398+
->disableOriginalConstructor()
399+
->onlyMethods(['getMatchingSecurityAdvisories'])
400+
->getMock();
401+
402+
$repoSet->method('getMatchingSecurityAdvisories')
403+
->willThrowException(new \Composer\Downloader\TransportException('The "https://example.org/packages.json" file could not be downloaded: HTTP/1.1 404 Not Found', 404));
404+
405+
$auditor = new Auditor();
406+
407+
// Test without ignoreUnreachable flag
408+
try {
409+
$auditor->audit(new BufferIO(), $repoSet, $packages, Auditor::FORMAT_PLAIN, false);
410+
self::fail('Expected TransportException was not thrown');
411+
} catch (\Composer\Downloader\TransportException $e) {
412+
self::assertStringContainsString('HTTP/1.1 404 Not Found', $e->getMessage());
413+
}
414+
415+
// Test with ignoreUnreachable flag
416+
$io = new BufferIO();
417+
$result = $auditor->audit($io, $repoSet, $packages, Auditor::FORMAT_PLAIN, false, [], Auditor::ABANDONED_IGNORE, [], true);
418+
self::assertSame(Auditor::STATUS_OK, $result);
419+
420+
$output = $io->getOutput();
421+
self::assertStringContainsString('Some repositories were unreachable', $output);
422+
self::assertStringContainsString('The following repositories were unreachable:', $output);
423+
self::assertStringContainsString('HTTP/1.1 404 Not Found', $output);
424+
425+
// Test with JSON format
426+
$io = new BufferIO();
427+
$result = $auditor->audit($io, $repoSet, $packages, Auditor::FORMAT_JSON, false, [], Auditor::ABANDONED_IGNORE, [], true);
428+
self::assertSame(Auditor::STATUS_OK, $result);
429+
430+
$json = json_decode($io->getOutput(), true);
431+
self::assertArrayHasKey('unreachable-repositories', $json);
432+
self::assertCount(1, $json['unreachable-repositories']);
433+
self::assertStringContainsString('HTTP/1.1 404 Not Found', $json['unreachable-repositories'][0]);
434+
}
435+
390436
/**
391437
* @dataProvider ignoreSeverityProvider
392438
* @phpstan-param array<\Composer\Package\Package> $packages

0 commit comments

Comments
 (0)