Files
coolify/tests/Feature/ImageScan/IgnoreMatcherFeatureTest.php
033f0c84da v4.7 (#12478)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Co-authored-by: Aditya Tripathi <aditya@climactic.co>
2026-11-06 14:25:02 +01:00

144 lines
6.3 KiB
PHP

<?php
use App\Models\Application;
use App\Models\Environment;
use App\Models\ImageScanIgnore;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Services\ImageScan\Finding;
use App\Services\ImageScan\IgnoreMatcher;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Once;
uses(RefreshDatabase::class);
beforeEach(function () {
Once::flush();
Server::flushIdentityMap();
InstanceSettings::forceCreate(['id' => 0]);
$this->team = Team::factory()->create();
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail();
$this->environment = Environment::factory()->create([
'project_id' => Project::factory()->create(['team_id' => $this->team->id])->id,
]);
$this->application = ignoreMatcherApplication($this->environment, $this->destination);
$this->finding = Finding::fromArray([
'id' => 'GHSA-jfh8-c2jp-5v3q', 'aliases' => ['CVE-2021-44228'], 'package' => 'log4j-core',
]);
});
function ignoreMatcherApplication(Environment $environment, StandaloneDocker $destination): Application
{
return Application::factory()->create([
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]);
}
it('loads a resource ignore and a team-wide ignore of the resource team', function () {
$own = ImageScanIgnore::factory()->create([
'team_id' => $this->team->id,
'resource_type' => $this->application->getMorphClass(),
'resource_id' => $this->application->id,
'vulnerability_id' => 'CVE-2021-44228',
]);
$wide = ImageScanIgnore::factory()->create([
'team_id' => $this->team->id,
'vulnerability_id' => null,
'package' => 'log4j-core',
]);
$matcher = IgnoreMatcher::forResource($this->application);
expect($matcher->matches($this->finding)->is($own))->toBeTrue()
->and($matcher->matches(Finding::fromArray(['id' => 'CVE-2020-1', 'package' => 'log4j-core']))->is($wide))->toBeTrue();
});
it('never loads an ignore of another team', function () {
$otherTeam = Team::factory()->create();
ImageScanIgnore::factory()->create(['team_id' => $otherTeam->id, 'vulnerability_id' => 'CVE-2021-44228']);
ImageScanIgnore::factory()->create([
'team_id' => $otherTeam->id,
'resource_type' => $this->application->getMorphClass(),
'resource_id' => $this->application->id,
'vulnerability_id' => 'CVE-2021-44228',
]);
expect(IgnoreMatcher::forResource($this->application)->matches($this->finding))->toBeNull();
});
it('does not load an ignore that belongs to another resource', function () {
$other = ignoreMatcherApplication($this->environment, $this->destination);
ImageScanIgnore::factory()->create([
'team_id' => $this->team->id,
'resource_type' => $other->getMorphClass(),
'resource_id' => $other->id,
'vulnerability_id' => 'CVE-2021-44228',
]);
expect(IgnoreMatcher::forResource($this->application)->matches($this->finding))->toBeNull()
->and(IgnoreMatcher::forResource($other)->matches($this->finding))->not->toBeNull();
});
it('does not load an expired ignore', function () {
ImageScanIgnore::factory()->create([
'team_id' => $this->team->id,
'vulnerability_id' => 'CVE-2021-44228',
'expires_at' => now()->subMinute(),
]);
expect(IgnoreMatcher::forResource($this->application)->matches($this->finding))->toBeNull();
});
it('matches nothing for a resource without a team', function () {
ImageScanIgnore::factory()->create(['team_id' => $this->team->id, 'vulnerability_id' => 'CVE-2021-44228']);
$orphan = new Application;
expect(IgnoreMatcher::forResource($orphan)->matches($this->finding))->toBeNull();
});
it('never matches an expired ignore passed in directly', function () {
$expired = new ImageScanIgnore(['vulnerability_id' => 'CVE-2021-44228', 'reason' => 'x', 'expires_at' => now()->subMinute()]);
$active = new ImageScanIgnore(['vulnerability_id' => 'CVE-2021-44228', 'reason' => 'x', 'expires_at' => now()->addDay()]);
expect(IgnoreMatcher::fromIgnores(collect([$expired]))->matches($this->finding))->toBeNull()
->and(IgnoreMatcher::fromIgnores(collect([$expired, $active]))->matches($this->finding))->toBe($active);
});
it('stops a never-ending team-wide ignore of a problem that attackers use after 90 days', function (int $days, bool $ignored) {
$this->travelTo(now()->subDays($days));
ImageScanIgnore::factory()->create(['team_id' => $this->team->id, 'vulnerability_id' => null, 'package' => 'log4j-core', 'expires_at' => null]);
$this->travelBack();
$exploited = Finding::fromArray(['id' => 'CVE-2021-44228', 'package' => 'log4j-core', 'severity' => 'critical', 'kev' => true]);
expect(IgnoreMatcher::forResource($this->application)->matches($exploited) !== null)->toBe($ignored);
})->with([
'89 days' => [89, true],
'91 days' => [91, false],
]);
it('keeps a never-ending ignore of a problem without an exploit signal', function () {
$this->travelTo(now()->subYears(2));
ImageScanIgnore::factory()->create(['team_id' => $this->team->id, 'vulnerability_id' => 'CVE-2021-44228', 'expires_at' => null]);
$this->travelBack();
$plain = Finding::fromArray(['id' => 'CVE-2021-44228', 'package' => 'log4j-core', 'severity' => 'critical', 'kev' => false]);
expect(IgnoreMatcher::forResource($this->application)->matches($plain))->not->toBeNull();
});
it('uses another ignore of the finding when a never-ending one stopped', function () {
$this->travelTo(now()->subDays(100));
ImageScanIgnore::factory()->create(['team_id' => $this->team->id, 'vulnerability_id' => 'CVE-2021-44228', 'expires_at' => null]);
$this->travelBack();
$recent = ImageScanIgnore::factory()->create(['team_id' => $this->team->id, 'vulnerability_id' => null, 'package' => 'log4j-core', 'expires_at' => now()->addDays(30)]);
$exploited = Finding::fromArray(['id' => 'CVE-2021-44228', 'package' => 'log4j-core', 'severity' => 'critical', 'kev' => true]);
expect(IgnoreMatcher::forResource($this->application)->matches($exploited)?->is($recent))->toBeTrue();
});