mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
refactor(image-scan): merge the feature migrations into one
The tables and columns are created in their final form, so the gate mode rewrite and the status refresh steps for earlier branch states are no longer needed.
This commit is contained in:
22 files changed
+204
-605
No files matched your search
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_scan_policies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('scope_type')->nullable();
|
||||
$table->unsignedBigInteger('scope_id')->nullable();
|
||||
$table->string('alert_level')->nullable();
|
||||
$table->boolean('gate_enabled')->nullable();
|
||||
$table->string('on_scan_error')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['scope_type', 'scope_id']);
|
||||
});
|
||||
|
||||
DB::table('image_scan_policies')->insert([
|
||||
'uuid' => new_public_id(),
|
||||
'scope_type' => null,
|
||||
'scope_id' => null,
|
||||
'alert_level' => 'critical',
|
||||
'gate_enabled' => false,
|
||||
'on_scan_error' => 'deploy',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('image_scan_policies');
|
||||
}
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_scan_ignores', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
|
||||
$table->nullableMorphs('resource');
|
||||
$table->string('vulnerability_id', 64)->nullable();
|
||||
$table->string('package', 214)->nullable();
|
||||
$table->string('reason', 500);
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['team_id', 'resource_type', 'resource_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('image_scan_ignores');
|
||||
}
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_sboms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('image_id', 80);
|
||||
$table->string('platform', 32);
|
||||
$table->jsonb('packages');
|
||||
$table->boolean('packages_truncated')->default(false);
|
||||
$table->timestamp('last_used_at');
|
||||
|
||||
$table->unique(['server_id', 'image_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('image_sboms');
|
||||
}
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_scans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('image_sbom_id')->nullable()->constrained('image_sboms')->nullOnDelete();
|
||||
$table->string('image_id', 80);
|
||||
$table->string('repo_digest', 80)->nullable();
|
||||
$table->morphs('resource');
|
||||
$table->string('image_ref', 512);
|
||||
$table->foreignId('deployment_queue_id')->nullable()->constrained('application_deployment_queues')->nullOnDelete();
|
||||
$table->string('status');
|
||||
$table->string('error_code')->nullable();
|
||||
$table->jsonb('error_context')->nullable();
|
||||
$table->jsonb('summary')->nullable();
|
||||
$table->jsonb('findings')->nullable();
|
||||
$table->jsonb('candidates')->nullable();
|
||||
$table->string('base_image_ref', 512)->nullable();
|
||||
$table->boolean('builds_image')->default(false);
|
||||
$table->boolean('is_baseline')->default(true);
|
||||
$table->timestamp('grype_db_built_at')->nullable();
|
||||
$table->timestamp('scanned_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['resource_type', 'resource_id', 'image_id']);
|
||||
$table->index(['server_id', 'image_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('image_scans');
|
||||
}
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scan_enabled')->default(false);
|
||||
$table->unsignedInteger('image_candidate_scans_today')->default(0);
|
||||
$table->date('image_candidate_scans_date')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scanning_allowed')->default(true);
|
||||
$table->string('image_scan_db_url')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->boolean('scan_images_on_new_servers')->default(false);
|
||||
});
|
||||
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scan_enabled')->default(true);
|
||||
$table->boolean('image_builds_by_team')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->jsonb('scan_gate_result')->nullable();
|
||||
$table->foreignId('scan_gate_bypassed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('scan_gate_bypassed_by');
|
||||
$table->dropColumn('scan_gate_result');
|
||||
});
|
||||
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scan_enabled', 'image_builds_by_team']);
|
||||
});
|
||||
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('scan_images_on_new_servers');
|
||||
});
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scanning_allowed', 'image_scan_db_url']);
|
||||
});
|
||||
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scan_enabled', 'image_candidate_scans_today', 'image_candidate_scans_date']);
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_email_notifications')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_email_notifications');
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_discord_notifications')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('discord_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_discord_notifications');
|
||||
});
|
||||
}
|
||||
};
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_telegram_notifications')->default(false);
|
||||
$table->text('telegram_notifications_image_vulnerabilities_thread_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_telegram_notifications');
|
||||
$table->dropColumn('telegram_notifications_image_vulnerabilities_thread_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_slack_notifications')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('slack_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_slack_notifications');
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_pushover_notifications')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pushover_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_pushover_notifications');
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('webhook_notification_settings', function (Blueprint $table) {
|
||||
$table->boolean('image_vulnerabilities_webhook_notifications')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('webhook_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('image_vulnerabilities_webhook_notifications');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->jsonb('notified_ids')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->dropColumn('notified_ids');
|
||||
});
|
||||
}
|
||||
};
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->jsonb('notified_candidates')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->dropColumn('notified_candidates');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Notification state moves from the scan rows to one row per resource. The scan columns of the two earlier
|
||||
* migrations of this unreleased feature are dropped here.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('resource');
|
||||
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
|
||||
$table->jsonb('notified_ids')->nullable();
|
||||
$table->jsonb('notified_candidates')->nullable();
|
||||
$table->boolean('is_baseline')->default(true);
|
||||
$table->unsignedBigInteger('baseline_deployment_queue_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['resource_type', 'resource_id']);
|
||||
});
|
||||
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->dropColumn(['notified_ids', 'notified_candidates']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scans', function (Blueprint $table) {
|
||||
$table->jsonb('notified_ids')->nullable();
|
||||
$table->jsonb('notified_candidates')->nullable();
|
||||
});
|
||||
|
||||
Schema::dropIfExists('image_scan_notification_states');
|
||||
}
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* The unique key on (resource_type, resource_id) already serves every lookup, so the index that morphs() added on the
|
||||
* same columns only slows down writes.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->dropIndex(['resource_type', 'resource_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->index(['resource_type', 'resource_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\RefreshImageScanSummariesJob;
|
||||
use App\Models\ImageScan;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Stores the security status (spec 15.6) on the current scans once: one queued summary refresh per team with scans.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
try {
|
||||
Server::query()
|
||||
->whereIn('id', ImageScan::query()->select('server_id')->distinct())
|
||||
->distinct()
|
||||
->pluck('team_id')
|
||||
->filter(fn ($teamId): bool => $teamId !== null)
|
||||
->each(fn ($teamId) => RefreshImageScanSummariesJob::dispatch((int) $teamId));
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* "Block deploys" gets three modes (spec 15.9): on → strict (closest to the old rule), off → off, inherit stays
|
||||
* inherit. The alert level is gone.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('image_scan_policies', function (Blueprint $table) {
|
||||
$table->string('gate_mode')->nullable()->after('gate_enabled');
|
||||
});
|
||||
|
||||
DB::table('image_scan_policies')->where('gate_enabled', true)->update(['gate_mode' => 'strict']);
|
||||
DB::table('image_scan_policies')->where('gate_enabled', false)->update(['gate_mode' => 'off']);
|
||||
DB::table('image_scan_policies')->whereNull('scope_type')->whereNull('gate_mode')->update(['gate_mode' => 'off']);
|
||||
|
||||
Schema::table('image_scan_policies', function (Blueprint $table) {
|
||||
$table->dropColumn(['gate_enabled', 'alert_level']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scan_policies', function (Blueprint $table) {
|
||||
$table->string('alert_level')->nullable()->after('scope_id');
|
||||
$table->boolean('gate_enabled')->nullable()->after('alert_level');
|
||||
});
|
||||
|
||||
DB::table('image_scan_policies')->whereIn('gate_mode', ['on', 'strict'])->update(['gate_enabled' => true]);
|
||||
DB::table('image_scan_policies')->where('gate_mode', 'off')->update(['gate_enabled' => false]);
|
||||
DB::table('image_scan_policies')->whereNull('scope_type')->update(['alert_level' => 'critical']);
|
||||
|
||||
Schema::table('image_scan_policies', function (Blueprint $table) {
|
||||
$table->dropColumn('gate_mode');
|
||||
});
|
||||
}
|
||||
};
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* `notified_ids` keeps the findings with an exploit signal that an immediate alert named; `digest_ids` the findings
|
||||
* with an action that a digest (or an alert) named; `check_reported` whether a digest said the resource needs a check;
|
||||
* `held_digests` how many digests held new problems back while the resource needed a check.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->jsonb('digest_ids')->nullable();
|
||||
$table->boolean('check_reported')->default(false);
|
||||
$table->unsignedSmallInteger('held_digests')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->dropColumn(['digest_ids', 'check_reported', 'held_digests']);
|
||||
});
|
||||
}
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
// Security updates of a Compose app: {service: {from, to}}; applied while the compose file still names `from`.
|
||||
$table->json('docker_compose_image_overrides')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_compose_image_overrides');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/** @var array<string, array{0: string, 1: bool}> notification settings table => [alert column, default] */
|
||||
private const NOTIFICATION_COLUMNS = [
|
||||
'email_notification_settings' => ['image_vulnerabilities_email_notifications', true],
|
||||
'discord_notification_settings' => ['image_vulnerabilities_discord_notifications', false],
|
||||
'telegram_notification_settings' => ['image_vulnerabilities_telegram_notifications', false],
|
||||
'slack_notification_settings' => ['image_vulnerabilities_slack_notifications', false],
|
||||
'pushover_notification_settings' => ['image_vulnerabilities_pushover_notifications', false],
|
||||
'webhook_notification_settings' => ['image_vulnerabilities_webhook_notifications', false],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('image_scan_policies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('scope_type')->nullable();
|
||||
$table->unsignedBigInteger('scope_id')->nullable();
|
||||
$table->string('gate_mode')->nullable();
|
||||
$table->string('on_scan_error')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['scope_type', 'scope_id']);
|
||||
});
|
||||
|
||||
DB::table('image_scan_policies')->insert([
|
||||
'uuid' => new_public_id(),
|
||||
'scope_type' => null,
|
||||
'scope_id' => null,
|
||||
'gate_mode' => 'off',
|
||||
'on_scan_error' => 'deploy',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Schema::create('image_scan_ignores', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
|
||||
$table->nullableMorphs('resource');
|
||||
$table->string('vulnerability_id', 64)->nullable();
|
||||
$table->string('package', 214)->nullable();
|
||||
$table->string('reason', 500);
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['team_id', 'resource_type', 'resource_id']);
|
||||
});
|
||||
|
||||
Schema::create('image_sboms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('image_id', 80);
|
||||
$table->string('platform', 32);
|
||||
$table->jsonb('packages');
|
||||
$table->boolean('packages_truncated')->default(false);
|
||||
$table->timestamp('last_used_at');
|
||||
|
||||
$table->unique(['server_id', 'image_id']);
|
||||
});
|
||||
|
||||
Schema::create('image_scans', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->foreignId('server_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('image_sbom_id')->nullable()->constrained('image_sboms')->nullOnDelete();
|
||||
$table->string('image_id', 80);
|
||||
$table->string('repo_digest', 80)->nullable();
|
||||
$table->morphs('resource');
|
||||
$table->string('image_ref', 512);
|
||||
$table->foreignId('deployment_queue_id')->nullable()->constrained('application_deployment_queues')->nullOnDelete();
|
||||
$table->string('status');
|
||||
$table->string('error_code')->nullable();
|
||||
$table->jsonb('error_context')->nullable();
|
||||
$table->jsonb('summary')->nullable();
|
||||
$table->jsonb('findings')->nullable();
|
||||
$table->jsonb('candidates')->nullable();
|
||||
$table->string('base_image_ref', 512)->nullable();
|
||||
$table->boolean('builds_image')->default(false);
|
||||
$table->boolean('is_baseline')->default(true);
|
||||
$table->timestamp('grype_db_built_at')->nullable();
|
||||
$table->timestamp('scanned_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['resource_type', 'resource_id', 'image_id']);
|
||||
$table->index(['server_id', 'image_id']);
|
||||
});
|
||||
|
||||
Schema::create('image_scan_notification_states', function (Blueprint $table) {
|
||||
$table->id();
|
||||
// No morphs(): its index would repeat the unique key below.
|
||||
$table->string('resource_type');
|
||||
$table->unsignedBigInteger('resource_id');
|
||||
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
|
||||
$table->jsonb('notified_ids')->nullable();
|
||||
$table->jsonb('notified_candidates')->nullable();
|
||||
$table->jsonb('digest_ids')->nullable();
|
||||
$table->boolean('is_baseline')->default(true);
|
||||
$table->boolean('check_reported')->default(false);
|
||||
$table->unsignedSmallInteger('held_digests')->default(0);
|
||||
$table->unsignedBigInteger('baseline_deployment_queue_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['resource_type', 'resource_id']);
|
||||
});
|
||||
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scan_enabled')->default(false);
|
||||
$table->unsignedInteger('image_candidate_scans_today')->default(0);
|
||||
$table->date('image_candidate_scans_date')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scanning_allowed')->default(true);
|
||||
$table->string('image_scan_db_url')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->boolean('scan_images_on_new_servers')->default(false);
|
||||
});
|
||||
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_image_scan_enabled')->default(true);
|
||||
$table->boolean('image_builds_by_team')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
// Security updates of a Compose app: {service: {from, to}}; applied while the compose file still names `from`.
|
||||
$table->json('docker_compose_image_overrides')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->jsonb('scan_gate_result')->nullable();
|
||||
$table->foreignId('scan_gate_bypassed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
});
|
||||
|
||||
foreach (self::NOTIFICATION_COLUMNS as $tableName => [$column, $default]) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($column, $default) {
|
||||
$table->boolean($column)->default($default);
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->text('telegram_notifications_image_vulnerabilities_thread_id')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('telegram_notification_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('telegram_notifications_image_vulnerabilities_thread_id');
|
||||
});
|
||||
|
||||
foreach (self::NOTIFICATION_COLUMNS as $tableName => [$column]) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($column) {
|
||||
$table->dropColumn($column);
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('scan_gate_bypassed_by');
|
||||
$table->dropColumn('scan_gate_result');
|
||||
});
|
||||
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_compose_image_overrides');
|
||||
});
|
||||
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scan_enabled', 'image_builds_by_team']);
|
||||
});
|
||||
|
||||
Schema::table('teams', function (Blueprint $table) {
|
||||
$table->dropColumn('scan_images_on_new_servers');
|
||||
});
|
||||
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scanning_allowed', 'image_scan_db_url']);
|
||||
});
|
||||
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['is_image_scan_enabled', 'image_candidate_scans_today', 'image_candidate_scans_date']);
|
||||
});
|
||||
|
||||
Schema::dropIfExists('image_scan_notification_states');
|
||||
Schema::dropIfExists('image_scans');
|
||||
Schema::dropIfExists('image_sboms');
|
||||
Schema::dropIfExists('image_scan_ignores');
|
||||
Schema::dropIfExists('image_scan_policies');
|
||||
}
|
||||
};
|
||||
@@ -223,28 +223,11 @@ it('keeps one index on the resource of a notification state, the unique key', fu
|
||||
->and($indexes->first()['unique'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('turns Block deploys on into strict and drops the alert level when it migrates', function () {
|
||||
$migration = require database_path('migrations/2026_10_27_130000_replace_image_scan_gate_enabled_with_gate_mode.php');
|
||||
$migration->down();
|
||||
ImageScanPolicy::query()->whereNull('scope_type')->delete();
|
||||
$rows = [
|
||||
'instance' => [null, null, null],
|
||||
'on' => [$this->server->getMorphClass(), $this->server->id, true],
|
||||
'off' => [$this->team->getMorphClass(), $this->team->id, false],
|
||||
'inherit' => ['App\\Models\\Application', 1, null],
|
||||
];
|
||||
foreach ($rows as $name => [$type, $id, $gate]) {
|
||||
DB::table('image_scan_policies')->insert(['uuid' => $name, 'scope_type' => $type, 'scope_id' => $id, 'alert_level' => 'high', 'gate_enabled' => $gate]);
|
||||
}
|
||||
it('starts with instance rules that do not block deploys and deploy when a scan cannot run', function () {
|
||||
$instance = DB::table('image_scan_policies')->whereNull('scope_type')->whereNull('scope_id')->first();
|
||||
|
||||
$migration->up();
|
||||
|
||||
expect(DB::table('image_scan_policies')->pluck('gate_mode', 'uuid')->all())->toBe([
|
||||
'instance' => 'off',
|
||||
'on' => 'strict',
|
||||
'off' => 'off',
|
||||
'inherit' => null,
|
||||
])
|
||||
expect($instance->gate_mode)->toBe('off')
|
||||
->and($instance->on_scan_error)->toBe('deploy')
|
||||
->and(Schema::hasColumns('image_scan_policies', ['alert_level']))->toBeFalse()
|
||||
->and(Schema::hasColumns('image_scan_policies', ['gate_enabled']))->toBeFalse();
|
||||
});
|
||||
@@ -4,7 +4,6 @@ use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ImageScanStatus;
|
||||
use App\Enums\SecurityActionType;
|
||||
use App\Enums\SecurityStatusType;
|
||||
use App\Jobs\RefreshImageScanSummariesJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Environment;
|
||||
@@ -825,14 +824,4 @@ describe('stored status', function () {
|
||||
->and($stored[ResourceScans::key($four[0])]->checkContext)->toBe(['checked' => 1, 'total' => 2])
|
||||
->and($stored[ResourceScans::key($four[0])]->toArray())->toBe(SecurityStatus::stored($four[0])->toArray());
|
||||
});
|
||||
|
||||
it('refreshes the stored status of every team once after the upgrade', function () {
|
||||
statusTestScan($this->application, []);
|
||||
$migration = require database_path('migrations/2026_10_27_120000_refresh_image_scan_security_status.php');
|
||||
|
||||
$migration->up();
|
||||
|
||||
Queue::assertPushed(RefreshImageScanSummariesJob::class, fn (RefreshImageScanSummariesJob $job): bool => $job->teamId === $this->team->id);
|
||||
Queue::assertPushed(RefreshImageScanSummariesJob::class, 1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user