From 7a35116ee580a56263079df3e789531da16367b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 3 Jul 2026 17:39:21 +0800 Subject: [PATCH] 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 --- src/server/__tests__/skill-market.test.ts | 71 ++++++++++++++++++- .../services/skillMarket/clawhubAdapter.ts | 27 ++++--- .../services/skillMarket/skillhubAdapter.ts | 5 +- 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/server/__tests__/skill-market.test.ts b/src/server/__tests__/skill-market.test.ts index 147abaf0..131da521 100644 --- a/src/server/__tests__/skill-market.test.ts +++ b/src/server/__tests__/skill-market.test.ts @@ -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) }) diff --git a/src/server/services/skillMarket/clawhubAdapter.ts b/src/server/services/skillMarket/clawhubAdapter.ts index 2678a232..705d148e 100644 --- a/src/server/services/skillMarket/clawhubAdapter.ts +++ b/src/server/services/skillMarket/clawhubAdapter.ts @@ -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, } } diff --git a/src/server/services/skillMarket/skillhubAdapter.ts b/src/server/services/skillMarket/skillhubAdapter.ts index f1ff9c0e..c12030f1 100644 --- a/src/server/services/skillMarket/skillhubAdapter.ts +++ b/src/server/services/skillMarket/skillhubAdapter.ts @@ -58,7 +58,6 @@ const SKILLHUB_DETAIL_INSTALLABLE_TRUST_STATES = new Set( ]) 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) => values.find((report) => report.status && statuses.has(report.status) && report.statusText)?.statusText - const summaryExcludingStatuses = (statuses: Set) => - 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 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'] {