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(),
+ )
+ })
})