Skip to content

CVE scanning enhancement#8437

Open
stutiibm wants to merge 7 commits into
ppc64le:masterfrom
stutiibm:cve_scanning_enhancement
Open

CVE scanning enhancement#8437
stutiibm wants to merge 7 commits into
ppc64le:masterfrom
stutiibm:cve_scanning_enhancement

Conversation

@stutiibm

Copy link
Copy Markdown
Contributor

Checklist

  • Have you checked and followed all the points mention in the CONTRIBUTING.MD
  • Have you validated script on UBI 9 container
  • Did you run the script(s) on fresh container with set -e option enabled and observe success ?
  • Did you have Legal approvals for patch files ?

stutiibm added 7 commits July 20, 2026 00:37
Adds generalized_wheel_scanner.py — a single-script CVE scanner that runs
inside the build container after auditwheel repair to detect vulnerabilities
in bundled .so files that standard scanners cannot identify (metadata stripped,
hash-renamed filenames).

Scanner is invoked automatically from create_wheel_wrapper.sh after auditwheel
repair completes. Failure is non-blocking — build continues regardless.

Scanning strategy (three lanes + phase 1):

Phase 1 — grype scan directly on extracted wheel directory.
           Results deduplicated against Lane 1 and Lane 2 before final report.

Lane 1  — Walks METADATA/egg-info inside the wheel to find all Python runtime
           dependencies, then queries grype for CVEs.

Lane 2  — Strips auditwheel hash from each bundled .so filename, matches it
           against a live RPM inventory (rpm -q --filesbypkg -a) to identify
           the source package, then queries grype for CVEs.

Lane 3  — Parses the build script (git clone + git checkout lines) to extract
           source-built C/C++ library names and versions. Queries NVD CPE API
           to resolve a unique identifier, then NVD CVE API for findings.
           All .so files not claimed by Lane 2 are attributed to Lane 3 libs.

Other details:
- grype self-installs at runtime (3 retries, 10s delay); non-blocking if it fails
- Shell variable substitution pre-pass handles $VAR / ${VAR:-default} in build scripts
- Layered NVD CPE search: raw repo name first, stripped name as fallback
- Language-binding CPE variants (-python, -java, etc.) filtered out
- Output: single unified JSON report per wheel saved to disk for COS upload

Fields in final_report:
  total_unique_cve  — globally distinct CVE IDs across all lanes
  total_cve         — per-library occurrence count
  cpe_resolved      — true = unique NVD CPE found; false = flagged, no CVE query
…per.sh

Adds the generalized_wheel_scanner.py invocation block to create_wheel_wrapper.sh.
The scanner is called after auditwheel repair completes and before SHA generation,
passing the repaired wheel and the original build script as arguments.

The call is non-blocking — if the scanner fails or the script is not found,
a warning is printed and the build continues normally.

Scanner invocation:
  if [ -f "generalized_wheel_scanner.py" ]; then
      python generalized_wheel_scanner.py "${wheel_final}" "${BUILD_SCRIPT_PATH}"
  fi

Arguments passed:
  $1 — repaired wheel file (manylinux-tagged .whl)
  $2 — original build script path (used by Lane 3 to parse source-built libs)

Output: <wheel_stem>_cve_report.json written to the build working directory,
picked up and uploaded to COS by the workflow upload step.
Adds an "Upload CVE report to COS" step to all five wheel build jobs
(py310, py311, py312, py313, py314) in the build workflow.

The step runs after the wheel artifact upload and pushes the
<wheel_stem>_cve_report.json file produced by generalized_wheel_scanner.py
to the same COS bucket and path as the existing build logs:

  ose-power-toolci-bucket-production/<PACKAGE_NAME>/<VERSION>/<wheel_stem>_cve_report.json

Example for numpy 2.2.5 (py312):
  ose-power-toolci-bucket-production/numpy/2.2.5/
    numpy-2.2.5-cp312-cp312-manylinux_2_34_ppc64le_cve_report.json

Design decisions:
- Reuses existing upload_file.sh — no new upload infrastructure needed
- Non-blocking — missing report only prints a warning, build continues
- One report per wheel per Python version, all landing under the same
  PACKAGE_NAME/VERSION prefix, no extra collector job required
- No changes to create_wheel_wrapper.sh or any scanner script
Added CVE report renaming logic after wheel post-processing.
@stutiibm
stutiibm marked this pull request as ready for review July 23, 2026 13:37
}

# Grype install URL
_GRYPE_INSTALL_URL = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a step in workflow to install grype, instead of installing it in the python script?

try:
# Step 1: one call — all package names + versions
meta_result = subprocess.run(
['rpm', '-qa', '--queryformat', '%{NAME}|%{VERSION}-%{RELEASE}\n'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you try with, it will give package name, version and files all in one go.

rpm -qa --queryformat 'PKG:%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n[%{FILENAMES}\n]'

print(" Detected RPM-based system")
inventory['packages'] = self._get_rpm_packages()
# Check if DEB-based system
elif shutil.which('dpkg'):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to prepare inventory for DEB based system, considering that the build happens on RHEL container?

try:
# Get all packages
result = subprocess.run(
['dpkg-query', '-W', '-f=${Package}|${Version}\n'],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Debian systems, dpkg stores the list of files for every installed package in plain text files under /var/lib/dpkg/info/.list. Reading these files directly in Python is better compared to executing the dpkg-query and then dpkg for each package.

'basename': Path(file_path).name
})
except:
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use except Exception: here instead of passing silently.

# libgfortran-2758a8fd.so.5.0.0 -> libgfortran
# libopenblasp-r0-a8f83a82.3.32.so -> libopenblasp-r0
# libabsl_base-eb206faa.so.2401.0.0 -> libabsl_base
base_name = re.sub(r'-[a-f0-9]{8,}', '', bundled_name) # Remove hex hash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be replaced with single regex?

site_dirs = []
try:
result = subprocess.run(
['find', '/', '-name', 'site-packages', '-type', 'd',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the following be used instead?

import site
dirs = site.getsitepackages() + [site.getusersitepackages()] 

queue = [wheel_pkg_name]

while queue:
pkg = queue.pop(0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use deque instead.

queue = [wheel_pkg_name]

while queue:
pkg = queue.pop(0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use deque instead.

if result.returncode == 0 and shutil.which('grype'):
print(" grype installed successfully.")
return True
print(f" Attempt {attempt} failed: {result.stderr.strip()[:200]}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use constant instead of magic number 200.

with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except Exception as e:
print(f" Warning: NVD API call failed ({url}): {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that this does not print the Key here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants