Skip to content

Commit da958e2

Browse files
authored
Merge pull request #17 from GHAS-Exercises/fix-normalize-purl-format
Add package name normalization function
2 parents 912babd + 329e4b1 commit da958e2

File tree

1 file changed

+21
-3
lines changed

1 file changed

+21
-3
lines changed

src/malwareMatcher.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ const ecosystemToPurlType: Record<string, string> = {
4141

4242
interface 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+
4457
function 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

5973
function 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

Comments
 (0)