From 7d6f2279d7d325fa90e2878581bf16e891e85127 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Fri, 30 Oct 2026 10:35:23 -0600 Subject: [PATCH] Report wheel-vendored libbrotlidec as a bundled copy of its package A libbrotlidec that a Python wheel vendored (auditwheel and delvewheel's .libs, delocate's /.dylibs) was reported as the system decoder library, with "update the system Brotli package" as the fix. An OS update never touches that copy. It is now a bundled-decoder finding for the python-wheel product that names the package and says to upgrade it. Only paths inside site-packages, dist-packages or a wheel count, so a libtool .libs build directory and other system paths stay decoder-library. Verdicts are unchanged. Co-Authored-By: Claude Opus 5.5 --- detectors/brotli/internal/assess/assess.go | 50 +++++++++++++++++-- .../brotli/internal/assess/assess_test.go | 12 +++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/detectors/brotli/internal/assess/assess.go b/detectors/brotli/internal/assess/assess.go index 8dbf038..dee3a6f 100644 --- a/detectors/brotli/internal/assess/assess.go +++ b/detectors/brotli/internal/assess/assess.go @@ -53,7 +53,11 @@ const ( Envoy Product = "envoy" Electron Product = "electron" GoBrotli Product = "go-andybalholm-brotli" - Other Product = "other" + // PyWheel is a libbrotlidec that a Python wheel vendored next to its + // own code (auditwheel's and delvewheel's .libs, delocate's + // /.dylibs): updating the system package does not touch it. + PyWheel Product = "python-wheel" + Other Product = "other" ) // Finding is the verdict on one file. @@ -129,6 +133,36 @@ func identify(p string, data []byte) (Product, fingerprint.Version, bool) { return Other, fingerprint.Version{}, false } +// wheelPackage names the Python package that vendored the library at p, from +// the directory a wheel repair tool put it in: .libs (auditwheel on +// Linux, delvewheel on Windows) names the distribution, /.dylibs +// (delocate on macOS) the import package, so imported reports which. Only +// paths inside site-packages, dist-packages or a wheel count, so a libtool +// .libs build directory stays the system library. +func wheelPackage(p string) (name string, imported, ok bool) { + p = strings.ReplaceAll(p, `\`, "/") + lower := strings.ToLower(p) + if !strings.Contains(lower, "/site-packages/") && !strings.Contains(lower, "/dist-packages/") && !strings.Contains(lower, ".whl!/") { + return "", false, false + } + if i := strings.LastIndex(p, "!/"); i >= 0 { + p = "/" + p[i+2:] + } + dir := path.Dir(p) + parent := path.Base(dir) + switch { + case parent == ".dylibs": + pkg := path.Base(path.Dir(dir)) + if pkg == "." || pkg == "/" || pkg == "site-packages" || pkg == "dist-packages" { + return "", false, false + } + return pkg, true, true + case strings.HasSuffix(parent, ".libs") && parent != ".libs": + return strings.TrimSuffix(parent, ".libs"), false, true + } + return "", false, false +} + // update returns the fix for a product line: "Update Node.js 24.22.0 to // 24.22.1", or, for a line with no fixed release, the lines to move to. func update(name string, v fingerprint.Version, known bool, fixed map[int]fingerprint.Version, all string) string { @@ -184,13 +218,23 @@ func label(p Product, v fingerprint.Version, known bool) string { func File(p string, data []byte) (Finding, bool) { prod, v, known := identify(p, data) f := Finding{Kind: Bundled, Path: p, Product: prod, Fix: fixFor(prod, v, known)} + what := label(prod, v, known) if prod == LibBrotliDec { - f.Kind = Library + if pkg, imported, ok := wheelPackage(p); ok { + prod, f.Product = PyWheel, PyWheel + who := "the " + pkg + " Python package" + if imported { + who = "the Python package that provides " + pkg + } + f.Fix = "Upgrade " + who + " to a release whose wheel bundles Brotli 1.0.10, 1.1.1, 1.2.1 or later. Updating the system Brotli package does not change this copy." + what = " (vendored in " + who + ")" + } else { + f.Kind = Library + } } if known { f.Version = v.String() } - what := label(prod, v, known) judge := func(s Status, reason string) { f.Status, f.Reason = s, reason if s == Fixed { diff --git a/detectors/brotli/internal/assess/assess_test.go b/detectors/brotli/internal/assess/assess_test.go index 36c94ef..903497f 100644 --- a/detectors/brotli/internal/assess/assess_test.go +++ b/detectors/brotli/internal/assess/assess_test.go @@ -48,6 +48,18 @@ func TestFile(t *testing.T) { assess.Fixed, assess.Bundled, assess.Node, ""}, {"node 25 stripped", "/usr/local/bin/node", testbin.ELF(decoder, node("25.9.0")), assess.Vulnerable, assess.Bundled, assess.Node, "Node.js 25 has no fixed release"}, + {"pillow wheel on linux", "/usr/local/lib/python3.14/site-packages/pillow.libs/libbrotlidec-7e5462ba.so.1.2.0", testbin.ELF(decoder, testbin.Table("1.2.0")), + assess.Vulnerable, assess.Bundled, assess.PyWheel, "Upgrade the pillow Python package"}, + {"pillow wheel fixed", "/usr/lib/python3/dist-packages/pillow.libs/libbrotlidec-0a1b2c3d.so.1.2.1", testbin.ELF(decoder, testbin.Table("1.2.1")), + assess.Fixed, assess.Bundled, assess.PyWheel, ""}, + {"pillow wheel on macos", "/Users/a/.venv/lib/python3.13/site-packages/PIL/.dylibs/libbrotlidec.1.2.0.dylib", testbin.ELF(decoder, testbin.Table("1.2.0")), + assess.Vulnerable, assess.Bundled, assess.PyWheel, "Upgrade the Python package that provides PIL"}, + {"wheel archive member", "/srv/wheels/pillow-13.0.0-cp314-manylinux_2_28_x86_64.whl!/pillow.libs/libbrotlidec-7e5462ba.so.1.2.0", testbin.ELF(decoder, testbin.Table("1.2.0")), + assess.Vulnerable, assess.Bundled, assess.PyWheel, "Updating the system Brotli package does not change this copy"}, + {"libtool build dir stays system", "/build/brotli-1.2.0/.libs/libbrotlidec.so.1.2.0", testbin.ELF(decoder, testbin.Table("1.2.0")), + assess.Vulnerable, assess.Library, assess.LibBrotliDec, "system Brotli package"}, + {"foo.libs outside python stays system", "/opt/vendor/foo.libs/libbrotlidec.so.1.2.0", testbin.ELF(decoder, testbin.Table("1.2.0")), + assess.Vulnerable, assess.Library, assess.LibBrotliDec, "system Brotli package"}, {"wheel member", "/srv/wheels/brotli-1.2.0-cp313.whl!/_brotli.cpython-313-x86_64-linux-gnu.so", testbin.Executable(testbin.Table("1.2.0")), assess.Vulnerable, assess.Bundled, assess.PyBrotli, "Brotli Python package to 1.2.1"}, {"brotlicffi", "/app/.venv/lib/python3.13/site-packages/brotlicffi/_brotlicffi.abi3.so", testbin.Executable(testbin.Table("1.2.0")),