From 9bbadca31b2bf0bc2c663745cc9b5e0300e659bd Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Thu, 17 Sep 2026 18:08:25 +0800 Subject: [PATCH] fix(security): clarify safe audit verdict Signed-off-by: dongmucat <1127093059@qq.com> --- web/e2e/security-audit-redaction.spec.ts | 173 ++++++++++++++++++ .../security-audit/finding-item.test.tsx | 14 ++ web/src/i18n/locales/en.json | 2 +- web/src/i18n/locales/ru.json | 2 +- web/src/i18n/locales/zh.json | 2 +- web/src/i18n/security-audit-locale.test.ts | 16 ++ 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 web/e2e/security-audit-redaction.spec.ts diff --git a/web/e2e/security-audit-redaction.spec.ts b/web/e2e/security-audit-redaction.spec.ts new file mode 100644 index 00000000..5ac14092 --- /dev/null +++ b/web/e2e/security-audit-redaction.spec.ts @@ -0,0 +1,173 @@ +import { expect, test, type Page, type Route } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' + +const CANARY = 'SYNTHETIC-CANARY-868' +const MASKED_SNIPPET = 'const token = "[REDACTED]"' + +function envelope(data: unknown) { + return { + code: 0, + msg: 'success', + data, + timestamp: '2026-09-17T00:00:00Z', + requestId: 'security-audit-redaction-fixture', + } +} + +async function fulfill(route: Route, data: unknown) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(envelope(data)), + }) +} + +async function mockSkillDetail(page: Page, audits: unknown[]) { + await page.route(/\/api\//, async (route) => { + const url = new URL(route.request().url()) + const { pathname } = url + + if (!pathname.startsWith('/api/')) { + await route.continue() + return + } + + if (pathname === '/api/v1/auth/me') { + await fulfill(route, { + userId: 'audit-admin', + displayName: 'Audit Admin', + platformRoles: ['SUPER_ADMIN'], + oauthProvider: 'local', + canChangePassword: true, + }) + return + } + + if (pathname === '/api/web/skills/global/redaction-skill') { + await fulfill(route, { + id: 868, + slug: 'redaction-skill', + displayName: 'Redaction Skill', + ownerId: 'skill-owner', + ownerDisplayName: 'Skill Owner', + summary: 'Security audit redaction fixture', + visibility: 'PUBLIC', + status: 'ACTIVE', + downloadCount: 0, + starCount: 0, + ratingCount: 0, + hidden: false, + namespace: 'global', + canManageLifecycle: false, + canSubmitPromotion: false, + canInteract: false, + canReport: false, + headlineVersion: { id: 8681, version: '1.0.0', status: 'PUBLISHED' }, + publishedVersion: { id: 8681, version: '1.0.0', status: 'PUBLISHED' }, + resolutionMode: 'PUBLISHED', + labels: [], + }) + return + } + + if (pathname === '/api/web/skills/global/redaction-skill/versions') { + await fulfill(route, { + items: [{ + id: 8681, + version: '1.0.0', + status: 'PUBLISHED', + fileCount: 0, + totalSize: 0, + publishedAt: '2026-09-17T00:00:00Z', + downloadAvailable: true, + }], + total: 1, + page: 0, + size: 20, + }) + return + } + + if (pathname === '/api/web/skills/global/redaction-skill/versions/1.0.0/files') { + await fulfill(route, []) + return + } + + if (pathname === '/api/v1/skills/868/versions/8681/security-audit') { + await fulfill(route, audits) + return + } + + if (pathname === '/api/web/skills/868/reviews') { + await fulfill(route, { items: [], total: 0, page: 0, size: 20 }) + return + } + + if (pathname === '/api/web/skills/868/reviews/me') { + await fulfill(route, null) + return + } + + if (pathname === '/api/web/notifications/unread-count') { + await fulfill(route, { count: 0 }) + return + } + + await fulfill(route, []) + }) +} + +test.describe('Security audit redaction', () => { + test.beforeEach(async ({ page }) => { + await setEnglishLocale(page) + }) + + test('shows the precise SAFE verdict and only the masked finding snippet', async ({ page }) => { + await mockSkillDetail(page, [{ + id: 1, + scanId: 'scan-868', + scannerType: 'builtin', + verdict: 'SAFE', + isSafe: true, + maxSeverity: 'MEDIUM', + findingsCount: 1, + findings: [{ + ruleId: 'SECRET-001', + severity: 'MEDIUM', + category: 'secret', + title: 'Embedded credential', + message: 'A credential-like value was detected.', + filePath: 'scripts/deploy.ts', + lineNumber: 7, + codeSnippet: MASKED_SNIPPET, + remediation: 'Read credentials from the environment.', + analyzer: 'synthetic', + metadata: { originalSnippet: CANARY }, + }], + scanDurationSeconds: 1.2, + failureReason: null, + scannedAt: '2026-09-17T00:00:00Z', + createdAt: '2026-09-17T00:00:00Z', + }]) + + await page.goto('/space/global/redaction-skill') + + await expect(page.getByRole('heading', { name: 'Redaction Skill', exact: true }).first()).toBeVisible() + await expect(page.getByText('No high-risk findings', { exact: true })).toBeVisible() + await page.getByRole('button', { name: 'View Details' }).click() + await page.getByRole('button', { name: 'Findings' }).click() + + await expect(page.getByText(MASKED_SNIPPET, { exact: true })).toBeVisible() + await expect(page.locator('body')).not.toContainText(CANARY) + }) + + test('keeps the skill detail usable when the audit list is empty', async ({ page }) => { + await mockSkillDetail(page, []) + + await page.goto('/space/global/redaction-skill') + + await expect(page.getByRole('heading', { name: 'Redaction Skill', exact: true }).first()).toBeVisible() + await expect(page.getByText('Security Audit', { exact: true })).toHaveCount(0) + await expect(page.locator('body')).not.toContainText(CANARY) + }) +}) diff --git a/web/src/features/security-audit/finding-item.test.tsx b/web/src/features/security-audit/finding-item.test.tsx index a0d0c488..72177864 100644 --- a/web/src/features/security-audit/finding-item.test.tsx +++ b/web/src/features/security-audit/finding-item.test.tsx @@ -98,6 +98,20 @@ describe('FindingItem', () => { expect(html).toContain('SELECT * FROM users WHERE id = ${input}') }) + it('renders a masked snippet without exposing the original canary', () => { + const html = renderToStaticMarkup( + , + ) + + expect(html).toContain('const token = "[REDACTED]"') + expect(html).not.toContain('SYNTHETIC-CANARY-868') + }) + it('omits the code snippet when codeSnippet is null', () => { const finding = createFinding({ codeSnippet: null }) const html = renderToStaticMarkup() diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index dd1facca..d798fac3 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1776,7 +1776,7 @@ "remediation": "Remediation", "viewDetails": "View Details", "verdict": { - "SAFE": "Safe", + "SAFE": "No high-risk findings", "SUSPICIOUS": "Suspicious", "DANGEROUS": "Dangerous", "BLOCKED": "High Risk" diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index c7a1c8c9..2785424d 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -1803,7 +1803,7 @@ "remediation": "Рекомендации", "viewDetails": "Подробности", "verdict": { - "SAFE": "Безопасно", + "SAFE": "Угроз высокого риска не обнаружено", "SUSPICIOUS": "Подозрительно", "DANGEROUS": "Опасно", "BLOCKED": "Высокий риск" diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 681e3545..53df4ad9 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1775,7 +1775,7 @@ "remediation": "修复建议", "viewDetails": "查看详情", "verdict": { - "SAFE": "安全", + "SAFE": "未发现高风险问题", "SUSPICIOUS": "可疑", "DANGEROUS": "危险", "BLOCKED": "高风险" diff --git a/web/src/i18n/security-audit-locale.test.ts b/web/src/i18n/security-audit-locale.test.ts index 3cb5e5f6..0ce69811 100644 --- a/web/src/i18n/security-audit-locale.test.ts +++ b/web/src/i18n/security-audit-locale.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import en from './locales/en.json' import zh from './locales/zh.json' +import ru from './locales/ru.json' describe('security audit locales', () => { it('defines the scanning label in both locales', () => { @@ -12,4 +13,19 @@ describe('security audit locales', () => { expect(zh.securityAudit.verdict.BLOCKED).toBe('高风险') expect(en.securityAudit.verdict.BLOCKED).toBe('High Risk') }) + + it('uses the precise safe verdict wording in all locales', () => { + expect(en.securityAudit.verdict.SAFE).toBe('No high-risk findings') + expect(zh.securityAudit.verdict.SAFE).toBe('未发现高风险问题') + expect(ru.securityAudit.verdict.SAFE).toBe('Угроз высокого риска не обнаружено') + }) + + it('keeps verdict keys in parity across all locales', () => { + expect(Object.keys(en.securityAudit.verdict).sort()).toEqual( + Object.keys(zh.securityAudit.verdict).sort(), + ) + expect(Object.keys(en.securityAudit.verdict).sort()).toEqual( + Object.keys(ru.securityAudit.verdict).sort(), + ) + }) })