mirror of
https://github.com/open-webui/open-webui.git
synced 2026-04-25 17:15:16 +02:00
195 lines
5.7 KiB
Bash
Executable File
195 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# generate-sbom.sh — Generate a clean CycloneDX SBOM using Syft
|
|
#
|
|
# Produces a single SBOM from resolved manifests only — no directory scanning,
|
|
# no venv pollution, no local state. Works identically locally and in CI.
|
|
#
|
|
# How it works:
|
|
# 1. Python: uv pip compile resolves all transitive deps from requirements.txt
|
|
# 2. JavaScript: package-lock.json already contains the full resolved tree
|
|
# 3. Syft scans these resolved files, not the filesystem
|
|
#
|
|
# Usage:
|
|
# ./scripts/generate-sbom.sh # generate sbom.cdx.json from manifests
|
|
# ./scripts/generate-sbom.sh docker # generate from Docker image (best license coverage)
|
|
# ./scripts/generate-sbom.sh docker IMG # generate from a specific image
|
|
# ./scripts/generate-sbom.sh validate # validate existing SBOM
|
|
#
|
|
# Requirements:
|
|
# - syft (brew install syft)
|
|
# - uv (brew install uv)
|
|
#
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
DIM='\033[2m'
|
|
BOLD='\033[1m'
|
|
RESET='\033[0m'
|
|
|
|
info() { echo -e "${BOLD}${GREEN}▸${RESET} $1"; }
|
|
warn() { echo -e "${BOLD}${RED}▸${RESET} $1"; }
|
|
dim() { echo -e "${DIM} $1${RESET}"; }
|
|
|
|
OUTPUT="$ROOT_DIR/sbom.cdx.json"
|
|
|
|
check_deps() {
|
|
local missing=()
|
|
command -v syft &>/dev/null || missing+=("syft")
|
|
command -v uv &>/dev/null || missing+=("uv")
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
warn "Missing: ${missing[*]}. Install with: brew install ${missing[*]}"
|
|
exit 1
|
|
fi
|
|
dim "Using $(syft --version), $(uv --version)"
|
|
}
|
|
|
|
generate() {
|
|
info "Generating SBOM from resolved manifests..."
|
|
check_deps
|
|
|
|
local VERSION
|
|
VERSION="$(python3 -c "import json; print(json.load(open('$ROOT_DIR/package.json'))['version'])")"
|
|
|
|
local WORK_DIR
|
|
WORK_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "$WORK_DIR"' RETURN
|
|
|
|
# --- Python: resolve all transitive deps without installing ---
|
|
dim "Resolving Python transitive deps (uv pip compile)..."
|
|
uv pip compile "$ROOT_DIR/backend/requirements.txt" \
|
|
--python-version 3.11 \
|
|
--quiet \
|
|
> "$WORK_DIR/requirements-resolved.txt" 2>/dev/null
|
|
|
|
# --- JavaScript: package-lock.json is already fully resolved ---
|
|
if [[ -f "$ROOT_DIR/package-lock.json" ]]; then
|
|
cp "$ROOT_DIR/package-lock.json" "$WORK_DIR/package-lock.json"
|
|
# Syft needs package.json alongside the lockfile
|
|
cp "$ROOT_DIR/package.json" "$WORK_DIR/package.json"
|
|
else
|
|
warn "package-lock.json not found — JS deps will be skipped"
|
|
fi
|
|
|
|
# --- Scan only the resolved files ---
|
|
dim "Scanning resolved manifests with Syft..."
|
|
syft scan "dir:$WORK_DIR" \
|
|
--output "cyclonedx-json=$OUTPUT" \
|
|
--source-name open-webui \
|
|
--source-version "$VERSION" \
|
|
--quiet
|
|
|
|
# Print summary
|
|
python3 -c "
|
|
import json
|
|
with open('$OUTPUT') as f:
|
|
data = json.load(f)
|
|
comps = data.get('components', [])
|
|
py = [c for c in comps if 'pypi' in c.get('purl', '')]
|
|
js = [c for c in comps if 'npm' in c.get('purl', '')]
|
|
with_lic = sum(1 for c in comps if c.get('licenses'))
|
|
print(f' {len(comps)} total ({len(py)} Python, {len(js)} JavaScript)')
|
|
print(f' {with_lic}/{len(comps)} with license info')
|
|
print(f' Serial: {data.get(\"serialNumber\", \"none\")}')
|
|
print(f' Timestamp: {data.get(\"metadata\", {}).get(\"timestamp\", \"none\")}')
|
|
"
|
|
|
|
info "SBOM written → sbom.cdx.json"
|
|
}
|
|
|
|
generate_docker() {
|
|
local IMAGE="${1:-ghcr.io/open-webui/open-webui:latest}"
|
|
info "Generating SBOM from Docker image: $IMAGE"
|
|
|
|
if ! command -v syft &>/dev/null; then
|
|
warn "syft is not installed. Install with: brew install syft"
|
|
exit 1
|
|
fi
|
|
|
|
dim "Pulling and scanning image..."
|
|
syft scan "docker:$IMAGE" \
|
|
--output "cyclonedx-json=$OUTPUT" \
|
|
--quiet
|
|
|
|
python3 -c "
|
|
import json
|
|
with open('$OUTPUT') as f:
|
|
data = json.load(f)
|
|
comps = data.get('components', [])
|
|
with_lic = sum(1 for c in comps if c.get('licenses'))
|
|
print(f' {len(comps)} total components')
|
|
print(f' {with_lic}/{len(comps)} with license info ({round(with_lic/max(len(comps),1)*100)}%)')
|
|
"
|
|
|
|
info "SBOM written → sbom.cdx.json"
|
|
}
|
|
|
|
validate() {
|
|
info "Validating SBOM..."
|
|
|
|
python3 -c "
|
|
import json, sys
|
|
|
|
try:
|
|
with open('$OUTPUT') as f:
|
|
data = json.load(f)
|
|
except FileNotFoundError:
|
|
print(' ✗ sbom.cdx.json not found — run ./scripts/generate-sbom.sh first')
|
|
sys.exit(1)
|
|
|
|
issues = []
|
|
if data.get('bomFormat') != 'CycloneDX':
|
|
issues.append('Not CycloneDX format')
|
|
if not data.get('specVersion'):
|
|
issues.append('Missing specVersion')
|
|
if not data.get('serialNumber'):
|
|
issues.append('Missing serial number')
|
|
|
|
components = data.get('components', [])
|
|
|
|
# Check for phantom local packages
|
|
phantoms = []
|
|
for c in components:
|
|
for ref in c.get('externalReferences', []):
|
|
url = ref.get('url', '')
|
|
if 'file://' in url and '/Users/' in url:
|
|
phantoms.append(c['name'])
|
|
if phantoms:
|
|
issues.append(f'Phantom local packages: {phantoms}')
|
|
|
|
with_lic = sum(1 for c in components if c.get('licenses'))
|
|
lic_pct = round(with_lic / max(len(components), 1) * 100)
|
|
|
|
if issues:
|
|
print(f' ✗ {len(components)} components, {lic_pct}% licensed')
|
|
for i in issues:
|
|
print(f' ✗ {i}')
|
|
sys.exit(1)
|
|
else:
|
|
print(f' ✓ {len(components)} components, {lic_pct}% licensed — PASS')
|
|
"
|
|
}
|
|
|
|
# --- Main ---
|
|
cd "$ROOT_DIR"
|
|
TARGET="${1:-generate}"
|
|
|
|
case "$TARGET" in
|
|
generate) generate ;;
|
|
docker) generate_docker "${2:-}" ;;
|
|
validate) validate ;;
|
|
*)
|
|
warn "Unknown target: $TARGET"
|
|
echo "Usage: $0 [generate|docker [IMAGE]|validate]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo ""
|
|
info "Done."
|