fix(security): clarify safe audit verdict

Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
dongmucat 2026-09-17 18:08:25 +08:00
parent ba0398bb1a
commit 9bbadca31b
6 changed files with 206 additions and 3 deletions

View file

@ -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)
})
})

View file

@ -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(
<FindingItem
finding={createFinding({
codeSnippet: 'const token = "[REDACTED]"',
metadata: { originalSnippet: 'SYNTHETIC-CANARY-868' },
})}
/>,
)
expect(html).toContain('const token = &quot;[REDACTED]&quot;')
expect(html).not.toContain('SYNTHETIC-CANARY-868')
})
it('omits the code snippet when codeSnippet is null', () => {
const finding = createFinding({ codeSnippet: null })
const html = renderToStaticMarkup(<FindingItem finding={finding} />)

View file

@ -1776,7 +1776,7 @@
"remediation": "Remediation",
"viewDetails": "View Details",
"verdict": {
"SAFE": "Safe",
"SAFE": "No high-risk findings",
"SUSPICIOUS": "Suspicious",
"DANGEROUS": "Dangerous",
"BLOCKED": "High Risk"

View file

@ -1803,7 +1803,7 @@
"remediation": "Рекомендации",
"viewDetails": "Подробности",
"verdict": {
"SAFE": "Безопасно",
"SAFE": "Угроз высокого риска не обнаружено",
"SUSPICIOUS": "Подозрительно",
"DANGEROUS": "Опасно",
"BLOCKED": "Высокий риск"

View file

@ -1775,7 +1775,7 @@
"remediation": "修复建议",
"viewDetails": "查看详情",
"verdict": {
"SAFE": "安全",
"SAFE": "未发现高风险问题",
"SUSPICIOUS": "可疑",
"DANGEROUS": "危险",
"BLOCKED": "高风险"

View file

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