Files
gongxue-base/supabase/migrations/202606290024_content_asset_security_scans.sql
2026-06-29 19:42:05 +08:00

94 lines
4.2 KiB
SQL

alter table public.content_assets
add column if not exists security_scan_status text not null default 'not_required',
add column if not exists security_scanned_at timestamptz,
add column if not exists security_scan_provider text,
add column if not exists security_scan_summary jsonb not null default '{}'::jsonb;
update public.content_assets
set security_scan_status = case
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
and upload_status = 'verified'
then 'passed'
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
and upload_status = 'failed'
then 'skipped'
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
then 'pending'
else 'not_required'
end,
security_scanned_at = case
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
and upload_status = 'verified'
then coalesce(security_scanned_at, verified_at, updated_at, created_at, now())
else security_scanned_at
end,
security_scan_provider = case
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
and upload_status = 'verified'
then coalesce(security_scan_provider, 'legacy_backfill')
else security_scan_provider
end,
security_scan_summary = case
when storage_provider in ('local_dev', 'supabase_storage', 'aliyun_oss', 'tencent_cos')
and object_key is not null
and upload_status = 'verified'
then coalesce(security_scan_summary, '{}'::jsonb)
|| jsonb_build_object(
'riskLevel', 'none',
'issueCodes', '[]'::jsonb,
'backfilled', true,
'backfilledAt', now()
)
else coalesce(security_scan_summary, '{}'::jsonb)
end
where security_scan_status = 'not_required'
or security_scan_summary = '{}'::jsonb;
do $$
begin
if not exists (select 1 from pg_constraint where conname = 'content_assets_security_scan_status_check') then
alter table public.content_assets
add constraint content_assets_security_scan_status_check
check (security_scan_status in ('not_required', 'pending', 'scanning', 'passed', 'failed', 'skipped'));
end if;
end $$;
create table if not exists public.content_asset_security_scan_events (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
asset_id uuid references public.content_assets(id) on delete set null,
provider text not null,
scan_status text not null
check (scan_status in ('pending', 'scanning', 'passed', 'failed', 'skipped')),
risk_level text not null default 'none'
check (risk_level in ('none', 'low', 'medium', 'high', 'critical')),
issue_codes text[] not null default '{}',
details jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
comment on table public.content_asset_security_scan_events is
'内容资源安全扫描事件。用于记录托管对象的内置规则扫描、杀毒/内容安全 provider 扫描结果、失败原因和审计轨迹。';
create index if not exists idx_asset_security_scan_events_asset
on public.content_asset_security_scan_events(tenant_id, asset_id, created_at desc);
create index if not exists idx_asset_security_scan_events_status
on public.content_asset_security_scan_events(tenant_id, scan_status, risk_level, created_at desc);
create index if not exists idx_content_assets_security_scan_status
on public.content_assets(tenant_id, security_scan_status, upload_status, status, updated_at desc);
alter table public.content_asset_security_scan_events enable row level security;
drop policy if exists tenant_isolation on public.content_asset_security_scan_events;
create policy tenant_isolation on public.content_asset_security_scan_events
for all
using (tenant_id = app.current_tenant_id() or app.is_platform_admin())
with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());