@@ -41,6 +41,19 @@ const ecosystemToPurlType: Record<string, string> = {
4141
4242interface ParsedPurl { type : string ; name : string ; version : string | null }
4343
44+ /**
45+ * Normalize a package name by decoding URL-encoded characters.
46+ * This ensures that both @scope/package and %40scope/package are treated identically.
47+ */
48+ function normalizePackageName ( name : string ) : string {
49+ try {
50+ return decodeURIComponent ( name ) ;
51+ } catch {
52+ // If decoding fails, return as-is
53+ return name ;
54+ }
55+ }
56+
4457function parsePurl ( p : string ) : ParsedPurl | null {
4558 // Basic PURL format: pkg:type/name@version (ignore qualifiers/subpath for matching)
4659 if ( ! p . startsWith ( "pkg:" ) ) return null ;
@@ -53,7 +66,8 @@ function parsePurl(p: string): ParsedPurl | null {
5366 const type = main . slice ( 0 , slashIdx ) . toLowerCase ( ) ;
5467 const name = main . slice ( slashIdx + 1 ) ;
5568 if ( ! type || ! name ) return null ;
56- return { type, name, version } ;
69+ // Normalize the name to handle URL-encoded characters like %40 -> @
70+ return { type, name : normalizePackageName ( name ) , version } ;
5771}
5872
5973function versionInRange ( ecosystem : string , version : string | null , range : string | null ) : boolean {
@@ -107,7 +121,9 @@ export function matchMalware(advisories: MalwareAdvisoryNode[], sboms: Repositor
107121 }
108122 for ( const vuln of adv . vulnerabilities ) {
109123 if ( ! vuln . name || ! vuln . ecosystem ) continue ;
110- const key = `${ vuln . ecosystem } ::${ vuln . name } ` . toLowerCase ( ) ;
124+ // Normalize the advisory name to handle both @scope /package and %40scope/package
125+ const normalizedName = normalizePackageName ( vuln . name ) ;
126+ const key = `${ vuln . ecosystem } ::${ normalizedName } ` . toLowerCase ( ) ;
111127 const arr = index . get ( key ) || [ ] ;
112128 if ( ! arr . includes ( adv ) ) arr . push ( adv ) ;
113129 index . set ( key , arr ) ;
@@ -156,7 +172,9 @@ export function matchMalware(advisories: MalwareAdvisoryNode[], sboms: Repositor
156172 for ( const adv of candidateAdvisories ) {
157173 for ( const vuln of adv . vulnerabilities ) {
158174 if ( vuln . ecosystem !== ecosystem ) continue ;
159- if ( vuln . name ?. toLowerCase ( ) !== parsed . name . toLowerCase ( ) ) continue ;
175+ // Normalize both names before comparison to handle URL encoding differences
176+ const normalizedVulnName = normalizePackageName ( vuln . name || "" ) ;
177+ if ( normalizedVulnName . toLowerCase ( ) !== parsed . name . toLowerCase ( ) ) continue ;
160178 if ( ! versionInRange ( ecosystem , version , vuln . vulnerableVersionRange ) ) continue ;
161179 const dedupeKey = `${ adv . ghsaId } @@${ purlStr } ` ;
162180 if ( seen . has ( dedupeKey ) ) continue ;
0 commit comments