Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions .github/scripts/generate_3rdparty_sbom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Generate a small CycloneDX JSON SBOM for OpenCV bundled (vendored) deps under 3rdparty/.

Important constraint:
- No curated allow-list of library names. This script *discovers* candidates by scanning the
3rdparty directory tree and extracting version-like identifiers from common metadata files.

Limitations:
- Vendored C/C++ sources often do not include a single authoritative version string; this uses
heuristics and therefore may miss some libraries or pick a less-than-perfect version string.
"""

import argparse
import json
import re
from pathlib import Path
from typing import Optional, Iterable


REPO_ROOT = Path(__file__).resolve().parents[2]


def read_text(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="ignore")


def first_match(text: str, patterns: list[str]) -> Optional[str]:
for pat in patterns:
m = re.search(pat, text, flags=re.MULTILINE)
if m:
return m.group(1)
return None


VERSION_PATTERNS = [
# C preprocessor macros: "#define FOO_VERSION "1.2.3""
r'^\s*#define\s+\w*VERSION\w*\s+"([0-9]+(?:\.[0-9]+){1,3})"\s*$',
# Generic comment header patterns: "version 1.2.3" or "v1.2.3 released"
r'\bversion\s+v?([0-9]+(?:\.[0-9]+){1,3})\b',
r'\bv([0-9]+(?:\.[0-9]+){1,3})\s+released\b',
# CMake: set(VERSION 1.2.3) / project(... VERSION 1.2.3)
r'^\s*set\s*\(\s*\w*VERSION\w*\s+"?([0-9]+(?:\.[0-9]+){1,3})"?\s*\)\s*$',
r'^\s*project\s*\(.*\bVERSION\s+([0-9]+(?:\.[0-9]+){1,3})\b.*\)\s*$',
]


def int_version_to_semver(n: int) -> Optional[str]:
# Heuristic for packed integer versions like protobuf:
# major * 10^6 + minor * 10^3 + micro
if n < 1_000_000 or n > 9_999_999:
return None
major = n // 1_000_000
minor = (n // 1_000) % 1_000
micro = n % 1_000
return f"{major}.{minor}.{micro}"


def extract_versions_from_text(text: str) -> list[str]:
found: list[str] = []
for pat in VERSION_PATTERNS:
for m in re.finditer(pat, text, flags=re.IGNORECASE | re.MULTILINE):
v = m.group(1)
if v and v not in found:
found.append(v)
# Packed integer version macro: "#define ..._VERSION 3019001"
for m in re.finditer(r"^\s*#define\s+\w*VERSION\w*\s+(\d{7})\s*$", text, flags=re.MULTILINE):
sv = int_version_to_semver(int(m.group(1)))
if sv and sv not in found:
found.append(sv)
return found


def candidate_files(root: Path) -> Iterable[Path]:
# Deterministic selection of "likely to contain version" files, to avoid scanning everything.
patterns = [
"CMakeLists.txt",
"configure.ac",
"configure.in",
"VERSION",
"version",
"version.txt",
"ChangeLog",
"CHANGES",
"NEWS",
"README",
"README.md",
"README.txt",
]
picked: list[Path] = []
# 1) exact-ish filename hits at shallow depth
for p in root.rglob("*"):
if not p.is_file():
continue
if p.stat().st_size > 256 * 1024:
continue
name = p.name
if name in patterns or "version" in name.lower() or "changelog" in name.lower():
picked.append(p)
# Prefer closer files, then lexicographic for determinism; cap to keep runtime bounded.
picked.sort(key=lambda x: (len(x.relative_to(root).parts), str(x)))
return picked[:200]


def infer_version_for_dir(d: Path) -> Optional[str]:
versions: list[str] = []
for p in candidate_files(d):
try:
text = read_text(p)
except Exception:
continue
for v in extract_versions_from_text(text):
versions.append(v)
if not versions:
return None
# Heuristic: prefer semver-like with 3 dots, then 2 dots, then 1 dot; stable by first appearance.
versions.sort(key=lambda v: (-v.count("."), -len(v)))
return versions[0]


def component(name: str, version: Optional[str], purl: Optional[str] = None) -> dict:
c = {"type": "library", "name": name}
if version:
c["version"] = version
if purl:
c["purl"] = purl
return c


def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--output", required=True, help="Output CycloneDX JSON file path")
args = ap.parse_args()

thirdparty = REPO_ROOT / "3rdparty"
comps: list[dict] = []
for child in sorted([p for p in thirdparty.iterdir() if p.is_dir()]):
name = child.name
version = infer_version_for_dir(child)
if not version:
continue
comps.append(component(name, version, f"pkg:generic/{name}"))

# Drop components with no version only if we couldn't determine anything useful.
bom = {
"$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": {
"component": {"type": "file", "name": "3rdparty"},
"tools": {"components": [{"type": "application", "name": "generate_3rdparty_sbom.py"}]},
},
"components": comps,
}

out = Path(args.output)
out.write_text(json.dumps(bom, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"Wrote {out} with {len(comps)} components")
return 0


if __name__ == "__main__":
raise SystemExit(main())


42 changes: 25 additions & 17 deletions .github/workflows/sonar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,38 @@
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Install sonar-scanner and build-wrapper
env:
SONAR_HOST_URL: ${{secrets.SONAR_HOST_URL}}
uses: SonarSource/sonarqube-github-c-cpp@v1
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install Build Wrapper
uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v7.0.0

Check warning on line 20 in .github/workflows/sonar.yaml

View check run for this annotation

Sonar-Nautilus / SonarQube Code Analysis

Use full commit SHA hash for this dependency.

[S7637] Using external GitHub actions and workflows without a commit reference is security-sensitive See more on https://nautilus.sonarqube.org/project/issues?id=SonarSource-Demos_opencv&pullRequest=2&issues=2ba0e298-a136-43c7-b753-fdcc16bae6e0&open=2ba0e298-a136-43c7-b753-fdcc16bae6e0
- name: Run build-wrapper
run: |
rm -rf build
mkdir build
cd build
cmake ..
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DBUILD_JAVA=OFF -DBUILD_opencv_java=OFF .
cd ..
- name: Run sonar-scanner
- name: Generate SBOM for bundled deps (CycloneDX)
shell: bash
run: |
set -euo pipefail
python3 .github/scripts/generate_3rdparty_sbom.py --output sbom-3rdparty.cdx.json
- name: Dump SBOM to logs
shell: bash
run: |
set -euo pipefail
echo "SBOM file: sbom-3rdparty.cdx.json"
echo "SBOM size (bytes): $(wc -c < sbom-3rdparty.cdx.json)"
echo "----- BEGIN SBOM (truncated to first 200000 bytes) -----"
head -c 200000 sbom-3rdparty.cdx.json || true
echo
echo "----- END SBOM -----"
- name: Run SonarQube scan
uses: SonarSource/sonarqube-scan-action@v7.0.0

Check warning on line 45 in .github/workflows/sonar.yaml

View check run for this annotation

Sonar-Nautilus / SonarQube Code Analysis

Use full commit SHA hash for this dependency.

[S7637] Using external GitHub actions and workflows without a commit reference is security-sensitive See more on https://nautilus.sonarqube.org/project/issues?id=SonarSource-Demos_opencv&pullRequest=2&issues=401ccf95-f1dc-4e5b-9bef-35eb0ef14a3c&open=401ccf95-f1dc-4e5b-9bef-35eb0ef14a3c
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{secrets.SONAR_HOST_URL}}
run: |
sonar-scanner
with:
args: >
-X
-Dsonar.sca.enabled=true
-Dsonar.sca.sbomImportPaths=sbom-3rdparty.cdx.json
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sonar.projectKey=SonarSource-Demos_opencv_552f564f-bcdb-47eb-abb2-4ba7feb428ee
sonar.projectKey=SonarSource-Demos_opencv
sonar.cfamily.compile-commands=build/compile_commands.json
sonar.exclusions=**/*.java