'array']); $this->withoutVite(); InstanceSettings::forceCreate(['id' => 0, 'is_image_scanning_allowed' => true]); Queue::fake(); $this->team = Team::factory()->create(['show_boarding' => false]); $this->admin = fleetMember($this->team, 'admin'); $this->server = fleetServer($this->team, 'scanning-server'); $this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail(); $this->project = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Shop']); $this->environment = Environment::query()->firstOrCreate(['project_id' => $this->project->id, 'name' => 'production']); }); function fleetMember(Team $team, string $role = 'member', ?array $access = null): User { $user = User::factory()->create(); $user->teams()->attach($team, ['role' => $role, 'access' => $access === null ? null : json_encode($access)]); return $user->fresh(); } function fleetServer(Team $team, string $name, bool $scanning = true): Server { $server = Server::factory()->create(['team_id' => $team->id, 'name' => $name]); $server->settings->forceFill(['is_image_scan_enabled' => $scanning])->saveQuietly(); return $server; } /** * @param array $attributes */ function fleetApplication(string $name, ?Environment $environment = null, ?StandaloneDocker $destination = null, array $attributes = []): Application { $destination ??= test()->destination; return Application::factory()->create([ 'name' => $name, 'environment_id' => ($environment ?? test()->environment)->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'build_pack' => 'dockerimage', 'docker_registry_image_name' => 'nginx', 'docker_registry_image_tag' => '1.27', ...$attributes, ]); } /** * @param array $overrides * @return array */ function fleetFinding(array $overrides = []): array { return [ 'id' => 'CVE-2024-0001', 'aliases' => [], 'severity' => 'high', 'package' => 'openssl', 'version' => '3.0.1', 'type' => 'apk', 'artifactId' => 'a1', 'fixedIn' => '3.0.2', 'fixState' => 'fixed', 'kev' => false, 'epss' => 0.2, 'risk' => 1.0, 'summary' => 'A bug.', 'layer' => 'app', ...$overrides, ]; } /** * A completed scan of a third-party image with the stored security status of `$status` (update_now, update_available, * no_fix, check_needed, ok). An update fixes the findings of update_now and update_available: the same image with the * tag of `update` (default 1.28). * * @param array $attributes Scan attributes, plus `server` and `update`. */ function fleetScan(Model $resource, string $status = 'ok', array $attributes = []): ImageScan { $server = $attributes['server'] ?? test()->server; $imageRef = $attributes['image_ref'] ?? 'nginx:1.27'; $update = str($imageRef)->beforeLast(':').':'.($attributes['update'] ?? '1.28'); unset($attributes['server'], $attributes['update']); $fixes = in_array($status, ['update_now', 'update_available'], true); $scan = ImageScan::factory()->create([ 'server_id' => $server->id, 'resource_type' => $resource->getMorphClass(), 'resource_id' => $resource->getKey(), 'image_ref' => $imageRef, 'builds_image' => false, 'findings' => match ($status) { 'update_now' => [fleetFinding(['kev' => true])], 'update_available' => [fleetFinding(['epss' => null])], 'no_fix' => [fleetFinding(['kev' => true, 'fixedIn' => null, 'fixState' => 'not-fixed'])], default => [], }, 'candidates' => [ 'status' => $fixes ? 'ok' : 'no_better', 'checked_at' => now()->toIso8601String(), 'error' => null, 'tags_truncated' => false, 'entries' => $fixes ? [['kind' => 'line', 'ref' => $update, 'better' => true, 'fixed_ids' => ['CVE-2024-0001'], 'eol' => null]] : [], ], 'summary' => ['truncated' => false, 'eol_distro' => null], 'grype_db_built_at' => $status === 'check_needed' ? now()->subDays(6) : now()->subDay(), 'scanned_at' => now(), ...$attributes, ]); $scan->setRelation('resource', $resource); ResourceScans::refreshSummaries(collect([$scan])); return $scan->fresh(); } /** * Livewire embeds list data with @js(), which writes quotes as \u0022. */ function fleetJsPayload(string $json): string { return str_replace('"', '\u0022', $json); } function fleetPage(User $user, ?Team $team = null, array $query = []): Testable { test()->actingAs($user); session(['currentTeam' => $team ?? test()->team]); return Livewire::withQueryParams($query)->test(FleetVulnerabilities::class); } describe('page', function () { it('opens for a member as the Security page and sorts by status, then name', function () { fleetScan(fleetApplication('b-ok-app'), 'ok'); fleetScan(fleetApplication('a-ok-app'), 'ok'); fleetApplication('never-app'); fleetScan(fleetApplication('check-app'), 'check_needed'); fleetScan(fleetApplication('nofix-app'), 'no_fix'); fleetScan(fleetApplication('available-app'), 'update_available'); fleetScan(fleetApplication('now-app'), 'update_now'); $this->actingAs(fleetMember($this->team)); session(['currentTeam' => $this->team]); $this->get(route('vulnerabilities', ['showNotChecked' => 1])) ->assertOk() ->assertSee('Security | Coolify', false) ->assertSeeInOrder([ 'now-app', 'Update now', 'available-app', 'Security update', 'nofix-app', 'No fix yet', 'check-app', 'Check needed', 'a-ok-app', 'No action needed', 'b-ok-app', 'No action needed', 'never-app', 'Not checked', ]); }); it('hides the resources that were not scanned until the user shows them', function () { fleetScan(fleetApplication('now-app'), 'update_now'); fleetApplication('never-app'); fleetPage($this->admin) ->assertSee('now-app') ->assertDontSee('never-app') ->assertSeeInOrder(['1 resource on 1 server was not scanned, so Coolify cannot tell.', 'Show them']) ->call('toggleNotChecked') ->assertSet('showNotChecked', true) ->assertSeeInOrder(['now-app', 'never-app', 'Not checked', 'Hide them']) ->call('toggleNotChecked') ->assertDontSee('never-app') ->call('toggleStatus', 'not_checked') ->assertSee('never-app') ->assertDontSee('now-app') ->assertDontSee('Show them'); }); it('says that no fix is released yet in the Action column of a no fix resource', function () { fleetScan(fleetApplication('nofix-app'), 'no_fix'); fleetPage($this->admin)->assertSeeInOrder(['nofix-app', 'No fix yet', TeamVulnerabilities::NO_FIX_ACTION]); }); it('shows the primary action as a link to the Security card of the resource, without buttons', function () { $now = fleetApplication('now-app'); fleetScan($now, 'update_now'); $noFix = fleetApplication('nofix-app'); fleetScan($noFix, 'no_fix'); $page = fleetPage($this->admin) ->assertSee('Update to 1.28.') ->assertSeeHtml('href="'.TeamVulnerabilities::vulnerabilitiesUrl($now).'#security-section"') ->assertDontSeeHtml('