diff --git a/src/server/__tests__/skill-market.test.ts b/src/server/__tests__/skill-market.test.ts index 888acf6d..4e57f9ea 100644 --- a/src/server/__tests__/skill-market.test.ts +++ b/src/server/__tests__/skill-market.test.ts @@ -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.', + }) + }) }) diff --git a/src/server/services/skillMarket/clawhubAdapter.ts b/src/server/services/skillMarket/clawhubAdapter.ts index 092b752b..8f57569a 100644 --- a/src/server/services/skillMarket/clawhubAdapter.ts +++ b/src/server/services/skillMarket/clawhubAdapter.ts @@ -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 } } diff --git a/src/server/services/skillMarket/skillhubAdapter.ts b/src/server/services/skillMarket/skillhubAdapter.ts index a1a6660d..520d5c9d 100644 --- a/src/server/services/skillMarket/skillhubAdapter.ts +++ b/src/server/services/skillMarket/skillhubAdapter.ts @@ -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 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 }