diff --git a/app/composables/npm/useAlgoliaSearch.ts b/app/composables/npm/useAlgoliaSearch.ts index 02ce461517..b678d7e6ab 100644 --- a/app/composables/npm/useAlgoliaSearch.ts +++ b/app/composables/npm/useAlgoliaSearch.ts @@ -50,6 +50,7 @@ interface AlgoliaHit { deprecated: boolean | string isDeprecated: boolean license: string | null + isSecurityHeld: boolean } const ATTRIBUTES_TO_RETRIEVE = [ @@ -67,6 +68,7 @@ const ATTRIBUTES_TO_RETRIEVE = [ 'deprecated', 'isDeprecated', 'license', + 'isSecurityHeld', ] const EXISTENCE_CHECK_ATTRS = ['name'] @@ -90,6 +92,7 @@ function hitToSearchResult(hit: AlgoliaHit): NpmSearchResult { email: owner.email, })) : [], + isSecurityHeld: hit.isSecurityHeld, }, searchScore: 0, downloads: { diff --git a/app/pages/search.vue b/app/pages/search.vue index 6ec32050a0..58805476c6 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -86,7 +86,7 @@ const { settings } = useSettings() /** * Reorder results to put exact package name match at the top, - * and optionally filter out platform-specific packages. + * and optionally filter out platform-specific packages or security holding packages. */ const visibleResults = computed(() => { const raw = rawVisibleResults.value @@ -94,6 +94,9 @@ const visibleResults = computed(() => { let objects = raw.objects + // Filter out "Security holding package" packages taken down by npm registry + objects = objects.filter(r => !r.package.isSecurityHeld) + // Filter out platform-specific packages if setting is enabled if (settings.value.hidePlatformPackages) { objects = objects.filter(r => !isPlatformSpecificPackage(r.package.name)) diff --git a/shared/types/npm-registry.ts b/shared/types/npm-registry.ts index 2849497ee9..6ffe94aded 100644 --- a/shared/types/npm-registry.ts +++ b/shared/types/npm-registry.ts @@ -189,6 +189,8 @@ export interface NpmSearchPackage { publisher?: NpmSearchPublisher maintainers?: NpmPerson[] license?: string + /** Algolia-only: package is an npm-owned security-holder takedown */ + isSecurityHeld?: boolean } /** diff --git a/test/fixtures/algolia/search/security-holder.json b/test/fixtures/algolia/search/security-holder.json new file mode 100644 index 0000000000..3c0bf8911a --- /dev/null +++ b/test/fixtures/algolia/search/security-holder.json @@ -0,0 +1,69 @@ +[ + { + "name": "vuln-npm", + "downloadsLast30Days": 0, + "downloadsRatio": 0, + "popular": false, + "version": "0.0.1-security", + "description": "security holding package", + "repository": { + "type": "git", + "url": "npm/security-holder", + "project": "security-holder", + "user": "npm", + "host": "github.com", + "path": "", + "branch": "master" + }, + "deprecated": false, + "isDeprecated": false, + "isSecurityHeld": true, + "homepage": null, + "license": null, + "keywords": [], + "modified": 1692592458394, + "owners": [ + { + "email": "npm@npmjs.com", + "name": "npm", + "avatar": "https://gravatar.com/avatar/46d8d00e190be647053f7d97fd0478e4", + "link": "https://www.npmjs.com/~npm" + } + ], + "objectID": "vuln-npm" + }, + { + "name": "npmx-connector", + "downloadsLast30Days": 1047, + "downloadsRatio": 0, + "popular": false, + "version": "0.9.0", + "description": "Local connector for npmx.dev - enables authenticated npm operations from the web UI", + "repository": { + "type": "git", + "url": "https://github.com/npmx-dev/npmx.dev", + "project": "npmx.dev", + "user": "npmx-dev", + "host": "github.com", + "path": "", + "head": "1f574e52f494032683c1aec7f64f6de72d4413a0", + "branch": "1f574e52f494032683c1aec7f64f6de72d4413a0" + }, + "deprecated": false, + "isDeprecated": false, + "isSecurityHeld": false, + "homepage": "https://npmx.dev", + "license": "MIT", + "keywords": [], + "modified": 1776194979856, + "owners": [ + { + "name": "danielroe", + "email": "daniel@roe.dev", + "avatar": "https://gravatar.com/avatar/f366ee6556b7307a1a2a253c8fb842ca", + "link": "https://www.npmjs.com/~danielroe" + } + ], + "objectID": "npmx-connector" + } +] diff --git a/test/nuxt/composables/use-algolia-search.spec.ts b/test/nuxt/composables/use-algolia-search.spec.ts new file mode 100644 index 0000000000..1e0113691e --- /dev/null +++ b/test/nuxt/composables/use-algolia-search.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it, vi } from 'vitest' +import fixture from '~~/test/fixtures/algolia/search/security-holder.json' + +const mockSearch = vi.fn() +vi.mock('algoliasearch/lite', () => ({ + liteClient: () => ({ search: mockSearch }), +})) + +describe('useAlgoliaSearch', () => { + it('maps isSecurityHeld through to NpmSearchResult.package', async () => { + mockSearch.mockResolvedValue({ + results: [{ hits: fixture, nbHits: fixture.length }], + }) + + const { search } = useAlgoliaSearch() + const { objects } = await search('') + + const bad = objects.find(o => o.package.name === 'vuln-npm') + const good = objects.find(o => o.package.name === 'npmx-connector') + + expect(bad?.package.isSecurityHeld).toBe(true) + expect(good?.package.isSecurityHeld).toBe(false) + + const filtered = objects.filter(o => !o.package.isSecurityHeld).map(o => o.package.name) + expect(filtered).toEqual(['npmx-connector']) + }) +})