feat(image-scan): manage image scanning and read scan results via API and MCP

This commit is contained in:
Andras Bacsai
2026-11-06 12:41:09 +01:00
parent 7bab8f76ad
commit 1703265353
18 changed files with 2232 additions and 46 deletions
@@ -0,0 +1,55 @@
<?php
namespace App\Actions\ImageScan;
use App\Jobs\PrepareImageScannerJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Services\ImageScan\ImageScanningRefused;
use Illuminate\Support\Facades\Cache;
use Lorisleiva\Actions\Concerns\AsAction;
/**
* Turns image scanning of a server on or off. Turning it on gets the scanner on the server.
*/
class SetServerImageScanning
{
use AsAction;
/**
* @throws ImageScanningRefused When the instance does not allow scanning or another instance manages the server.
*/
public function handle(Server $server, bool $enabled): void
{
if (! $enabled) {
$server->settings->update(['is_image_scan_enabled' => false]);
PrepareImageScannerJob::forget($server);
return;
}
self::ensureManagedHere($server);
if (! InstanceSettings::get()->is_image_scanning_allowed) {
throw new ImageScanningRefused('Image scanning is turned off for this Coolify instance.');
}
$server->settings->update(['is_image_scan_enabled' => true]);
self::prepareScanner($server);
}
/**
* Gets the scanner on the server again, for a server that scans images.
*/
public static function prepareScanner(Server $server): void
{
Cache::put(PrepareImageScannerJob::stateCacheKey($server), PrepareImageScannerJob::STATE_PULLING, now()->addHour());
PrepareImageScannerJob::dispatch($server);
}
public static function ensureManagedHere(Server $server): void
{
if ($server->isTransferredAway()) {
throw new ImageScanningRefused('This server is managed by another Coolify instance.');
}
}
}
@@ -80,6 +80,7 @@ class ApplicationsController extends Controller
'is_consistent_container_name_enabled',
'custom_internal_name',
'custom_container_name_prefix',
'is_image_scan_enabled',
];
private const BOOLEAN_APPLICATION_SETTING_FIELDS = [
@@ -104,6 +105,7 @@ class ApplicationsController extends Controller
'is_log_drain_enabled',
'is_gpu_enabled',
'is_consistent_container_name_enabled',
'is_image_scan_enabled',
];
protected function findTaggableResource(string $uuid, int|string $teamId): mixed
@@ -498,6 +500,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -711,6 +714,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -924,6 +928,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -1137,6 +1142,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -1320,6 +1326,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -1499,6 +1506,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
@@ -3536,6 +3544,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'is_image_scan_enabled' => ['type' => 'boolean', 'description' => 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
+73
View File
@@ -132,6 +132,79 @@ use OpenApi\Attributes as OA;
new OA\Property(property: 'deleted_at', type: 'string', format: 'date-time', nullable: true),
],
),
new OA\Schema(
schema: 'SecurityStatus',
description: 'Security status of a resource from image scanning.',
properties: [
new OA\Property(property: 'status', type: 'string', enum: ['update_now', 'update_available', 'no_fix', 'check_needed', 'ok', 'not_checked']),
new OA\Property(property: 'label', type: 'string'),
new OA\Property(property: 'action', type: 'string', nullable: true, description: 'What to do, as the UI shows it.'),
new OA\Property(property: 'check_reason', type: 'string', nullable: true),
new OA\Property(property: 'problem_count', type: 'integer'),
new OA\Property(property: 'exploited_count', type: 'integer'),
new OA\Property(property: 'any_exploited', type: 'boolean'),
new OA\Property(property: 'critical_count', type: 'integer'),
new OA\Property(property: 'high_count', type: 'integer'),
new OA\Property(property: 'medium_count', type: 'integer'),
new OA\Property(property: 'eol', type: 'object', nullable: true, description: 'The end-of-life OS of the image.'),
new OA\Property(property: 'checked_at', type: 'string', format: 'date-time', nullable: true),
new OA\Property(property: 'actions', type: 'array', items: new OA\Items(properties: [
new OA\Property(property: 'type', type: 'string'),
new OA\Property(property: 'sentence', type: 'string'),
new OA\Property(property: 'target', type: 'string', nullable: true),
new OA\Property(property: 'copy', type: 'string', nullable: true),
new OA\Property(property: 'image', type: 'string', nullable: true),
new OA\Property(property: 'ref', type: 'string', nullable: true),
], type: 'object')),
],
type: 'object',
),
new OA\Schema(
schema: 'ResourceVulnerabilities',
description: 'Image scan results of a resource.',
properties: [
new OA\Property(property: 'uuid', type: 'string'),
new OA\Property(property: 'type', type: 'string', enum: ['application', 'service', 'database']),
new OA\Property(property: 'off_reason', type: 'string', nullable: true, enum: ['instance', 'server', 'application'], description: 'Why the resource is not scanned. Null when it is scanned.'),
new OA\Property(property: 'security', ref: '#/components/schemas/SecurityStatus', nullable: true),
new OA\Property(property: 'images', type: 'array', items: new OA\Items(properties: [
new OA\Property(property: 'scan_uuid', type: 'string'),
new OA\Property(property: 'owner_uuid', type: 'string', description: 'The application, database, or service application or database that runs the image.'),
new OA\Property(property: 'image_ref', type: 'string'),
new OA\Property(property: 'scan_status', type: 'string', enum: ['queued', 'running', 'completed', 'failed']),
new OA\Property(property: 'error_code', type: 'string', nullable: true),
new OA\Property(property: 'scanned_at', type: 'string', format: 'date-time', nullable: true),
new OA\Property(property: 'truncated', type: 'boolean'),
new OA\Property(property: 'security_status', type: 'string'),
], type: 'object')),
new OA\Property(property: 'findings', type: 'array', items: new OA\Items(properties: [
new OA\Property(property: 'id', type: 'string'),
new OA\Property(property: 'display_id', type: 'string'),
new OA\Property(property: 'aliases', type: 'array', items: new OA\Items(type: 'string')),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'severity', type: 'string', enum: ['critical', 'high', 'medium', 'low', 'unknown']),
new OA\Property(property: 'package', type: 'string'),
new OA\Property(property: 'version', type: 'string'),
new OA\Property(property: 'package_type', type: 'string'),
new OA\Property(property: 'fixed_in', type: 'string', nullable: true),
new OA\Property(property: 'state', type: 'string', enum: ['fixed', 'no_fix', 'ignored'], description: 'fixed: a fix is available upstream.'),
new OA\Property(property: 'ignore', type: 'object', nullable: true),
new OA\Property(property: 'kev', type: 'boolean', description: 'Listed in the CISA Known Exploited Vulnerabilities catalog.'),
new OA\Property(property: 'epss', type: 'number', nullable: true),
new OA\Property(property: 'risk', type: 'number', nullable: true),
new OA\Property(property: 'cvss_score', type: 'number', nullable: true),
new OA\Property(property: 'advisory_url', type: 'string', nullable: true),
new OA\Property(property: 'image_ref', type: 'string'),
new OA\Property(property: 'owner_uuid', type: 'string'),
], type: 'object')),
new OA\Property(property: 'pagination', properties: [
new OA\Property(property: 'page', type: 'integer'),
new OA\Property(property: 'per_page', type: 'integer'),
new OA\Property(property: 'total', type: 'integer'),
], type: 'object'),
],
type: 'object',
),
],
responses: [
new OA\Response(
@@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\ImageScan\SetServerImageScanning;
use App\Http\Controllers\Controller;
use App\Jobs\PrepareImageScannerJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Services\ImageScan\ImageScanningRefused;
use Carbon\CarbonImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use OpenApi\Attributes as OA;
class ServerImageScanningController extends Controller
{
private const ALLOWED_FIELDS = ['is_image_scan_enabled'];
/**
* @return array{is_image_scanning_allowed: bool, is_image_scan_enabled: bool, scanner_state: ?string, scanner_failed_step: ?string, scanner_built_at: ?string, scanner_disk_used_bytes: ?int}
*/
private function transform(Server $server): array
{
$state = Cache::get(PrepareImageScannerJob::stateCacheKey($server));
$failedStep = Cache::get(PrepareImageScannerJob::failedStepCacheKey($server));
$builtAt = Cache::get(PrepareImageScannerJob::builtAtCacheKey($server));
$diskUsed = Cache::get(PrepareImageScannerJob::diskUsedCacheKey($server));
return [
'is_image_scanning_allowed' => (bool) InstanceSettings::get()->is_image_scanning_allowed,
'is_image_scan_enabled' => (bool) $server->settings->is_image_scan_enabled,
'scanner_state' => is_string($state) ? $state : null,
'scanner_failed_step' => is_string($failedStep) ? $failedStep : null,
'scanner_built_at' => is_string($builtAt) ? CarbonImmutable::parse($builtAt)->toIso8601String() : null,
'scanner_disk_used_bytes' => is_int($diskUsed) ? $diskUsed : null,
];
}
private function findServer(Request $request): ?Server
{
$teamId = getTeamIdFromToken();
return is_null($teamId) ? null : Server::whereTeamId($teamId)->whereUuid($request->route('uuid'))->first();
}
#[OA\Get(
summary: 'Get image scanning settings',
description: 'Get the image vulnerability scanning settings of a server owned by the authenticated team, and the state of the scanner on the server. Needs a team admin or owner.',
path: '/servers/{uuid}/image-scanning',
operationId: 'get-server-image-scanning',
security: [['bearerAuth' => []]],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Image scanning settings.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'is_image_scanning_allowed', type: 'boolean', readOnly: true, description: 'Whether the Coolify instance allows image scanning.'),
new OA\Property(property: 'is_image_scan_enabled', type: 'boolean', description: 'Whether the server scans the images of its resources.'),
new OA\Property(property: 'scanner_state', type: 'string', nullable: true, enum: ['pulling', 'updating_database', 'ready', 'failed'], description: 'State of the scanner on the server. Null when unknown.'),
new OA\Property(property: 'scanner_failed_step', type: 'string', nullable: true, description: 'The step that failed when scanner_state is failed.'),
new OA\Property(property: 'scanner_built_at', type: 'string', format: 'date-time', nullable: true, description: 'Time of the vulnerability database on the server.'),
new OA\Property(property: 'scanner_disk_used_bytes', type: 'integer', nullable: true, description: 'Disk space that the scanner uses on the server.'),
],
type: 'object',
),
),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 403, description: 'Needs a team admin or owner.'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
],
)]
public function show(Request $request): JsonResponse
{
if (is_null(getTeamIdFromToken())) {
return invalidTokenResponse();
}
$server = $this->findServer($request);
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('viewSecurity', $server);
return response()->json($this->transform($server));
}
#[OA\Patch(
summary: 'Update image scanning settings',
description: 'Turn image vulnerability scanning of a server owned by the authenticated team on or off. Turning it on gets the scanner on the server in the background.',
path: '/servers/{uuid}/image-scanning',
operationId: 'update-server-image-scanning',
security: [['bearerAuth' => []]],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['is_image_scan_enabled'],
properties: [
new OA\Property(property: 'is_image_scan_enabled', type: 'boolean'),
],
type: 'object',
),
),
responses: [
new OA\Response(response: 200, description: 'Updated image scanning settings.'),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 403, description: 'Needs a team admin or owner.'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
new OA\Response(response: 409, description: 'The instance does not allow image scanning, or another Coolify instance manages the server.'),
new OA\Response(response: 422, ref: '#/components/responses/422'),
],
)]
public function update(Request $request): JsonResponse
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
}
$server = $this->findServer($request);
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$this->authorize('update', $server);
$validator = customApiValidator($request->all(), [
'is_image_scan_enabled' => 'required|boolean',
]);
$extraFields = array_diff(array_keys($request->all()), self::ALLOWED_FIELDS);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$enabled = $request->boolean('is_image_scan_enabled');
try {
SetServerImageScanning::run($server, $enabled);
} catch (ImageScanningRefused $e) {
return response()->json(['message' => $e->getMessage()], 409);
}
auditLog($enabled ? 'api.server.image_scanning_enabled' : 'api.server.image_scanning_disabled', [
'team_id' => $teamId,
'server_uuid' => $server->uuid,
'server_name' => $server->name,
]);
return response()->json($this->transform($server->refresh()));
}
}
+8 -3
View File
@@ -238,12 +238,16 @@ class TeamController extends Controller
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['is_build_server_fallback_enabled'],
minProperties: 1,
properties: [
'is_build_server_fallback_enabled' => [
'type' => 'boolean',
'description' => 'Whether deployments can fall back to the deployment server when no usable dedicated build server is available.',
],
'scan_images_on_new_servers' => [
'type' => 'boolean',
'description' => 'Whether image scanning is turned on for servers added to the team from now on.',
],
],
),
),
@@ -269,9 +273,10 @@ class TeamController extends Controller
$this->authorize('update', $team);
$allowedFields = ['is_build_server_fallback_enabled'];
$allowedFields = ['is_build_server_fallback_enabled', 'scan_images_on_new_servers'];
$validator = customApiValidator($request->all(), [
'is_build_server_fallback_enabled' => ['required', 'boolean'],
'is_build_server_fallback_enabled' => ['required_without:scan_images_on_new_servers', 'boolean'],
'scan_images_on_new_servers' => ['required_without:is_build_server_fallback_enabled', 'boolean'],
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -0,0 +1,398 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\ImageScan;
use App\Models\ImageScanIgnore;
use App\Models\Service;
use App\Services\ImageScan\Finding;
use App\Services\ImageScan\ImageScanPolicyResolver;
use App\Services\ImageScan\RelevanceRules;
use App\Services\ImageScan\ResourceScans;
use App\Services\ImageScan\ResourceSecurity;
use App\Services\ImageScan\SecurityAction;
use App\Services\ImageScan\SecurityActions;
use App\Services\ImageScan\SecurityStatus;
use App\Services\ImageScan\TeamVulnerabilities;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use OpenApi\Attributes as OA;
/**
* Image scan results, with the same data and permissions as the Security pages of the UI.
*/
#[OA\Parameter(parameter: 'VulnerabilityState', name: 'state', in: 'query', required: false, description: 'Comma-separated finding states: fixed, no_fix, ignored. All when not sent.', schema: new OA\Schema(type: 'string'))]
#[OA\Parameter(parameter: 'VulnerabilitySeverity', name: 'severity', in: 'query', required: false, description: 'Comma-separated severities: critical, high, medium, low, unknown.', schema: new OA\Schema(type: 'string'))]
#[OA\Parameter(parameter: 'VulnerabilityImage', name: 'image', in: 'query', required: false, description: 'Only the findings of this scanned image ref.', schema: new OA\Schema(type: 'string'))]
#[OA\Parameter(parameter: 'VulnerabilitySearch', name: 'search', in: 'query', required: false, description: 'Text in the vulnerability ID, an alias, the package, or the version.', schema: new OA\Schema(type: 'string'))]
#[OA\Parameter(parameter: 'VulnerabilityPage', name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1, minimum: 1))]
#[OA\Parameter(parameter: 'VulnerabilityPerPage', name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 50, maximum: 200, minimum: 1))]
class VulnerabilitiesController extends Controller
{
private const SEVERITY_RANK = ['critical' => 4, 'high' => 3, 'medium' => 2, 'low' => 1, 'unknown' => 0];
private const FINDING_STATES = ['fixed', 'no_fix', 'ignored'];
#[OA\Get(
summary: 'List vulnerabilities',
description: 'List the applications, services, and databases of the authenticated team with their stored security status from image scanning.',
path: '/vulnerabilities',
operationId: 'list-vulnerabilities',
security: [['bearerAuth' => []]],
tags: ['Vulnerabilities'],
parameters: [
new OA\Parameter(name: 'status', in: 'query', required: false, description: 'Comma-separated statuses to return.', schema: new OA\Schema(type: 'string', example: 'update_now,update_available')),
],
responses: [
new OA\Response(
response: 200,
description: 'Resources with their security status, worst first.',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'is_image_scanning_allowed', type: 'boolean'),
new OA\Property(property: 'resources', type: 'array', items: new OA\Items(
properties: [
new OA\Property(property: 'uuid', type: 'string'),
new OA\Property(property: 'name', type: 'string'),
new OA\Property(property: 'type', type: 'string', enum: ['application', 'service', 'database']),
new OA\Property(property: 'project_uuid', type: 'string', nullable: true),
new OA\Property(property: 'environment_uuid', type: 'string', nullable: true),
new OA\Property(property: 'server_uuid', type: 'string', nullable: true),
new OA\Property(property: 'images', properties: [
new OA\Property(property: 'update', type: 'integer'),
new OA\Property(property: 'total', type: 'integer'),
], type: 'object'),
new OA\Property(property: 'security', ref: '#/components/schemas/SecurityStatus'),
],
type: 'object',
)),
],
type: 'object',
),
),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
],
)]
public function index(Request $request): JsonResponse
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$statuses = $this->listParameter($request, 'status', TeamVulnerabilities::STATUS_ORDER);
if ($statuses instanceof JsonResponse) {
return $statuses;
}
$rank = array_flip(TeamVulnerabilities::STATUS_ORDER);
$resources = TeamVulnerabilities::load((int) $teamId)->entries
->filter(fn (array $entry): bool => $statuses === [] || in_array($entry['security']->status->value, $statuses, true))
->sortBy(fn (array $entry): array => [$rank[$entry['security']->status->value] ?? PHP_INT_MAX, (string) $entry['resource']->getAttribute('name')])
->map(fn (array $entry): array => [
'uuid' => (string) $entry['resource']->getAttribute('uuid'),
'name' => (string) $entry['resource']->getAttribute('name'),
'type' => $this->resourceType($entry['resource']),
'project_uuid' => $entry['project']?->uuid,
'environment_uuid' => $entry['environment']?->uuid,
'server_uuid' => $entry['server']?->uuid,
'images' => $entry['images'],
'security' => $this->security($entry['security']),
])
->values();
return response()->json([
'is_image_scanning_allowed' => (bool) instanceSettings()->is_image_scanning_allowed,
'resources' => $resources,
]);
}
#[OA\Get(
summary: 'Get application vulnerabilities',
description: 'Get the security status, the scanned images, and the vulnerabilities of an application.',
path: '/applications/{uuid}/vulnerabilities',
operationId: 'get-application-vulnerabilities',
security: [['bearerAuth' => []]],
tags: ['Vulnerabilities'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Application UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityState'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySeverity'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityImage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySearch'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPerPage'),
],
responses: [
new OA\Response(response: 200, description: 'Scan results.', content: new OA\JsonContent(ref: '#/components/schemas/ResourceVulnerabilities')),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
new OA\Response(response: 422, ref: '#/components/responses/422'),
],
)]
public function application(Request $request): JsonResponse
{
return $this->show($request, 'application');
}
#[OA\Get(
summary: 'Get service vulnerabilities',
description: 'Get the security status, the scanned images, and the vulnerabilities of the applications and databases of a service.',
path: '/services/{uuid}/vulnerabilities',
operationId: 'get-service-vulnerabilities',
security: [['bearerAuth' => []]],
tags: ['Vulnerabilities'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityState'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySeverity'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityImage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySearch'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPerPage'),
],
responses: [
new OA\Response(response: 200, description: 'Scan results.', content: new OA\JsonContent(ref: '#/components/schemas/ResourceVulnerabilities')),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
new OA\Response(response: 422, ref: '#/components/responses/422'),
],
)]
public function service(Request $request): JsonResponse
{
return $this->show($request, 'service');
}
#[OA\Get(
summary: 'Get database vulnerabilities',
description: 'Get the security status, the scanned image, and the vulnerabilities of a standalone database.',
path: '/databases/{uuid}/vulnerabilities',
operationId: 'get-database-vulnerabilities',
security: [['bearerAuth' => []]],
tags: ['Vulnerabilities'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Database UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityState'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySeverity'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityImage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilitySearch'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPage'),
new OA\Parameter(ref: '#/components/parameters/VulnerabilityPerPage'),
],
responses: [
new OA\Response(response: 200, description: 'Scan results.', content: new OA\JsonContent(ref: '#/components/schemas/ResourceVulnerabilities')),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
new OA\Response(response: 422, ref: '#/components/responses/422'),
],
)]
public function database(Request $request): JsonResponse
{
return $this->show($request, 'database');
}
private function show(Request $request, string $type): JsonResponse
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$validator = customApiValidator($request->query(), [
'state' => 'nullable|string',
'severity' => 'nullable|string',
'image' => 'nullable|string|max:1000',
'search' => 'nullable|string|max:200',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:200',
]);
if ($validator->fails()) {
return response()->json(['message' => 'Validation failed.', 'errors' => $validator->errors()], 422);
}
$states = $this->listParameter($request, 'state', self::FINDING_STATES);
$severities = $this->listParameter($request, 'severity', array_keys(self::SEVERITY_RANK));
foreach ([$states, $severities] as $parameter) {
if ($parameter instanceof JsonResponse) {
return $parameter;
}
}
$resource = ResourceScans::find($type, (string) $request->route('uuid'), [(int) $teamId]);
if (! $resource instanceof Model) {
return response()->json(['message' => ucfirst($type).' not found.'], 404);
}
$this->authorize('view', $resource);
$offState = ImageScanPolicyResolver::offState($resource, ImageScanPolicyResolver::for($resource)->scanServer);
$scans = $offState === null ? ResourceScans::current($resource) : collect();
$rules = [];
$rulesFor = function (Model $owner) use (&$rules): RelevanceRules {
return $rules[$owner->getMorphClass().':'.$owner->getKey()] ??= RelevanceRules::forResource($owner);
};
$imageStatuses = $scans->mapWithKeys(fn (ImageScan $scan): array => [(string) $scan->uuid => SecurityStatus::forScan($scan, $rulesFor($scan->resource))])->all();
$findings = $this->findings($scans, $rulesFor);
$image = trim((string) $request->query('image', ''));
$search = mb_strtolower(trim((string) $request->query('search', '')));
$findings = array_values(array_filter($findings, fn (array $row): bool => ($states === [] || in_array($row['state'], $states, true))
&& ($severities === [] || in_array($row['severity'], $severities, true))
&& ($image === '' || $row['image_ref'] === $image)
&& ($search === '' || str_contains(mb_strtolower(implode(' ', [$row['id'], ...$row['aliases'], $row['package'], $row['version']])), $search))));
$perPage = (int) $request->query('per_page', 50);
$page = (int) $request->query('page', 1);
return response()->json([
'uuid' => (string) $resource->getAttribute('uuid'),
'type' => $type,
'off_reason' => $offState,
'security' => $scans->isEmpty() ? null : $this->security(SecurityStatus::forResource($resource, $scans, $imageStatuses)),
'images' => $scans->map(fn (ImageScan $scan): array => [
'scan_uuid' => (string) $scan->uuid,
'owner_uuid' => (string) $scan->resource?->getAttribute('uuid'),
'image_ref' => (string) $scan->image_ref,
'scan_status' => $scan->status?->value,
'error_code' => $scan->error_code?->value,
'scanned_at' => $scan->scanned_at?->toIso8601String(),
'truncated' => (bool) data_get($scan->summary, 'truncated', false),
'security_status' => $imageStatuses[(string) $scan->uuid]->status->value,
])->values(),
'findings' => array_slice($findings, ($page - 1) * $perPage, $perPage),
'pagination' => ['page' => $page, 'per_page' => $perPage, 'total' => count($findings)],
]);
}
/**
* The findings of the current scans, worst first, in the same fix states as the UI.
*
* @param Collection<int, ImageScan> $scans
* @param Closure(Model): RelevanceRules $rulesFor
* @return list<array<string, mixed>>
*/
private function findings(Collection $scans, Closure $rulesFor): array
{
$rows = [];
foreach ($scans as $scan) {
$owner = $scan->resource;
$rules = $rulesFor($owner);
$classify = SecurityActions::fixStates($scan, $rules);
foreach (ResourceScans::findings($scan) as $finding) {
$ignore = $rules->ignores->matches($finding);
$rows[] = $this->finding($finding, $ignore === null ? $classify($finding)->value : 'ignored', $ignore) + [
'image_ref' => (string) $scan->image_ref,
'owner_uuid' => (string) $owner->getAttribute('uuid'),
];
}
}
usort($rows, fn (array $a, array $b): int => [$b['kev'], self::SEVERITY_RANK[$b['severity']], $b['risk'] ?? 0.0]
<=> [$a['kev'], self::SEVERITY_RANK[$a['severity']], $a['risk'] ?? 0.0]);
return $rows;
}
/**
* @return array<string, mixed>
*/
private function finding(Finding $finding, string $state, ?ImageScanIgnore $ignore): array
{
return [
'id' => $finding->id,
'display_id' => $finding->displayId,
'aliases' => $finding->aliases,
'title' => $finding->title,
'severity' => TeamVulnerabilities::severityOf($finding),
'package' => $finding->package,
'version' => $finding->version,
'package_type' => $finding->type,
'fixed_in' => $finding->fixedIn,
'state' => $state,
'ignore' => $ignore === null ? null : [
'reason' => $ignore->reason,
'expires_at' => $ignore->expires_at?->toIso8601String(),
'team_wide' => $ignore->resource_type === null,
],
'kev' => $finding->kev,
'epss' => $finding->epss,
'risk' => $finding->risk,
'cvss_score' => $finding->cvssScore,
'advisory_url' => ResourceScans::advisoryUrl($finding->displayId),
];
}
/**
* The security status without the internal facts and record IDs.
*
* @return array<string, mixed>
*/
private function security(ResourceSecurity $security): array
{
return [
'status' => $security->status->value,
'label' => TeamVulnerabilities::statusLabel($security->status),
'action' => TeamVulnerabilities::action($security)['text'] ?? null,
'check_reason' => $security->checkReason,
'problem_count' => $security->problemCount,
'exploited_count' => $security->exploitedCount,
'any_exploited' => $security->anyExploited,
'critical_count' => $security->criticalCount,
'high_count' => $security->highCount,
'medium_count' => $security->mediumCount,
'eol' => $security->eol,
'checked_at' => $security->checkedAt?->toIso8601String(),
'actions' => array_map(fn (SecurityAction $action): array => [
'type' => $action->type->value,
'sentence' => $action->sentence,
'target' => $action->target,
'copy' => $action->copy,
'image' => $action->image,
'ref' => $action->ref,
], $security->actions),
];
}
private function resourceType(Model $resource): string
{
return match (true) {
$resource instanceof Application => 'application',
$resource instanceof Service => 'service',
default => 'database',
};
}
/**
* A comma-separated query parameter, checked against the allowed values. Empty when not sent.
*
* @param list<string> $allowed
* @return list<string>|JsonResponse
*/
private function listParameter(Request $request, string $name, array $allowed): array|JsonResponse
{
$raw = $request->query($name);
if ($raw === null || $raw === '') {
return [];
}
if (! is_string($raw)) {
return response()->json(['message' => 'Validation failed.', 'errors' => [$name => ["The {$name} must be a comma-separated string."]]], 422);
}
$values = array_values(array_unique(array_filter(array_map('trim', explode(',', strtolower($raw))))));
$unknown = array_diff($values, $allowed);
if ($unknown !== []) {
return response()->json(['message' => 'Validation failed.', 'errors' => [$name => ['Allowed values: '.implode(', ', $allowed).'.']]], 422);
}
return $values;
}
}
+1
View File
@@ -22,6 +22,7 @@ class ApiAbility extends CheckForAnyAbility
'deployments',
'resources',
'version',
'vulnerabilities',
];
public const SCOPED_ACCESS_DENIED_MESSAGE = 'This API token is limited to selected projects, environments, or resources, so it cannot use this endpoint.';
+10 -37
View File
@@ -2,10 +2,12 @@
namespace App\Livewire\ImageScan;
use App\Actions\ImageScan\SetServerImageScanning;
use App\Jobs\PrepareImageScannerJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Services\ImageScan\ImageRef;
use App\Services\ImageScan\ImageScanningRefused;
use App\Services\ImageScan\ScannerCommands;
use App\Traits\ListensToTeamChannel;
use Carbon\CarbonImmutable;
@@ -47,19 +49,11 @@ class ServerImageScanning extends Component
$this->authorize('update', $this->server);
try {
$this->ensureManagedHere();
if (! InstanceSettings::get()->is_image_scanning_allowed) {
$this->dispatch('error', message: 'Image scanning is turned off for this Coolify instance.');
return;
}
$settings = $this->server->settings;
$settings->is_image_scan_enabled = true;
$settings->save();
SetServerImageScanning::run($this->server, true);
$this->auditImageScanning('image_scanning_enabled');
$this->startPreparing();
$this->dispatch('success', 'Image scanning turned on. Coolify is getting the scanner.');
} catch (ImageScanningRefused $e) {
$this->dispatch('error', message: $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -70,14 +64,14 @@ class ServerImageScanning extends Component
$this->authorize('update', $this->server);
try {
$this->ensureManagedHere();
if (! InstanceSettings::get()->is_image_scanning_allowed || ! $this->isEnabled()) {
SetServerImageScanning::ensureManagedHere($this->server);
if (! InstanceSettings::get()->is_image_scanning_allowed || ! $this->server->settings->refresh()->is_image_scan_enabled) {
$this->dispatch('error', message: 'Turn on image scanning first.');
return;
}
$this->startPreparing();
SetServerImageScanning::prepareScanner($this->server);
$this->dispatch('success', 'Coolify is getting the scanner.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -89,10 +83,7 @@ class ServerImageScanning extends Component
$this->authorize('update', $this->server);
try {
$settings = $this->server->settings;
$settings->is_image_scan_enabled = false;
$settings->save();
PrepareImageScannerJob::forget($this->server);
SetServerImageScanning::run($this->server, false);
$this->auditImageScanning('image_scanning_disabled');
$this->dispatch('success', 'Image scanning turned off.');
} catch (\Throwable $e) {
@@ -105,7 +96,7 @@ class ServerImageScanning extends Component
$this->authorize('update', $this->server);
try {
$this->ensureManagedHere();
SetServerImageScanning::ensureManagedHere($this->server);
$commands = new ScannerCommands($this->server, Str::lower(Str::random(24)));
instant_remote_process($commands->deleteScannerData(), $this->server, throwError: true, timeout: 120);
PrepareImageScannerJob::forget($this->server);
@@ -116,24 +107,6 @@ class ServerImageScanning extends Component
}
}
private function startPreparing(): void
{
Cache::put(PrepareImageScannerJob::stateCacheKey($this->server), PrepareImageScannerJob::STATE_PULLING, now()->addHour());
PrepareImageScannerJob::dispatch($this->server);
}
private function isEnabled(): bool
{
return (bool) $this->server->settings->refresh()->is_image_scan_enabled;
}
private function ensureManagedHere(): void
{
if ($this->server->isTransferredAway()) {
throw new \RuntimeException('This server is managed by another Coolify instance.');
}
}
private function auditImageScanning(string $action): void
{
auditLog("ui.server.{$action}", [
+6
View File
@@ -34,6 +34,7 @@ use App\Mcp\Tools\GetServerResources;
use App\Mcp\Tools\GetService;
use App\Mcp\Tools\GetServiceApplication;
use App\Mcp\Tools\GetServiceDatabase;
use App\Mcp\Tools\GetVulnerabilities;
use App\Mcp\Tools\ListApplicationPreviews;
use App\Mcp\Tools\ListApplications;
use App\Mcp\Tools\ListAuditEvents;
@@ -70,6 +71,7 @@ use App\Mcp\Tools\ManageDestination;
use App\Mcp\Tools\ManageDns;
use App\Mcp\Tools\ManageEnv;
use App\Mcp\Tools\ManageGitSource;
use App\Mcp\Tools\ManageImageScanning;
use App\Mcp\Tools\ManageIntegrationTokens;
use App\Mcp\Tools\ManageNotifications;
use App\Mcp\Tools\ManagePreview;
@@ -118,6 +120,7 @@ Then: list_*/get_* for details. Debug is DB-first:
- list_deployments → get_deployment(include_log_summary=true)
- list_env_keys / list_shared_env_keys (names only, never values)
- get_logs only if status is running; on failure use reason + next_tools (do not loop)
- get_vulnerabilities — image vulnerability scan results (list for the team, resource for one app/service/database); manage_image_scanning turns scanning on or off
Lifecycle (requires token ability **deploy**):
- control (start|stop|restart; stop needs confirm=true)
@@ -218,6 +221,8 @@ MD;
ManageNotifications::class,
ListAuditEvents::class,
GetServerMetrics::class,
GetVulnerabilities::class,
ManageImageScanning::class,
];
/**
@@ -244,6 +249,7 @@ MD;
ManageTags::class,
ManageServer::class,
ManageProxy::class,
ManageImageScanning::class,
ManageCloudflareTunnel::class,
DockerCleanup::class,
ManageDestination::class,
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Mcp\Tools;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
#[IsReadOnly]
class GetVulnerabilities extends RestActionTool
{
protected string $name = 'get_vulnerabilities';
protected string $description = 'Read image vulnerability scan results of the authenticated team: the security status of every resource, or the status, scanned images, and vulnerabilities of one application, service, or database. Requires read ability. Same data and permissions as the REST API.';
protected function actions(): array
{
return [
'list' => ['method' => 'GET', 'path' => 'vulnerabilities', 'ability' => 'read', 'description' => 'every resource with its security status, worst first (data: status, comma-separated: update_now, update_available, no_fix, check_needed, ok, not_checked)'],
'resource' => ['method' => 'GET', 'paths' => [
'application' => 'applications/{uuid}/vulnerabilities',
'service' => 'services/{uuid}/vulnerabilities',
'database' => 'databases/{uuid}/vulnerabilities',
], 'ability' => 'read', 'description' => 'status, scanned images, and vulnerabilities of one resource, worst first (data: state fixed|no_fix|ignored, severity, image, search, page, per_page max 200; state and severity are comma-separated)'],
];
}
protected function pathParams(): array
{
return [
'uuid' => 'Resource UUID (resource action).',
];
}
protected function dataDescription(): string
{
return 'Filters, sent as query parameters: status (list); state, severity, image, search, page, per_page (resource).';
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ class ManageApplication extends RestActionTool
protected function actions(): array
{
return [
'update' => ['method' => 'PATCH', 'path' => 'applications/{uuid}', 'ability' => 'write', 'description' => 'update application settings (data: same fields as the REST update, e.g. name, domains, git_branch, ports_exposes, build_command, is_container_label_readonly_enabled, include_coolify_default_variables; instant_deploy and manual_webhook_secret_* need deploy ability)'],
'update' => ['method' => 'PATCH', 'path' => 'applications/{uuid}', 'ability' => 'write', 'description' => 'update application settings (data: same fields as the REST update, e.g. name, domains, git_branch, ports_exposes, build_command, is_container_label_readonly_enabled, include_coolify_default_variables, is_image_scan_enabled; instant_deploy and manual_webhook_secret_* need deploy ability)'],
'delete' => ['method' => 'DELETE', 'path' => 'applications/{uuid}', 'ability' => 'delete', 'confirm' => true, 'description' => 'delete the application and its containers (data: delete_volumes, delete_connected_networks, delete_configurations, docker_cleanup)'],
'clone' => ['method' => 'POST', 'path' => 'applications/{uuid}/clone', 'ability' => 'write', 'description' => 'clone the application to a destination of the team (data: destination_uuid, name, clone_volumes; clone_volumes needs deploy ability)'],
'move' => ['method' => 'POST', 'path' => 'applications/{uuid}/move', 'ability' => 'write', 'confirm' => true, 'description' => 'move the application to another environment of the team (data: environment_uuid)'],
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Mcp\Tools;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
#[IsReadOnly(false)]
class ManageImageScanning extends RestActionTool
{
protected string $name = 'manage_image_scanning';
protected string $description = 'Read or change image vulnerability scanning of a server of the authenticated team, or the team default for new servers. Requires read ability for get_server, write ability for update_server and update_team. Needs a team admin or owner. Turn scanning of one application on or off with manage_application update (is_image_scan_enabled). Read the results with get_vulnerabilities. Same validation and permissions as the REST API.';
protected function actions(): array
{
return [
'get_server' => ['method' => 'GET', 'path' => 'servers/{uuid}/image-scanning', 'ability' => 'read', 'description' => 'whether the instance allows scanning, whether the server scans, and the scanner state on the server'],
'update_server' => ['method' => 'PATCH', 'path' => 'servers/{uuid}/image-scanning', 'ability' => 'write', 'description' => 'turn scanning of the server on or off (data: is_image_scan_enabled); turning it on gets the scanner on the server'],
'update_team' => ['method' => 'PATCH', 'path' => 'team', 'ability' => 'write', 'description' => 'turn scanning on or off for servers added to the team from now on (data: scan_images_on_new_servers)'],
];
}
protected function pathParams(): array
{
return [
'uuid' => 'Server UUID (get_server and update_server).',
];
}
protected function dataDescription(): string
{
return 'Request body: is_image_scan_enabled (update_server), scan_images_on_new_servers (update_team).';
}
}
+1
View File
@@ -36,6 +36,7 @@ use OpenApi\Attributes as OA;
'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
'is_build_server_fallback_enabled' => ['type' => 'boolean', 'description' => 'Whether deployments can fall back to the deployment server when no usable dedicated build server is available.'],
'scan_images_on_new_servers' => ['type' => 'boolean', 'description' => 'Whether image scanning is turned on for servers added to the team from now on.'],
'members' => new OA\Property(
property: 'members',
type: 'array',
@@ -0,0 +1,10 @@
<?php
namespace App\Services\ImageScan;
use RuntimeException;
/**
* SetServerImageScanning did not turn scanning on. The message is shown to the user.
*/
final class ImageScanningRefused extends RuntimeException {}
+2
View File
@@ -264,6 +264,7 @@ function sharedDataApplications(?Application $application = null): array
}
}],
'is_consistent_container_name_enabled' => 'boolean',
'is_image_scan_enabled' => 'boolean',
'custom_internal_name' => ['nullable', ...ValidationPatterns::containerNameRules()],
'custom_container_name_prefix' => 'string|nullable|max:'.ApplicationSetting::MAX_CONTAINER_NAME_PREFIX_LENGTH,
'preview_url_template' => 'string',
@@ -551,6 +552,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('gpu_device_ids');
$request->offsetUnset('gpu_options');
$request->offsetUnset('is_consistent_container_name_enabled');
$request->offsetUnset('is_image_scan_enabled');
$request->offsetUnset('custom_internal_name');
$request->offsetUnset('custom_container_name_prefix');
$request->offsetUnset('docker_compose_raw');
+868 -3
View File
@@ -944,6 +944,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -1612,6 +1616,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -2280,6 +2288,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -2948,6 +2960,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -3506,6 +3522,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -4044,6 +4064,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -4861,6 +4885,10 @@
"type": "boolean",
"description": "Use a consistent container name across deployments."
},
"is_image_scan_enabled": {
"type": "boolean",
"description": "Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true."
},
"custom_internal_name": {
"type": "string",
"nullable": true,
@@ -19480,6 +19508,167 @@
]
}
},
"\/servers\/{uuid}\/image-scanning": {
"get": {
"tags": [
"Servers"
],
"summary": "Get image scanning settings",
"description": "Get the image vulnerability scanning settings of a server owned by the authenticated team, and the state of the scanner on the server. Needs a team admin or owner.",
"operationId": "get-server-image-scanning",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Server UUID",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Image scanning settings.",
"content": {
"application\/json": {
"schema": {
"properties": {
"is_image_scanning_allowed": {
"description": "Whether the Coolify instance allows image scanning.",
"type": "boolean",
"readOnly": true
},
"is_image_scan_enabled": {
"description": "Whether the server scans the images of its resources.",
"type": "boolean"
},
"scanner_state": {
"description": "State of the scanner on the server. Null when unknown.",
"type": [
"string",
"null"
],
"enum": [
"pulling",
"updating_database",
"ready",
"failed"
]
},
"scanner_failed_step": {
"description": "The step that failed when scanner_state is failed.",
"type": [
"string",
"null"
]
},
"scanner_built_at": {
"description": "Time of the vulnerability database on the server.",
"type": [
"string",
"null"
],
"format": "date-time"
},
"scanner_disk_used_bytes": {
"description": "Disk space that the scanner uses on the server.",
"type": [
"integer",
"null"
]
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"403": {
"description": "Needs a team admin or owner."
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"patch": {
"tags": [
"Servers"
],
"summary": "Update image scanning settings",
"description": "Turn image vulnerability scanning of a server owned by the authenticated team on or off. Turning it on gets the scanner on the server in the background.",
"operationId": "update-server-image-scanning",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Server UUID",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application\/json": {
"schema": {
"required": [
"is_image_scan_enabled"
],
"properties": {
"is_image_scan_enabled": {
"type": "boolean"
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Updated image scanning settings."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"403": {
"description": "Needs a team admin or owner."
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"409": {
"description": "The instance does not allow image scanning, or another Coolify instance manages the server."
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/servers\/{uuid}\/log-drains": {
"get": {
"tags": [
@@ -25793,13 +25982,15 @@
"content": {
"application\/json": {
"schema": {
"required": [
"is_build_server_fallback_enabled"
],
"minProperties": 1,
"properties": {
"is_build_server_fallback_enabled": {
"type": "boolean",
"description": "Whether deployments can fall back to the deployment server when no usable dedicated build server is available."
},
"scan_images_on_new_servers": {
"type": "boolean",
"description": "Whether image scanning is turned on for servers added to the team from now on."
}
},
"type": "object"
@@ -26405,6 +26596,314 @@
]
}
},
"\/vulnerabilities": {
"get": {
"tags": [
"Vulnerabilities"
],
"summary": "List vulnerabilities",
"description": "List the applications, services, and databases of the authenticated team with their stored security status from image scanning.",
"operationId": "list-vulnerabilities",
"parameters": [
{
"name": "status",
"in": "query",
"description": "Comma-separated statuses to return.",
"required": false,
"schema": {
"type": "string",
"example": "update_now,update_available"
}
}
],
"responses": {
"200": {
"description": "Resources with their security status, worst first.",
"content": {
"application\/json": {
"schema": {
"properties": {
"is_image_scanning_allowed": {
"type": "boolean"
},
"resources": {
"type": "array",
"items": {
"properties": {
"uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"application",
"service",
"database"
]
},
"project_uuid": {
"type": [
"string",
"null"
]
},
"environment_uuid": {
"type": [
"string",
"null"
]
},
"server_uuid": {
"type": [
"string",
"null"
]
},
"images": {
"properties": {
"update": {
"type": "integer"
},
"total": {
"type": "integer"
}
},
"type": "object"
},
"security": {
"$ref": "#\/components\/schemas\/SecurityStatus"
}
},
"type": "object"
}
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/applications\/{uuid}\/vulnerabilities": {
"get": {
"tags": [
"Vulnerabilities"
],
"summary": "Get application vulnerabilities",
"description": "Get the security status, the scanned images, and the vulnerabilities of an application.",
"operationId": "get-application-vulnerabilities",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Application UUID",
"required": true,
"schema": {
"type": "string"
}
},
{
"$ref": "#\/components\/parameters\/VulnerabilityState"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySeverity"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityImage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySearch"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPerPage"
}
],
"responses": {
"200": {
"description": "Scan results.",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/ResourceVulnerabilities"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/services\/{uuid}\/vulnerabilities": {
"get": {
"tags": [
"Vulnerabilities"
],
"summary": "Get service vulnerabilities",
"description": "Get the security status, the scanned images, and the vulnerabilities of the applications and databases of a service.",
"operationId": "get-service-vulnerabilities",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Service UUID",
"required": true,
"schema": {
"type": "string"
}
},
{
"$ref": "#\/components\/parameters\/VulnerabilityState"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySeverity"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityImage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySearch"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPerPage"
}
],
"responses": {
"200": {
"description": "Scan results.",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/ResourceVulnerabilities"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases\/{uuid}\/vulnerabilities": {
"get": {
"tags": [
"Vulnerabilities"
],
"summary": "Get database vulnerabilities",
"description": "Get the security status, the scanned image, and the vulnerabilities of a standalone database.",
"operationId": "get-database-vulnerabilities",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "Database UUID",
"required": true,
"schema": {
"type": "string"
}
},
{
"$ref": "#\/components\/parameters\/VulnerabilityState"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySeverity"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityImage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilitySearch"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPage"
},
{
"$ref": "#\/components\/parameters\/VulnerabilityPerPage"
}
],
"responses": {
"200": {
"description": "Scan results.",
"content": {
"application\/json": {
"schema": {
"$ref": "#\/components\/schemas\/ResourceVulnerabilities"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/vultr\/regions": {
"get": {
"tags": [
@@ -27125,6 +27624,305 @@
},
"type": "object"
},
"SecurityStatus": {
"description": "Security status of a resource from image scanning.",
"properties": {
"status": {
"type": "string",
"enum": [
"update_now",
"update_available",
"no_fix",
"check_needed",
"ok",
"not_checked"
]
},
"label": {
"type": "string"
},
"action": {
"description": "What to do, as the UI shows it.",
"type": [
"string",
"null"
]
},
"check_reason": {
"type": [
"string",
"null"
]
},
"problem_count": {
"type": "integer"
},
"exploited_count": {
"type": "integer"
},
"any_exploited": {
"type": "boolean"
},
"critical_count": {
"type": "integer"
},
"high_count": {
"type": "integer"
},
"medium_count": {
"type": "integer"
},
"eol": {
"description": "The end-of-life OS of the image.",
"type": [
"object",
"null"
]
},
"checked_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"actions": {
"type": "array",
"items": {
"properties": {
"type": {
"type": "string"
},
"sentence": {
"type": "string"
},
"target": {
"type": [
"string",
"null"
]
},
"copy": {
"type": [
"string",
"null"
]
},
"image": {
"type": [
"string",
"null"
]
},
"ref": {
"type": [
"string",
"null"
]
}
},
"type": "object"
}
}
},
"type": "object"
},
"ResourceVulnerabilities": {
"description": "Image scan results of a resource.",
"properties": {
"uuid": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"application",
"service",
"database"
]
},
"off_reason": {
"description": "Why the resource is not scanned. Null when it is scanned.",
"type": [
"string",
"null"
],
"enum": [
"instance",
"server",
"application"
]
},
"security": {
"oneOf": [
{
"$ref": "#\/components\/schemas\/SecurityStatus"
},
{
"type": "null"
}
]
},
"images": {
"type": "array",
"items": {
"properties": {
"scan_uuid": {
"type": "string"
},
"owner_uuid": {
"description": "The application, database, or service application or database that runs the image.",
"type": "string"
},
"image_ref": {
"type": "string"
},
"scan_status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed"
]
},
"error_code": {
"type": [
"string",
"null"
]
},
"scanned_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"truncated": {
"type": "boolean"
},
"security_status": {
"type": "string"
}
},
"type": "object"
}
},
"findings": {
"type": "array",
"items": {
"properties": {
"id": {
"type": "string"
},
"display_id": {
"type": "string"
},
"aliases": {
"type": "array",
"items": {
"type": "string"
}
},
"title": {
"type": "string"
},
"severity": {
"type": "string",
"enum": [
"critical",
"high",
"medium",
"low",
"unknown"
]
},
"package": {
"type": "string"
},
"version": {
"type": "string"
},
"package_type": {
"type": "string"
},
"fixed_in": {
"type": [
"string",
"null"
]
},
"state": {
"description": "fixed: a fix is available upstream.",
"type": "string",
"enum": [
"fixed",
"no_fix",
"ignored"
]
},
"ignore": {
"type": [
"object",
"null"
]
},
"kev": {
"description": "Listed in the CISA Known Exploited Vulnerabilities catalog.",
"type": "boolean"
},
"epss": {
"type": [
"number",
"null"
]
},
"risk": {
"type": [
"number",
"null"
]
},
"cvss_score": {
"type": [
"number",
"null"
]
},
"advisory_url": {
"type": [
"string",
"null"
]
},
"image_ref": {
"type": "string"
},
"owner_uuid": {
"type": "string"
}
},
"type": "object"
}
},
"pagination": {
"properties": {
"page": {
"type": "integer"
},
"per_page": {
"type": "integer"
},
"total": {
"type": "integer"
}
},
"type": "object"
}
},
"type": "object"
},
"ApplicationPreview": {
"properties": {
"uuid": {
@@ -29204,6 +30002,10 @@
"type": "boolean",
"description": "Whether deployments can fall back to the deployment server when no usable dedicated build server is available."
},
"scan_images_on_new_servers": {
"type": "boolean",
"description": "Whether image scanning is turned on for servers added to the team from now on."
},
"members": {
"description": "The members of the team.",
"type": "array",
@@ -29395,6 +30197,65 @@
}
}
},
"parameters": {
"VulnerabilityState": {
"name": "state",
"in": "query",
"description": "Comma-separated finding states: fixed, no_fix, ignored. All when not sent.",
"required": false,
"schema": {
"type": "string"
}
},
"VulnerabilitySeverity": {
"name": "severity",
"in": "query",
"description": "Comma-separated severities: critical, high, medium, low, unknown.",
"required": false,
"schema": {
"type": "string"
}
},
"VulnerabilityImage": {
"name": "image",
"in": "query",
"description": "Only the findings of this scanned image ref.",
"required": false,
"schema": {
"type": "string"
}
},
"VulnerabilitySearch": {
"name": "search",
"in": "query",
"description": "Text in the vulnerability ID, an alias, the package, or the version.",
"required": false,
"schema": {
"type": "string"
}
},
"VulnerabilityPage": {
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 1,
"minimum": 1
}
},
"VulnerabilityPerPage": {
"name": "per_page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 50,
"maximum": 200,
"minimum": 1
}
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
@@ -29524,6 +30385,10 @@
"name": "Teams",
"description": "Teams"
},
{
"name": "Vulnerabilities",
"description": "Vulnerabilities"
},
{
"name": "Vultr",
"description": "Vultr"
+534 -2
View File
@@ -625,6 +625,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -1070,6 +1073,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -1515,6 +1521,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -1960,6 +1969,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -2342,6 +2354,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -2710,6 +2725,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -3258,6 +3276,9 @@ paths:
is_consistent_container_name_enabled:
type: boolean
description: 'Use a consistent container name across deployments.'
is_image_scan_enabled:
type: boolean
description: 'Scan the images of the application for vulnerabilities. Needs image scanning on the server. Default true.'
custom_internal_name:
type: string
nullable: true
@@ -12375,6 +12396,89 @@ paths:
security:
-
bearerAuth: []
'/servers/{uuid}/image-scanning':
get:
tags:
- Servers
summary: 'Get image scanning settings'
description: 'Get the image vulnerability scanning settings of a server owned by the authenticated team, and the state of the scanner on the server. Needs a team admin or owner.'
operationId: get-server-image-scanning
parameters:
-
name: uuid
in: path
description: 'Server UUID'
required: true
schema:
type: string
responses:
'200':
description: 'Image scanning settings.'
content:
application/json:
schema:
properties:
is_image_scanning_allowed: { description: 'Whether the Coolify instance allows image scanning.', type: boolean, readOnly: true }
is_image_scan_enabled: { description: 'Whether the server scans the images of its resources.', type: boolean }
scanner_state: { description: 'State of the scanner on the server. Null when unknown.', type: [string, 'null'], enum: [pulling, updating_database, ready, failed] }
scanner_failed_step: { description: 'The step that failed when scanner_state is failed.', type: [string, 'null'] }
scanner_built_at: { description: 'Time of the vulnerability database on the server.', type: [string, 'null'], format: date-time }
scanner_disk_used_bytes: { description: 'Disk space that the scanner uses on the server.', type: [integer, 'null'] }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'403':
description: 'Needs a team admin or owner.'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
patch:
tags:
- Servers
summary: 'Update image scanning settings'
description: 'Turn image vulnerability scanning of a server owned by the authenticated team on or off. Turning it on gets the scanner on the server in the background.'
operationId: update-server-image-scanning
parameters:
-
name: uuid
in: path
description: 'Server UUID'
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
required:
- is_image_scan_enabled
properties:
is_image_scan_enabled:
type: boolean
type: object
responses:
'200':
description: 'Updated image scanning settings.'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'403':
description: 'Needs a team admin or owner.'
'404':
$ref: '#/components/responses/404'
'409':
description: 'The instance does not allow image scanning, or another Coolify instance manages the server.'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/servers/{uuid}/log-drains':
get:
tags:
@@ -16285,12 +16389,14 @@ paths:
content:
application/json:
schema:
required:
- is_build_server_fallback_enabled
minProperties: 1
properties:
is_build_server_fallback_enabled:
type: boolean
description: 'Whether deployments can fall back to the deployment server when no usable dedicated build server is available.'
scan_images_on_new_servers:
type: boolean
description: 'Whether image scanning is turned on for servers added to the team from now on.'
type: object
responses:
'200':
@@ -16690,6 +16796,174 @@ paths:
security:
-
bearerAuth: []
/vulnerabilities:
get:
tags:
- Vulnerabilities
summary: 'List vulnerabilities'
description: 'List the applications, services, and databases of the authenticated team with their stored security status from image scanning.'
operationId: list-vulnerabilities
parameters:
-
name: status
in: query
description: 'Comma-separated statuses to return.'
required: false
schema:
type: string
example: 'update_now,update_available'
responses:
'200':
description: 'Resources with their security status, worst first.'
content:
application/json:
schema:
properties:
is_image_scanning_allowed: { type: boolean }
resources: { type: array, items: { properties: { uuid: { type: string }, name: { type: string }, type: { type: string, enum: [application, service, database] }, project_uuid: { type: [string, 'null'] }, environment_uuid: { type: [string, 'null'] }, server_uuid: { type: [string, 'null'] }, images: { properties: { update: { type: integer }, total: { type: integer } }, type: object }, security: { $ref: '#/components/schemas/SecurityStatus' } }, type: object } }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
security:
-
bearerAuth: []
'/applications/{uuid}/vulnerabilities':
get:
tags:
- Vulnerabilities
summary: 'Get application vulnerabilities'
description: 'Get the security status, the scanned images, and the vulnerabilities of an application.'
operationId: get-application-vulnerabilities
parameters:
-
name: uuid
in: path
description: 'Application UUID'
required: true
schema:
type: string
-
$ref: '#/components/parameters/VulnerabilityState'
-
$ref: '#/components/parameters/VulnerabilitySeverity'
-
$ref: '#/components/parameters/VulnerabilityImage'
-
$ref: '#/components/parameters/VulnerabilitySearch'
-
$ref: '#/components/parameters/VulnerabilityPage'
-
$ref: '#/components/parameters/VulnerabilityPerPage'
responses:
'200':
description: 'Scan results.'
content:
application/json:
schema:
$ref: '#/components/schemas/ResourceVulnerabilities'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/services/{uuid}/vulnerabilities':
get:
tags:
- Vulnerabilities
summary: 'Get service vulnerabilities'
description: 'Get the security status, the scanned images, and the vulnerabilities of the applications and databases of a service.'
operationId: get-service-vulnerabilities
parameters:
-
name: uuid
in: path
description: 'Service UUID'
required: true
schema:
type: string
-
$ref: '#/components/parameters/VulnerabilityState'
-
$ref: '#/components/parameters/VulnerabilitySeverity'
-
$ref: '#/components/parameters/VulnerabilityImage'
-
$ref: '#/components/parameters/VulnerabilitySearch'
-
$ref: '#/components/parameters/VulnerabilityPage'
-
$ref: '#/components/parameters/VulnerabilityPerPage'
responses:
'200':
description: 'Scan results.'
content:
application/json:
schema:
$ref: '#/components/schemas/ResourceVulnerabilities'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/databases/{uuid}/vulnerabilities':
get:
tags:
- Vulnerabilities
summary: 'Get database vulnerabilities'
description: 'Get the security status, the scanned image, and the vulnerabilities of a standalone database.'
operationId: get-database-vulnerabilities
parameters:
-
name: uuid
in: path
description: 'Database UUID'
required: true
schema:
type: string
-
$ref: '#/components/parameters/VulnerabilityState'
-
$ref: '#/components/parameters/VulnerabilitySeverity'
-
$ref: '#/components/parameters/VulnerabilityImage'
-
$ref: '#/components/parameters/VulnerabilitySearch'
-
$ref: '#/components/parameters/VulnerabilityPage'
-
$ref: '#/components/parameters/VulnerabilityPerPage'
responses:
'200':
description: 'Scan results.'
content:
application/json:
schema:
$ref: '#/components/schemas/ResourceVulnerabilities'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
/vultr/regions:
get:
tags:
@@ -17195,6 +17469,212 @@ components:
- 'null'
format: date-time
type: object
SecurityStatus:
description: 'Security status of a resource from image scanning.'
properties:
status:
type: string
enum:
- update_now
- update_available
- no_fix
- check_needed
- ok
- not_checked
label:
type: string
action:
description: 'What to do, as the UI shows it.'
type:
- string
- 'null'
check_reason:
type:
- string
- 'null'
problem_count:
type: integer
exploited_count:
type: integer
any_exploited:
type: boolean
critical_count:
type: integer
high_count:
type: integer
medium_count:
type: integer
eol:
description: 'The end-of-life OS of the image.'
type:
- object
- 'null'
checked_at:
type:
- string
- 'null'
format: date-time
actions:
type: array
items:
properties:
type:
type: string
sentence:
type: string
target:
type:
- string
- 'null'
copy:
type:
- string
- 'null'
image:
type:
- string
- 'null'
ref:
type:
- string
- 'null'
type: object
type: object
ResourceVulnerabilities:
description: 'Image scan results of a resource.'
properties:
uuid:
type: string
type:
type: string
enum:
- application
- service
- database
off_reason:
description: 'Why the resource is not scanned. Null when it is scanned.'
type:
- string
- 'null'
enum:
- instance
- server
- application
security:
oneOf:
-
$ref: '#/components/schemas/SecurityStatus'
-
type: 'null'
images:
type: array
items:
properties:
scan_uuid:
type: string
owner_uuid:
description: 'The application, database, or service application or database that runs the image.'
type: string
image_ref:
type: string
scan_status:
type: string
enum:
- queued
- running
- completed
- failed
error_code:
type:
- string
- 'null'
scanned_at:
type:
- string
- 'null'
format: date-time
truncated:
type: boolean
security_status:
type: string
type: object
findings:
type: array
items:
properties:
id:
type: string
display_id:
type: string
aliases:
type: array
items:
type: string
title:
type: string
severity:
type: string
enum:
- critical
- high
- medium
- low
- unknown
package:
type: string
version:
type: string
package_type:
type: string
fixed_in:
type:
- string
- 'null'
state:
description: 'fixed: a fix is available upstream.'
type: string
enum:
- fixed
- no_fix
- ignored
ignore:
type:
- object
- 'null'
kev:
description: 'Listed in the CISA Known Exploited Vulnerabilities catalog.'
type: boolean
epss:
type:
- number
- 'null'
risk:
type:
- number
- 'null'
cvss_score:
type:
- number
- 'null'
advisory_url:
type:
- string
- 'null'
image_ref:
type: string
owner_uuid:
type: string
type: object
pagination:
properties:
page:
type: integer
per_page:
type: integer
total:
type: integer
type: object
type: object
ApplicationPreview:
properties:
uuid:
@@ -18696,6 +19176,9 @@ components:
is_build_server_fallback_enabled:
type: boolean
description: 'Whether deployments can fall back to the deployment server when no usable dedicated build server is available.'
scan_images_on_new_servers:
type: boolean
description: 'Whether image scanning is turned on for servers added to the team from now on.'
members:
description: 'The members of the team.'
type: array
@@ -18818,6 +19301,52 @@ components:
- 'null'
example: forbidden
type: object
parameters:
VulnerabilityState:
name: state
in: query
description: 'Comma-separated finding states: fixed, no_fix, ignored. All when not sent.'
required: false
schema:
type: string
VulnerabilitySeverity:
name: severity
in: query
description: 'Comma-separated severities: critical, high, medium, low, unknown.'
required: false
schema:
type: string
VulnerabilityImage:
name: image
in: query
description: 'Only the findings of this scanned image ref.'
required: false
schema:
type: string
VulnerabilitySearch:
name: search
in: query
description: 'Text in the vulnerability ID, an alias, the package, or the version.'
required: false
schema:
type: string
VulnerabilityPage:
name: page
in: query
required: false
schema:
type: integer
default: 1
minimum: 1
VulnerabilityPerPage:
name: per_page
in: query
required: false
schema:
type: integer
default: 50
maximum: 200
minimum: 1
securitySchemes:
bearerAuth:
type: http
@@ -18914,6 +19443,9 @@ tags:
-
name: Teams
description: Teams
-
name: Vulnerabilities
description: Vulnerabilities
-
name: Vultr
description: Vultr
+9
View File
@@ -29,6 +29,7 @@ use App\Http\Controllers\Api\SentinelController;
use App\Http\Controllers\Api\ServerCloudflareTunnelController;
use App\Http\Controllers\Api\ServerDockerCleanupController;
use App\Http\Controllers\Api\ServerDockerRegistriesController;
use App\Http\Controllers\Api\ServerImageScanningController;
use App\Http\Controllers\Api\ServerLogDrainsController;
use App\Http\Controllers\Api\ServerProxyController;
use App\Http\Controllers\Api\ServersController;
@@ -41,6 +42,7 @@ use App\Http\Controllers\Api\SharedEnvironmentVariablesController;
use App\Http\Controllers\Api\TagsController;
use App\Http\Controllers\Api\TeamController;
use App\Http\Controllers\Api\VolumeBackupsController;
use App\Http\Controllers\Api\VulnerabilitiesController;
use App\Http\Controllers\Api\VultrController;
use App\Http\Middleware\ApiAllowed;
use Illuminate\Support\Facades\Route;
@@ -184,6 +186,9 @@ Route::group([
Route::get('/servers/{uuid}/sentinel', [ServerSentinelController::class, 'show'])->middleware(['api.ability:read']);
Route::patch('/servers/{uuid}/sentinel', [ServerSentinelController::class, 'update'])->middleware(['api.ability:write', 'api.ability:deploy']);
Route::get('/servers/{uuid}/image-scanning', [ServerImageScanningController::class, 'show'])->middleware(['api.ability:read']);
Route::patch('/servers/{uuid}/image-scanning', [ServerImageScanningController::class, 'update'])->middleware(['api.ability:write']);
Route::get('/servers/{uuid}/cloudflare-tunnel', [ServerCloudflareTunnelController::class, 'show'])->middleware(['api.ability:read']);
Route::patch('/servers/{uuid}/cloudflare-tunnel', [ServerCloudflareTunnelController::class, 'update'])->middleware(['api.ability:write']);
Route::post('/servers/{uuid}/cloudflare-tunnel/enable', [ServerCloudflareTunnelController::class, 'enable'])->middleware(['api.ability:write']);
@@ -248,6 +253,7 @@ Route::group([
Route::post('/servers/hostinger', [HostingerController::class, 'createServer'])->middleware(['api.ability:write']);
Route::get('/resources', [ResourcesController::class, 'resources'])->middleware(['api.ability:read']);
Route::get('/vulnerabilities', [VulnerabilitiesController::class, 'index'])->middleware(['api.ability:read']);
Route::get('/tags', [TagsController::class, 'tags'])->middleware(['api.ability:read']);
Route::post('/tags', [TagsController::class, 'create'])->middleware(['api.ability:write']);
@@ -263,6 +269,7 @@ Route::group([
Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['api.ability:write']);
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']);
Route::get('/applications/{uuid}/vulnerabilities', [VulnerabilitiesController::class, 'application'])->middleware(['api.ability:read']);
Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware(['api.ability:delete']);
@@ -352,6 +359,7 @@ Route::group([
Route::post('/databases/sqlite', [DatabasesController::class, 'create_database_sqlite'])->middleware(['api.ability:write']);
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']);
Route::get('/databases/{uuid}/vulnerabilities', [VulnerabilitiesController::class, 'database'])->middleware(['api.ability:read']);
Route::post('/databases/{uuid}/imports/uploads', [DatabasesController::class, 'upload_import'])->middleware(['api.ability:write'])->name('api.databases.imports.upload');
Route::post('/databases/{uuid}/imports', [DatabasesController::class, 'create_import'])->middleware(['api.ability:write'])->name('api.databases.imports.store');
Route::get('/databases/{uuid}/imports/{activity_id}', [DatabasesController::class, 'show_import'])->middleware(['api.ability:read'])->whereNumber('activity_id')->name('api.databases.imports.show');
@@ -403,6 +411,7 @@ Route::group([
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']);
Route::get('/services/{uuid}/vulnerabilities', [VulnerabilitiesController::class, 'service'])->middleware(['api.ability:read']);
Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['api.ability:delete']);