mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
fix(security): clarify safe audit verdict
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
ba0398bb1a
commit
9bbadca31b
6 changed files with 206 additions and 3 deletions
173
web/e2e/security-audit-redaction.spec.ts
Normal file
173
web/e2e/security-audit-redaction.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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 = "[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(<FindingItem finding={finding} />)
|
||||
|
|
|
|||
|
|
@ -1776,7 +1776,7 @@
|
|||
"remediation": "Remediation",
|
||||
"viewDetails": "View Details",
|
||||
"verdict": {
|
||||
"SAFE": "Safe",
|
||||
"SAFE": "No high-risk findings",
|
||||
"SUSPICIOUS": "Suspicious",
|
||||
"DANGEROUS": "Dangerous",
|
||||
"BLOCKED": "High Risk"
|
||||
|
|
|
|||
|
|
@ -1803,7 +1803,7 @@
|
|||
"remediation": "Рекомендации",
|
||||
"viewDetails": "Подробности",
|
||||
"verdict": {
|
||||
"SAFE": "Безопасно",
|
||||
"SAFE": "Угроз высокого риска не обнаружено",
|
||||
"SUSPICIOUS": "Подозрительно",
|
||||
"DANGEROUS": "Опасно",
|
||||
"BLOCKED": "Высокий риск"
|
||||
|
|
|
|||
|
|
@ -1775,7 +1775,7 @@
|
|||
"remediation": "修复建议",
|
||||
"viewDetails": "查看详情",
|
||||
"verdict": {
|
||||
"SAFE": "安全",
|
||||
"SAFE": "未发现高风险问题",
|
||||
"SUSPICIOUS": "可疑",
|
||||
"DANGEROUS": "危险",
|
||||
"BLOCKED": "高风险"
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue