forked from wangziqi/gongxue-base
feat: add content asset security scanning
This commit is contained in:
@@ -3,6 +3,7 @@ import crypto from 'node:crypto';
|
||||
import { spawn } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import pg from 'pg';
|
||||
import { SignJWT } from 'jose';
|
||||
import ExcelJS from 'exceljs';
|
||||
|
||||
@@ -147,6 +148,39 @@ async function check(name, fn) {
|
||||
console.log(`[PASS] ${name}`);
|
||||
}
|
||||
|
||||
async function markAssetSecurityScanPassed(assetId, provider = 'api_integration_test') {
|
||||
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || DEFAULT_DATABASE_URL });
|
||||
try {
|
||||
await pool.query(
|
||||
`
|
||||
update public.content_assets
|
||||
set security_scan_status = 'passed',
|
||||
security_scanned_at = now(),
|
||||
security_scan_provider = $3,
|
||||
security_scan_summary = '{"riskLevel":"none","issueCodes":[],"provider":"api_integration_test"}'::jsonb,
|
||||
security_flags = coalesce(security_flags, '{}'::jsonb) - 'assetSecurityScanFailed',
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2::uuid
|
||||
`,
|
||||
[MAIN_TENANT_ID, assetId, provider],
|
||||
);
|
||||
await pool.query(
|
||||
`
|
||||
insert into public.content_asset_security_scan_events (
|
||||
tenant_id, asset_id, provider, scan_status, risk_level, issue_codes, details
|
||||
)
|
||||
values (
|
||||
$1, $2::uuid, $3, 'passed', 'none', '{}'::text[],
|
||||
'{"source":"api_integration_test"}'::jsonb
|
||||
)
|
||||
`,
|
||||
[MAIN_TENANT_ID, assetId, provider],
|
||||
);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
function getFreePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
@@ -3518,6 +3552,7 @@ async function testTenantContentAssetsAndImports() {
|
||||
assert.equal(localAsset.item?.objectKey, upload.assetDraft.objectKey, 'asset upsert should keep normalized object key');
|
||||
assert.equal(localAsset.item?.status, 'draft', 'managed object asset should stay draft before upload confirmation');
|
||||
assert.equal(localAsset.item?.uploadStatus, 'pending', 'managed object asset should be pending before upload confirmation');
|
||||
assert.equal(localAsset.item?.securityScanStatus, 'pending', 'managed object asset should require security scan before publishing');
|
||||
|
||||
const unconfirmedDownload = await request('/api/catalog/assets/download', {
|
||||
query: { assetId: localAsset.item.id },
|
||||
@@ -3536,16 +3571,60 @@ async function testTenantContentAssetsAndImports() {
|
||||
publish: true,
|
||||
},
|
||||
});
|
||||
assert.equal(confirmLocal.item?.status, 'active', 'confirmed managed asset should publish when requested');
|
||||
assert.equal(confirmLocal.item?.status, 'draft', 'confirmed managed asset should stay draft until security scan passes');
|
||||
assert.equal(confirmLocal.item?.uploadStatus, 'verified', 'confirm upload should mark asset verified');
|
||||
assert.equal(confirmLocal.item?.securityScanStatus, 'pending', 'confirm upload should mark asset security scan pending');
|
||||
assert.equal(confirmLocal.item?.verifiedChecksumSha256, 'a'.repeat(64), 'confirm upload should persist checksum');
|
||||
|
||||
const scanPendingEvents = await request('/api/tenant-content/assets/security-scan-events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetId: localAsset.item.id },
|
||||
});
|
||||
assert.ok(Array.isArray(scanPendingEvents.items), 'tenant admin should list asset security scan events');
|
||||
|
||||
const localDownload = await request('/api/catalog/assets/download', {
|
||||
query: { assetId: localAsset.item.id },
|
||||
expectStatus: 404,
|
||||
});
|
||||
assert.equal(localDownload.download?.method, 'GET', 'object asset download should sign GET');
|
||||
assert.equal(localDownload.download?.signatureMode, 'local-placeholder', 'local object asset should use local placeholder signer');
|
||||
assert.ok(localDownload.download?.url?.includes(encodeURIComponent(localAsset.item.bucket)), 'object asset download should include bucket');
|
||||
assert.equal(localDownload.code, 'ASSET_NOT_FOUND', 'unscanned draft asset should not be downloadable');
|
||||
|
||||
await markAssetSecurityScanPassed(localAsset.item.id);
|
||||
const publishScannedLocal = await request('/api/tenant-content/assets', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: localAsset.item.id,
|
||||
title: '集成测试本地对象资料',
|
||||
assetType: 'pdf',
|
||||
storageProvider: 'local_dev',
|
||||
bucket: upload.assetDraft.bucket,
|
||||
objectKey: upload.assetDraft.objectKey,
|
||||
fileName: upload.assetDraft.fileName,
|
||||
mimeType: upload.assetDraft.mimeType,
|
||||
fileSizeBytes: upload.assetDraft.fileSizeBytes,
|
||||
checksumSha256: upload.assetDraft.checksumSha256,
|
||||
visibility: 'tenant',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(publishScannedLocal.item?.status, 'active', 'scanned managed asset should publish');
|
||||
assert.equal(publishScannedLocal.item?.securityScanStatus, 'passed', 'published managed asset should keep passed scan status');
|
||||
|
||||
const scanEventsAfterPass = await request('/api/tenant-content/assets/security-scan-events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { assetId: localAsset.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
scanEventsAfterPass.items?.some(item => item.scanStatus === 'passed' && item.provider === 'api_integration_test'),
|
||||
'asset security scan pass event should be visible to tenant admin',
|
||||
);
|
||||
|
||||
const scannedLocalDownload = await request('/api/catalog/assets/download', {
|
||||
query: { assetId: localAsset.item.id },
|
||||
});
|
||||
assert.equal(scannedLocalDownload.download?.method, 'GET', 'object asset download should sign GET');
|
||||
assert.equal(scannedLocalDownload.download?.signatureMode, 'local-placeholder', 'local object asset should use local placeholder signer');
|
||||
assert.ok(scannedLocalDownload.download?.url?.includes(encodeURIComponent(localAsset.item.bucket)), 'object asset download should include bucket');
|
||||
|
||||
const localPreview = await request('/api/catalog/assets/preview', {
|
||||
query: { assetId: localAsset.item.id },
|
||||
|
||||
Reference in New Issue
Block a user