feat: add content asset security scanning

This commit is contained in:
Codex
2026-06-29 19:42:05 +08:00
parent d696fc38b0
commit ecd269b548
21 changed files with 936 additions and 75 deletions

View File

@@ -8,6 +8,7 @@ const tenantId = '00000000-0000-0000-0000-000000000001';
const ids = {
okAsset: '20000000-0000-0000-0000-000000000801',
badAsset: '20000000-0000-0000-0000-000000000802',
scanBadAsset: '20000000-0000-0000-0000-000000000803',
};
const checksumA = 'a'.repeat(64);
@@ -52,16 +53,23 @@ async function cleanup(pool) {
delete from public.audit_logs
where tenant_id = $1
and target_type = 'content_asset'
and target_id in ($2, $3)
and target_id in ($2, $3, $4)
`,
[tenantId, ids.okAsset, ids.badAsset],
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
);
await pool.query(
`
delete from public.content_asset_security_scan_events
where tenant_id = $1 and asset_id in ($2::uuid, $3::uuid, $4::uuid)
`,
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
);
await pool.query(
`
delete from public.content_assets
where tenant_id = $1 and id in ($2::uuid, $3::uuid)
where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid)
`,
[tenantId, ids.okAsset, ids.badAsset],
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
);
}
@@ -72,7 +80,9 @@ async function seed(pool) {
id, tenant_id, asset_key, title, asset_type, storage_provider,
bucket, object_key, file_name, mime_type, file_size_bytes, checksum_sha256,
visibility, status, upload_status, verified_at, verified_size_bytes,
verified_checksum_sha256, verification_details, security_flags, source,
verified_checksum_sha256, verification_details,
security_scan_status, security_scan_provider, security_scan_summary,
security_flags, source,
created_at, updated_at
)
values
@@ -80,14 +90,27 @@ async function seed(pool) {
$1, $2, 'asset-worker-ok', '资源复检正常 PDF', 'pdf', 'local_dev',
'tenant-assets', $3, 'ok.pdf', 'application/pdf', 4096, $4,
'tenant', 'active', 'verified', now() - interval '2 days', 4096,
$4, '{"source":"asset-worker-test"}'::jsonb, '{}'::jsonb, 'integration-test',
$4, '{"source":"asset-worker-test"}'::jsonb,
'pending', 'metadata_rules', '{}'::jsonb,
'{}'::jsonb, 'integration-test',
now() - interval '2 days', now() - interval '2 days'
),
(
$5, $2, 'asset-worker-bad', '资源复检异常 PDF', 'pdf', 'local_dev',
'tenant-assets', $6, 'bad.pdf', 'application/pdf', 1024, $7,
'tenant', 'active', 'verified', now() - interval '2 days', 2048,
$7, '{"source":"asset-worker-test"}'::jsonb, '{}'::jsonb, 'integration-test',
$7, '{"source":"asset-worker-test"}'::jsonb,
'pending', 'metadata_rules', '{}'::jsonb,
'{}'::jsonb, 'integration-test',
now() - interval '2 days', now() - interval '2 days'
),
(
$8, $2, 'asset-worker-scan-bad', '资源安全扫描异常 PDF', 'pdf', 'local_dev',
'tenant-assets', $9, 'scan-bad.pdf', 'application/pdf', 4096, $4,
'tenant', 'active', 'verified', now() - interval '2 days', 4096,
$4, '{"source":"asset-worker-test"}'::jsonb,
'pending', 'metadata_rules', '{"securityScanForceFail":true}'::jsonb,
'{}'::jsonb, 'integration-test',
now() - interval '2 days', now() - interval '2 days'
)
`,
@@ -99,6 +122,8 @@ async function seed(pool) {
ids.badAsset,
`${tenantId}/assets/worker-bad.pdf`,
checksumB,
ids.scanBadAsset,
`${tenantId}/assets/worker-scan-bad.pdf`,
],
);
}
@@ -115,32 +140,78 @@ async function main() {
const output = await runWorkerOnce();
assert.ok(countFromWorkerOutput(output, 'verified') >= 1, 'worker should verify at least one asset');
assert.ok(countFromWorkerOutput(output, 'failed') >= 1, 'worker should fail at least one mismatched asset');
assert.ok(countFromWorkerOutput(output, 'failed') >= 2, 'worker should fail metadata and security scan assets');
const assets = await pool.query(
`
select id, status, upload_status, verification_details, security_flags
select id, status, upload_status, verification_details,
security_scan_status, security_scan_provider, security_scan_summary,
security_flags
from public.content_assets
where tenant_id = $1 and id in ($2::uuid, $3::uuid)
where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid)
order by id
`,
[tenantId, ids.okAsset, ids.badAsset],
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
);
const okAsset = assets.rows.find(row => row.id === ids.okAsset);
const badAsset = assets.rows.find(row => row.id === ids.badAsset);
const scanBadAsset = assets.rows.find(row => row.id === ids.scanBadAsset);
assert.equal(okAsset?.status, 'active', 'verified asset should remain active');
assert.equal(okAsset?.upload_status, 'verified', 'verified asset should remain verified');
assert.equal(okAsset?.security_scan_status, 'passed', 'verified asset should pass security scan');
assert.equal(okAsset?.security_scan_provider, 'metadata_rules', 'verified asset should record scan provider');
assert.equal(okAsset?.verification_details?.assetWorker?.lastResult, 'verified', 'verified asset should record worker result');
assert.equal(okAsset?.security_flags?.assetRecheckFailed, undefined, 'verified asset should not keep recheck failure flag');
assert.equal(badAsset?.status, 'draft', 'mismatched asset should be unpublished');
assert.equal(badAsset?.upload_status, 'failed', 'mismatched asset should be marked failed');
assert.equal(badAsset?.security_scan_status, 'skipped', 'mismatched asset should skip security scan');
assert.equal(badAsset?.security_flags?.assetRecheckFailed, true, 'mismatched asset should record security flag');
assert.deepEqual(
badAsset?.verification_details?.assetWorker?.issues,
['file_size_mismatch'],
'mismatched asset should record exact issue',
);
assert.equal(scanBadAsset?.status, 'draft', 'security failed asset should be unpublished');
assert.equal(scanBadAsset?.upload_status, 'verified', 'security failed asset should keep upload verification evidence');
assert.equal(scanBadAsset?.security_scan_status, 'failed', 'security failed asset should record failed scan status');
assert.equal(scanBadAsset?.security_flags?.assetSecurityScanFailed, true, 'security failed asset should record security flag');
assert.ok(
scanBadAsset?.security_scan_summary?.issueCodes?.includes('security_scan_forced_failure'),
'security failed asset should record issue code',
);
const scanEvents = await pool.query(
`
select asset_id, scan_status, risk_level, issue_codes, provider, details
from public.content_asset_security_scan_events
where tenant_id = $1 and asset_id in ($2::uuid, $3::uuid)
order by created_at asc
`,
[tenantId, ids.okAsset, ids.scanBadAsset],
);
assert.ok(
scanEvents.rows.some(row => row.asset_id === ids.okAsset && row.scan_status === 'passed' && row.provider === 'metadata_rules'),
'worker should write passed security scan event',
);
assert.ok(
scanEvents.rows.some(row => row.asset_id === ids.scanBadAsset && row.scan_status === 'failed' && row.issue_codes.includes('security_scan_forced_failure')),
'worker should write failed security scan event',
);
const skippedScanEvents = await pool.query(
`
select asset_id, scan_status, risk_level, issue_codes, provider, details
from public.content_asset_security_scan_events
where tenant_id = $1 and asset_id = $2::uuid
order by created_at asc
`,
[tenantId, ids.badAsset],
);
assert.ok(
skippedScanEvents.rows.some(row => row.scan_status === 'skipped' && row.issue_codes.includes('file_size_mismatch')),
'worker should write skipped scan event when metadata verification fails',
);
const audits = await pool.query(
`
@@ -148,10 +219,10 @@ async function main() {
from public.audit_logs
where tenant_id = $1
and target_type = 'content_asset'
and target_id in ($2, $3)
and target_id in ($2, $3, $4)
order by created_at asc
`,
[tenantId, ids.okAsset, ids.badAsset],
[tenantId, ids.okAsset, ids.badAsset, ids.scanBadAsset],
);
assert.ok(
audits.rows.some(row => row.action === 'content.asset.rechecked' && row.details?.result === 'verified'),
@@ -161,6 +232,10 @@ async function main() {
audits.rows.some(row => row.action === 'content.asset.recheck_failed' && row.details?.issues?.includes('file_size_mismatch')),
'worker should write failed audit log',
);
assert.ok(
audits.rows.some(row => row.action === 'content.asset.security_scan_failed' && row.details?.issueCodes?.includes('security_scan_forced_failure')),
'worker should write failed security scan audit log',
);
console.log('Asset worker integration test complete.');
} catch (error) {