Skip to content

Commit 28c2f38

Browse files
authored
add support for having multiple projects that have the same CPE (#133)
* add support for having multiple projects that have the same CPE * fixup spelling
1 parent 3ab446c commit 28c2f38

File tree

2 files changed

+41
-40
lines changed

2 files changed

+41
-40
lines changed

scripts/search_upstream_advisories.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function main()
1818
# many vulns as possible with these higher-level searches before going one-by-one due to API limits
1919
if startswith(input, "--project")
2020
_, proj= split(input, [' ','='])
21-
vendorproducts = [k for (k,v) in SecurityAdvisories.upstream_projects_by_vendor_product() if v == proj]
21+
vendorproducts = split.(SecurityAdvisories.upstream_projects()[proj]["cpes"], ":", limit=2)
2222
nvds = []
2323
euvds = []
2424
for (vendor, product) in vendorproducts
@@ -127,6 +127,9 @@ function main()
127127

128128
function print_advisory_package_version_details(io, id, adv)
129129
pkgs = SecurityAdvisories.vulnerable_packages(adv)
130+
cpes = unique(Iterators.flatten(keys(entry.source_mapping) for entry in adv.affected if !isnothing(entry.source_mapping)))
131+
ambiguous_cpes = filter(>(1)lengthSecurityAdvisories.upstream_projects_by_cpe, cpes)
132+
130133
affectedsrcidx = something(findlast(x->"affected" in x.fields, adv.jlsec_sources), 1)
131134
html_url = get(adv.jlsec_sources, affectedsrcidx, (;html_url="")).html_url
132135
println(io, "* [$id]($html_url) for packages: ", join("**" .* pkgs .* "**", ", ", ", and "))
@@ -158,6 +161,10 @@ function main()
158161
end
159162
end
160163
end
164+
ambiguous_sources = filter(in(ambiguous_cpes), keys(entry.source_mapping))
165+
for ambig in ambiguous_sources
166+
println(io, " * ⚠ `", ambig, "` might mean a different project; it could be one of ", join("`" .* SecurityAdvisories.upstream_projects_by_cpe(ambig) .* "`", ", ", " or "))
167+
end
161168
end
162169
end
163170

src/common.jl

Lines changed: 33 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -250,42 +250,27 @@ function upstream_projects()
250250
return UPSTREAM_PROJECTS[] = relevant_info
251251
end
252252

253-
# A computed dictionary that maps a (vendor, product) tuple to a known upstream project name
254-
const UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT = Ref{Dict{Tuple{String,String}, String}}()
255-
function upstream_projects_by_vendor_product()
256-
isassigned(UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT) && return UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT[]
257-
d = Dict{Tuple{String,String}, String}()
258-
for (project, deets) in upstream_projects()
259-
for cpe in get(deets, "cpes", [])
260-
v, p = split(cpe, ":", limit=2)
261-
d[(lowercase(v),lowercase(p))] = project
253+
# A computed dictionary that maps a (vendor, product) tuple to known upstream project names
254+
const UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT = Ref{Dict{Tuple{String,String}, Vector{String}}}()
255+
function upstream_projects_by_vendor_product(vendor, product)
256+
if !isassigned(UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT)
257+
d = Dict{Tuple{String,String}, Vector{String}}()
258+
for (project, deets) in upstream_projects()
259+
for cpe in get(deets, "cpes", [])
260+
v, p = split(cpe, ":", limit=2)
261+
union!(get!(Vector{String}, d, (lowercase(v),lowercase(p))), (project,))
262+
end
262263
end
264+
UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT[] = d
263265
end
264-
UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT[] = d
266+
return get(UPSTREAM_PROJECTS_BY_VENDOR_PRODUCT[], (lowercase(vendor),lowercase(product)), String[])
265267
end
268+
upstream_projects_by_cpe(vendorproduct) = upstream_projects_by_vendor_product(split(vendorproduct, ":", limit=2)...)
266269

267270
function packages_with_project(proj)
268271
return [pkgname for (pkgname,versioninfo) in package_components() if any(v->haskey(v,proj), values(versioninfo))]
269272
end
270273

271-
272-
273-
function upstream_versions_used_by_cpe(cpe)
274-
# First find the projects that match the CPE:
275-
matched_projects = [k for (k,v) in upstream_projects() if cpe in get(v, "cpes", [])]
276-
# Then the _upstream_ versions of that project that are used by JLLs
277-
upstream_components = package_components()
278-
versions = []
279-
for (_, pkgversions) in upstream_components
280-
for (_, components) in pkgversions
281-
for (k, v) in components
282-
k in matched_projects && push!(versions, v)
283-
end
284-
end
285-
end
286-
return unique(versions)
287-
end
288-
289274
function package_project_version_map(pkg, proj)
290275
return Dict(v => components[proj] for (v, components) in package_components()[pkg])
291276
end
@@ -348,31 +333,40 @@ function convert_versions(pkg_project_map, vulnerable_range)
348333
versions
349334
end
350335

336+
"""
337+
affected_julia_packages(description, vendorproductversions)
338+
339+
Given some advisory's description an an array of 3-tuples (vendor, product, versionrange)
340+
for which the vulnerability applies, return the vector of the corresponding Julia `PackageVulnerability`s.
341+
"""
351342
function affected_julia_packages(description, vendorproductversions)
352343
pkgs = DefaultDict{String, Any}(()->DefaultDict{String, Any}(()->OrderedDict{String, Any}()))
353-
# There are three reasons why this might return a ["*"] range
344+
# There are four reasons why this might return a ["*"] range
354345
# 1. That's the correct answer
355346
# 2. It's pessimistically returned because we failed to parse the versions reported in the advisory
356347
# 3. It's pessimistically returned because we failed to match a mentioned Julia package to a product
348+
# 4. The upstream component version is unknown — itself a "*" — at the latest Julia package version
357349
julia_like_pkgs_mentioned = union((m.captures[1] for m in eachmatch(r"\b(\w+)\.jl\b", description)),
358350
(m.captures[1]*"_jll" for m in eachmatch(r"\b(\w+)_jll\b", description)))
359351
jlpkgs_mentioned = filter(registry_has_package, julia_like_pkgs_mentioned)
360352
found_match = false
361353
advisory_type = nothing
362354
for (vendor, product, version) in unique(vendorproductversions)
363355
# First check for a known **NON-JULIA-PACKAGE** CPE:
364-
if haskey(upstream_projects_by_vendor_product(), (lowercase(vendor), lowercase(product)))
365-
matched_project = upstream_projects_by_vendor_product()[(lowercase(vendor), lowercase(product))]
366-
found_match = true
356+
upstream_projects = upstream_projects_by_vendor_product(vendor, product)
357+
if !isempty(upstream_projects)
367358
# We have an upstream component! Compute the remapped version range if we can.
368-
matched_pkgs = packages_with_project(matched_project)
369-
r = tryparse(VersionRange, version)
370-
for pkg in matched_pkgs
371-
pkgs[pkg]["$vendor:$product"][version] = isnothing(r) ?
372-
[VersionRange{VersionNumber}("*")] : convert_versions(package_project_version_map(pkg, matched_project), r)
359+
found_match = true
360+
for matched_project in upstream_projects
361+
matched_pkgs = packages_with_project(matched_project)
362+
r = tryparse(VersionRange, version)
363+
for pkg in matched_pkgs
364+
pkgs[pkg]["$vendor:$product"][version] = isnothing(r) ?
365+
[VersionRange{VersionNumber}("*")] : convert_versions(package_project_version_map(pkg, matched_project), r)
366+
end
367+
isnothing(advisory_type) || @assert(advisory_type == "upstream", "advisory directly lists $pkg, but it also finds upstream components")
368+
advisory_type = "upstream"
373369
end
374-
isnothing(advisory_type) || @assert(advisory_type == "upstream", "advisory directly lists $pkg, but it also finds upstream components")
375-
advisory_type = "upstream"
376370
else
377371
if (contains(lowercase(vendor), "julia") || endswith(product, ".jl")) && registry_has_package(chopsuffix(product, ".jl"))
378372
# A vendor or package _looks_ really julia-ish and is in the registry

0 commit comments

Comments
 (0)