fix: align skill market security summaries

This commit is contained in:
程序员阿江(Relakkes) 2026-07-03 17:08:12 +08:00
parent 26b3322fb7
commit c744213aae
3 changed files with 87 additions and 4 deletions

View File

@ -57,6 +57,19 @@ describe('skill market source normalization', () => {
})
})
it('uses malicious ClawHub scanner summaries for malicious scans', () => {
expect(normalizeClawHubScan({
status: 'malicious',
scanners: {
metadata: { status: 'clean', summary: 'No dangerous patterns detected.' },
staticAnalysis: { status: 'malicious', summary: 'Credential exfiltration detected.' },
},
})).toMatchObject({
trustState: 'blocked',
trustSummary: 'Credential exfiltration detected.',
})
})
it('normalizes SkillHub list items as fallback candidates with Chinese summary', () => {
const result = normalizeSkillHubList(SKILLHUB_TOP_SKILLS_RESPONSE)
@ -130,6 +143,38 @@ describe('skill market source normalization', () => {
})
})
it('rejects SkillHub external URLs with userinfo', () => {
const list = normalizeSkillHubList({
code: 0,
data: {
skills: [
{
slug: 'skill-vetter',
name: 'Skill Vetter',
upstream_url: 'https://evil.test@github.com/path',
},
],
},
})
expect(list.items[0]).toMatchObject({
canonicalUrl: 'https://skillhub.cn/skills/skill-vetter',
upstreamUrl: 'https://skillhub.cn/skills/skill-vetter',
})
const detail = normalizeSkillHubDetail({
skill: {
slug: 'skill-vetter',
displayName: 'Skill Vetter',
sourceUrl: 'https://user:password@github.com/path',
},
})
expect(detail).toMatchObject({
canonicalUrl: 'https://skillhub.cn/skills/skill-vetter',
})
})
it('normalizes SkillHub detail security reports', () => {
const detail = normalizeSkillHubDetail(SKILLHUB_DETAIL_RESPONSE)
@ -141,4 +186,22 @@ describe('skill market source normalization', () => {
trustSummary: '安全,无风险',
})
})
it('uses malicious SkillHub report summaries for blocked details', () => {
const detail = normalizeSkillHubDetail({
securityReports: {
community: { status: 'benign', statusText: 'safe' },
staticAnalysis: { status: 'malicious', statusText: 'Credential exfiltration detected.' },
},
skill: {
slug: 'skill-vetter',
displayName: 'Skill Vetter',
},
})
expect(detail).toMatchObject({
trustState: 'blocked',
trustSummary: 'Credential exfiltration detected.',
})
})
})

View File

@ -56,15 +56,27 @@ export function normalizeClawHubScan(payload: ClawHubScanResponse): {
trustSummary?: string
packageSha256?: string
} {
const scannerSummary = Object.values(payload.scanners ?? {}).find((entry) => entry.summary)?.summary
const scannerEntries = Object.values(payload.scanners ?? {})
const scannerSummary = scannerEntries.find((entry) => entry.summary)?.summary
const scannerSummaryForStatuses = (statuses: string[]) =>
scannerEntries.find((entry) => entry.summary && entry.status && statuses.includes(entry.status))?.summary
if (payload.status === 'clean' && !payload.hasWarnings) {
return { trustState: 'clean', trustSummary: scannerSummary, packageSha256: payload.sha256 }
}
if (payload.status === 'malicious' || payload.status === 'blocked') {
return { trustState: 'blocked', trustSummary: scannerSummary, packageSha256: payload.sha256 }
return {
trustState: 'blocked',
trustSummary: scannerSummaryForStatuses(['malicious', 'blocked']) ?? scannerSummary,
packageSha256: payload.sha256,
}
}
if (payload.status === 'suspicious' || payload.hasWarnings) {
return { trustState: 'warning', trustSummary: scannerSummary, packageSha256: payload.sha256 }
return {
trustState: 'warning',
trustSummary: scannerSummaryForStatuses(['suspicious', 'warning']) ?? scannerSummary,
packageSha256: payload.sha256,
}
}
return { trustState: 'unknown', trustSummary: scannerSummary, packageSha256: payload.sha256 }
}

View File

@ -61,6 +61,9 @@ function normalizeExternalUrl(value: string | undefined, slug: string) {
try {
const url = new URL(value)
if (url.username || url.password) {
return fallbackUrl
}
if (url.protocol === 'https:' && ALLOWED_EXTERNAL_URL_HOSTS.has(url.hostname)) {
return url.toString()
}
@ -77,7 +80,12 @@ function trustFromReports(reports?: Record<string, { status?: string; statusText
} {
const values = Object.values(reports ?? {})
if (values.some((report) => report.status === 'malicious' || report.status === 'blocked')) {
return { trustState: 'blocked', trustSummary: values.find((report) => report.statusText)?.statusText }
return {
trustState: 'blocked',
trustSummary: values.find((report) =>
(report.status === 'malicious' || report.status === 'blocked') && report.statusText
)?.statusText,
}
}
if (values.length > 0 && values.every((report) => report.status === 'benign')) {
return { trustState: 'benign', trustSummary: values.find((report) => report.statusText)?.statusText }