mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-01 16:43:37 +08:00
fix: harden skill market normalization
This commit is contained in:
parent
5aeab5c206
commit
26b3322fb7
@ -51,6 +51,12 @@ describe('skill market source normalization', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps malicious ClawHub scan responses blocked even with warnings', () => {
|
||||||
|
expect(normalizeClawHubScan({ status: 'malicious', hasWarnings: true })).toMatchObject({
|
||||||
|
trustState: 'blocked',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('normalizes SkillHub list items as fallback candidates with Chinese summary', () => {
|
it('normalizes SkillHub list items as fallback candidates with Chinese summary', () => {
|
||||||
const result = normalizeSkillHubList(SKILLHUB_TOP_SKILLS_RESPONSE)
|
const result = normalizeSkillHubList(SKILLHUB_TOP_SKILLS_RESPONSE)
|
||||||
|
|
||||||
@ -60,11 +66,70 @@ describe('skill market source normalization', () => {
|
|||||||
slug: 'skill-vetter',
|
slug: 'skill-vetter',
|
||||||
summaryZh: 'AI智能体技能安全预审工具。',
|
summaryZh: 'AI智能体技能安全预审工具。',
|
||||||
canonicalUrl: 'https://clawhub.ai/spclaudehome/skill-vetter',
|
canonicalUrl: 'https://clawhub.ai/spclaudehome/skill-vetter',
|
||||||
trustState: 'benign',
|
trustState: 'unknown',
|
||||||
requiresApiKey: false,
|
requiresApiKey: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('normalizes verified SkillHub list items as signed', () => {
|
||||||
|
const result = normalizeSkillHubList({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
slug: 'verified-skill',
|
||||||
|
name: 'Verified Skill',
|
||||||
|
upstream_url: 'https://github.com/example/verified-skill',
|
||||||
|
verified: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
slug: 'verified-skill',
|
||||||
|
canonicalUrl: 'https://github.com/example/verified-skill',
|
||||||
|
upstreamUrl: 'https://github.com/example/verified-skill',
|
||||||
|
trustState: 'signed',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back when SkillHub external URLs are invalid', () => {
|
||||||
|
const list = normalizeSkillHubList({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
slug: 'unsafe/slug',
|
||||||
|
name: 'Unsafe URL Skill',
|
||||||
|
upstream_url: 'http://evil.test/unsafe/slug',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(list.items[0]).toMatchObject({
|
||||||
|
canonicalUrl: 'https://skillhub.cn/skills/unsafe%2Fslug',
|
||||||
|
upstreamUrl: 'https://skillhub.cn/skills/unsafe%2Fslug',
|
||||||
|
})
|
||||||
|
|
||||||
|
const detail = normalizeSkillHubDetail({
|
||||||
|
securityReports: {
|
||||||
|
keen: { status: 'benign', statusText: 'safe' },
|
||||||
|
},
|
||||||
|
skill: {
|
||||||
|
slug: 'unsafe/slug',
|
||||||
|
displayName: 'Unsafe URL Skill',
|
||||||
|
sourceUrl: 'https://evil.test/unsafe/slug',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(detail).toMatchObject({
|
||||||
|
canonicalUrl: 'https://skillhub.cn/skills/unsafe%2Fslug',
|
||||||
|
trustState: 'benign',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('normalizes SkillHub detail security reports', () => {
|
it('normalizes SkillHub detail security reports', () => {
|
||||||
const detail = normalizeSkillHubDetail(SKILLHUB_DETAIL_RESPONSE)
|
const detail = normalizeSkillHubDetail(SKILLHUB_DETAIL_RESPONSE)
|
||||||
|
|
||||||
|
|||||||
@ -60,11 +60,11 @@ export function normalizeClawHubScan(payload: ClawHubScanResponse): {
|
|||||||
if (payload.status === 'clean' && !payload.hasWarnings) {
|
if (payload.status === 'clean' && !payload.hasWarnings) {
|
||||||
return { trustState: 'clean', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
return { trustState: 'clean', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
||||||
}
|
}
|
||||||
if (payload.status === 'suspicious' || payload.hasWarnings) {
|
|
||||||
return { trustState: 'warning', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
|
||||||
}
|
|
||||||
if (payload.status === 'malicious' || payload.status === 'blocked') {
|
if (payload.status === 'malicious' || payload.status === 'blocked') {
|
||||||
return { trustState: 'blocked', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
return { trustState: 'blocked', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
||||||
}
|
}
|
||||||
|
if (payload.status === 'suspicious' || payload.hasWarnings) {
|
||||||
|
return { trustState: 'warning', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
||||||
|
}
|
||||||
return { trustState: 'unknown', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
return { trustState: 'unknown', trustSummary: scannerSummary, packageSha256: payload.sha256 }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,10 +37,40 @@ type SkillHubDetailResponse = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ALLOWED_EXTERNAL_URL_HOSTS = new Set([
|
||||||
|
'clawhub.ai',
|
||||||
|
'skillhub.cn',
|
||||||
|
'api.skillhub.cn',
|
||||||
|
'github.com',
|
||||||
|
'raw.githubusercontent.com',
|
||||||
|
])
|
||||||
|
|
||||||
function requiresApiKey(labels?: { requires_api_key?: string }) {
|
function requiresApiKey(labels?: { requires_api_key?: string }) {
|
||||||
return labels?.requires_api_key === 'true'
|
return labels?.requires_api_key === 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skillHubCanonicalUrl(slug: string) {
|
||||||
|
return `https://skillhub.cn/skills/${encodeURIComponent(slug)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExternalUrl(value: string | undefined, slug: string) {
|
||||||
|
const fallbackUrl = skillHubCanonicalUrl(slug)
|
||||||
|
if (!value) {
|
||||||
|
return fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
if (url.protocol === 'https:' && ALLOWED_EXTERNAL_URL_HOSTS.has(url.hostname)) {
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackUrl
|
||||||
|
}
|
||||||
|
|
||||||
function trustFromReports(reports?: Record<string, { status?: string; statusText?: string }>): {
|
function trustFromReports(reports?: Record<string, { status?: string; statusText?: string }>): {
|
||||||
trustState: SkillMarketTrustState
|
trustState: SkillMarketTrustState
|
||||||
trustSummary?: string
|
trustSummary?: string
|
||||||
@ -58,25 +88,30 @@ function trustFromReports(reports?: Record<string, { status?: string; statusText
|
|||||||
export function normalizeSkillHubList(payload: SkillHubListResponse): SkillMarketListResult {
|
export function normalizeSkillHubList(payload: SkillHubListResponse): SkillMarketListResult {
|
||||||
const items = (payload.data?.skills ?? [])
|
const items = (payload.data?.skills ?? [])
|
||||||
.filter((item) => item.slug && item.name)
|
.filter((item) => item.slug && item.name)
|
||||||
.map((item): SkillMarketItem => ({
|
.map((item): SkillMarketItem => {
|
||||||
|
const slug = item.slug!
|
||||||
|
const normalizedUrl = normalizeExternalUrl(item.upstream_url, slug)
|
||||||
|
|
||||||
|
return {
|
||||||
source: 'skillhub',
|
source: 'skillhub',
|
||||||
sourceMode: 'fallback',
|
sourceMode: 'fallback',
|
||||||
slug: item.slug!,
|
slug,
|
||||||
displayName: item.name!,
|
displayName: item.name!,
|
||||||
summary: item.description || item.description_zh || '',
|
summary: item.description || item.description_zh || '',
|
||||||
summaryZh: item.description_zh,
|
summaryZh: item.description_zh,
|
||||||
owner: item.ownerName,
|
owner: item.ownerName,
|
||||||
canonicalUrl: item.upstream_url || `https://skillhub.cn/skills/${item.slug}`,
|
canonicalUrl: normalizedUrl,
|
||||||
upstreamUrl: item.upstream_url,
|
upstreamUrl: normalizedUrl,
|
||||||
version: item.version,
|
version: item.version,
|
||||||
downloads: item.downloads,
|
downloads: item.downloads,
|
||||||
installs: item.installs,
|
installs: item.installs,
|
||||||
stars: item.stars,
|
stars: item.stars,
|
||||||
category: item.category,
|
category: item.category,
|
||||||
requiresApiKey: requiresApiKey(item.labels),
|
requiresApiKey: requiresApiKey(item.labels),
|
||||||
trustState: item.verified ? 'signed' : 'benign',
|
trustState: item.verified ? 'signed' : 'unknown',
|
||||||
installed: false,
|
installed: false,
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
@ -90,6 +125,7 @@ export function normalizeSkillHubDetail(payload: SkillHubDetailResponse): SkillM
|
|||||||
const trust = trustFromReports(payload.securityReports)
|
const trust = trustFromReports(payload.securityReports)
|
||||||
const skill = payload.skill ?? {}
|
const skill = payload.skill ?? {}
|
||||||
const slug = skill.slug || 'unknown'
|
const slug = skill.slug || 'unknown'
|
||||||
|
const normalizedUrl = normalizeExternalUrl(skill.sourceUrl, slug)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: 'skillhub',
|
source: 'skillhub',
|
||||||
@ -99,7 +135,7 @@ export function normalizeSkillHubDetail(payload: SkillHubDetailResponse): SkillM
|
|||||||
summary: skill.summary || skill.summary_zh || '',
|
summary: skill.summary || skill.summary_zh || '',
|
||||||
summaryZh: skill.summary_zh,
|
summaryZh: skill.summary_zh,
|
||||||
owner: payload.owner?.handle || payload.owner?.displayName,
|
owner: payload.owner?.handle || payload.owner?.displayName,
|
||||||
canonicalUrl: skill.sourceUrl || `https://skillhub.cn/skills/${slug}`,
|
canonicalUrl: normalizedUrl,
|
||||||
version: payload.latestVersion?.version,
|
version: payload.latestVersion?.version,
|
||||||
downloads: skill.stats?.downloads,
|
downloads: skill.stats?.downloads,
|
||||||
installs: skill.stats?.installs,
|
installs: skill.stats?.installs,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user