fix: prioritize risky clawhub scanner results

Tested: bun test src/server/__tests__/skill-market.test.ts
Tested: git diff --check
Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-03 17:39:21 +08:00
parent b6bdc00877
commit 7a35116ee5
3 changed files with 88 additions and 15 deletions

View File

@ -70,6 +70,19 @@ describe('skill market source normalization', () => {
})
})
it('prioritizes malicious ClawHub scanner results over clean top-level status', () => {
expect(normalizeClawHubScan({
status: 'clean',
scanners: {
metadata: { status: 'clean', summary: 'No dangerous patterns detected.' },
staticAnalysis: { status: 'malicious', summary: 'Credential exfiltration detected.' },
},
})).toMatchObject({
trustState: 'blocked',
trustSummary: 'Credential exfiltration detected.',
})
})
it('does not use clean ClawHub scanner summaries for blocked scans', () => {
expect(normalizeClawHubScan({
status: 'malicious',
@ -96,6 +109,31 @@ describe('skill market source normalization', () => {
})
})
it('prioritizes warning ClawHub scanner results over clean top-level status', () => {
expect(normalizeClawHubScan({
status: 'clean',
scanners: {
metadata: { status: 'clean', summary: 'No dangerous patterns detected.' },
staticAnalysis: { status: 'warning', summary: 'Reads shell profile files.' },
},
})).toMatchObject({
trustState: 'warning',
trustSummary: 'Reads shell profile files.',
})
})
it('maps ClawHub top-level warning status to warning trust state', () => {
expect(normalizeClawHubScan({
status: 'warning',
scanners: {
staticAnalysis: { status: 'warning', summary: 'Reads shell profile files.' },
},
})).toMatchObject({
trustState: 'warning',
trustSummary: 'Reads shell profile files.',
})
})
it('does not use clean ClawHub scanner summaries for unknown scans', () => {
expect(normalizeClawHubScan({
status: 'unknown',
@ -109,6 +147,19 @@ describe('skill market source normalization', () => {
})
})
it('does not use unscored ClawHub scanner summaries for unknown scans', () => {
expect(normalizeClawHubScan({
status: 'unknown',
scanners: {
metadata: { summary: 'No dangerous patterns detected.' },
},
})).toEqual({
trustState: 'unknown',
trustSummary: undefined,
packageSha256: undefined,
})
})
it('normalizes SkillHub list items as fallback candidates with Chinese summary', () => {
const result = normalizeSkillHubList(SKILLHUB_TOP_SKILLS_RESPONSE)
@ -294,7 +345,25 @@ describe('skill market source normalization', () => {
})
expect(detail.trustState).toBe('unknown')
expect(detail.trustSummary).toBe('Scanner still reviewing.')
expect(detail.trustSummary).toBeUndefined()
expect(detail.installEligibility.status).toBe('blocked')
expect(detail.installEligibility.reason).toMatch(/security report is missing or inconclusive/i)
})
it('does not use unscored SkillHub report summaries for unknown details', () => {
const detail = normalizeSkillHubDetail({
securityReports: {
community: { status: 'benign', statusText: 'safe' },
staticAnalysis: { statusText: 'No issues detected.' },
},
skill: {
slug: 'unscored-report-skill',
displayName: 'Unscored Report Skill',
},
})
expect(detail.trustState).toBe('unknown')
expect(detail.trustSummary).toBeUndefined()
expect(detail.installEligibility.status).toBe('blocked')
expect(detail.installEligibility.reason).toMatch(/security report is missing or inconclusive/i)
})

View File

@ -57,32 +57,39 @@ export function normalizeClawHubScan(payload: ClawHubScanResponse): {
packageSha256?: string
} {
const scannerEntries = Object.values(payload.scanners ?? {})
const blockedStatuses = ['malicious', 'blocked']
const warningStatuses = ['suspicious', 'warning']
const scannerSummary = scannerEntries.find((entry) => entry.summary)?.summary
const scannerSummaryForStatuses = (statuses: string[]) =>
scannerEntries.find((entry) => entry.summary && entry.status && statuses.includes(entry.status))?.summary
const scannerSummaryExcludingStatuses = (statuses: string[]) =>
scannerEntries.find((entry) => entry.summary && (!entry.status || !statuses.includes(entry.status)))?.summary
const hasScannerStatus = (statuses: string[]) =>
scannerEntries.some((entry) => entry.status && statuses.includes(entry.status))
if (payload.status === 'clean' && !payload.hasWarnings) {
return { trustState: 'clean', trustSummary: scannerSummary, packageSha256: payload.sha256 }
}
if (payload.status === 'malicious' || payload.status === 'blocked') {
if (payload.status === 'malicious' || payload.status === 'blocked' || hasScannerStatus(blockedStatuses)) {
return {
trustState: 'blocked',
trustSummary: scannerSummaryForStatuses(['malicious', 'blocked']),
trustSummary: scannerSummaryForStatuses(blockedStatuses),
packageSha256: payload.sha256,
}
}
if (payload.status === 'suspicious' || payload.hasWarnings) {
if (
payload.status === 'suspicious'
|| payload.status === 'warning'
|| payload.hasWarnings
|| hasScannerStatus(warningStatuses)
) {
return {
trustState: 'warning',
trustSummary: scannerSummaryForStatuses(['suspicious', 'warning']),
trustSummary: scannerSummaryForStatuses(warningStatuses),
packageSha256: payload.sha256,
}
}
if (payload.status === 'clean') {
return { trustState: 'clean', trustSummary: scannerSummary, packageSha256: payload.sha256 }
}
return {
trustState: 'unknown',
trustSummary: scannerSummaryExcludingStatuses(['benign', 'clean']),
trustSummary: undefined,
packageSha256: payload.sha256,
}
}

View File

@ -58,7 +58,6 @@ const SKILLHUB_DETAIL_INSTALLABLE_TRUST_STATES = new Set<SkillMarketTrustState>(
])
const SKILLHUB_BLOCKED_REPORT_STATUSES = new Set(['malicious', 'blocked'])
const SKILLHUB_WARNING_REPORT_STATUSES = new Set(['warning', 'suspicious'])
const SKILLHUB_BENIGN_REPORT_STATUSES = new Set(['benign', 'clean'])
function requiresApiKey(labels?: { requires_api_key?: string }) {
return labels?.requires_api_key === 'true'
@ -96,8 +95,6 @@ function trustFromReports(reports?: Record<string, { status?: string; statusText
const values = Object.values(reports ?? {})
const summaryForStatuses = (statuses: Set<string>) =>
values.find((report) => report.status && statuses.has(report.status) && report.statusText)?.statusText
const summaryExcludingStatuses = (statuses: Set<string>) =>
values.find((report) => (!report.status || !statuses.has(report.status)) && report.statusText)?.statusText
if (values.some((report) => report.status && SKILLHUB_BLOCKED_REPORT_STATUSES.has(report.status))) {
return {
@ -114,7 +111,7 @@ function trustFromReports(reports?: Record<string, { status?: string; statusText
if (values.length > 0 && values.every((report) => report.status === 'benign')) {
return { trustState: 'benign', trustSummary: values.find((report) => report.statusText)?.statusText }
}
return { trustState: 'unknown', trustSummary: summaryExcludingStatuses(SKILLHUB_BENIGN_REPORT_STATUSES) }
return { trustState: 'unknown' }
}
function installEligibilityFromTrustState(trustState: SkillMarketTrustState): SkillMarketDetail['installEligibility'] {