mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
feat(image-scan): manage image scanning and read scan results via API and MCP
This commit is contained in:
@@ -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.'],
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.';
|
||||
|
||||
@@ -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}", [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).';
|
||||
}
|
||||
}
|
||||
@@ -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)'],
|
||||
|
||||
@@ -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).';
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user