cdxgen (CycloneDX Generator) is the OWASP-backed, open-source tool for generating CycloneDX software bill of materials files across more than twenty language ecosystems and container image formats. It resolves dependency trees from package manager lockfiles (package-lock.json, poetry.lock, go.sum, pom.xml, Gemfile.lock, and others), constructs a hierarchical component inventory with PURL identifiers, and outputs standards-compliant CycloneDX 1.4/1.5 JSON or XML. cdxgen is commonly embedded as the first step in software supply chain security pipelines — its output is consumed by vulnerability scanners (Bomber, Grype), compliance platforms (Dependency-Track), and SLSA attestation tools. When cdxgen silently generates an empty or incomplete SBOM — because the lockfile was missing, the container image pull failed, or an unsupported ecosystem was encountered — all downstream tools receive truncated inventory and their vulnerability findings are meaningless. Vigilmon gives you external monitoring for cdxgen process availability, SBOM generation success rates, component count completeness, schema validity, and per-ecosystem coverage so you catch generation failures before they propagate to your vulnerability analysis and compliance pipelines.
What You'll Set Up
- cdxgen binary and Node.js runtime availability
- SBOM generation success rate per project and ecosystem
- Component count completeness checks (empty or suspiciously small SBOMs)
- CycloneDX schema validation and PURL integrity checks
- Container image SBOM generation availability
Prerequisites
- cdxgen installed:
npm install -g @cyclonedx/cdxgenor via containerghcr.io/cyclonedx/cdxgen - Node.js 18+ (cdxgen requires a recent LTS version)
- Access to your CI pipeline log output or a results webhook
- Optional:
cyclonedx-clifor schema validation steps - A free Vigilmon account
Step 1: Monitor cdxgen Binary and Runtime Availability
cdxgen is distributed as a Node.js package — its availability depends on both the cdxgen binary being in PATH and the underlying Node.js runtime being functional. In CI environments it is often installed at job start via npm install -g, which can fail if npm is rate-limited, if the package registry is unreachable, or if the Node.js version is incompatible. In persistent runner environments it may be pre-installed but outdated relative to ecosystem lockfile schema changes. Monitor runtime availability first:
- In Vigilmon, click Add Monitor → Cron Heartbeat.
- Set Expected ping interval to
15 minutes. - Copy the heartbeat URL (e.g.,
https://vigilmon.online/heartbeat/abc123).
#!/bin/bash
# /usr/local/bin/cdxgen-health-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/abc123"
CDXGEN_BIN="${CDXGEN_BIN:-cdxgen}"
# Check Node.js runtime version (cdxgen requires Node 18+)
NODE_VERSION=$(node --version 2>/dev/null | grep -oE '[0-9]+' | head -1)
if [ -z "$NODE_VERSION" ] || [ "$NODE_VERSION" -lt 18 ]; then
echo "Node.js not found or version too old: need 18+, found ${NODE_VERSION:-none}"
exit 1
fi
# Check cdxgen binary availability
if ! command -v "$CDXGEN_BIN" &>/dev/null; then
# Try npx fallback
if npx @cyclonedx/cdxgen --version &>/dev/null 2>&1; then
CDXGEN_BIN="npx @cyclonedx/cdxgen"
echo "cdxgen found via npx"
else
echo "cdxgen not found (checked PATH and npx)"
exit 1
fi
fi
# Check cdxgen version output
VERSION=$($CDXGEN_BIN --version 2>&1 | head -1)
if [ -z "$VERSION" ]; then
echo "cdxgen binary exists but produced no version output"
exit 1
fi
echo "cdxgen OK: $VERSION (Node.js v${NODE_VERSION})"
curl -s "$HEARTBEAT_URL"
chmod +x /usr/local/bin/cdxgen-health-check.sh
(crontab -l 2>/dev/null; echo "*/15 * * * * /usr/local/bin/cdxgen-health-check.sh >> /var/log/cdxgen-health.log 2>&1") | crontab -
For Docker-based cdxgen (ghcr.io/cyclonedx/cdxgen), verify the image is pullable and the container starts correctly as a separate health check step.
Step 2: Monitor SBOM Generation Success Rate
cdxgen can exit with code 0 even when it encounters errors in individual ecosystem parsers — it generates a partial SBOM rather than failing outright. This means a CI pipeline that checks only the exit code misses cases where cdxgen produced a file but with far fewer components than the project actually has. Monitor generation by checking both exit code and output file validity after every run:
#!/bin/bash
# /usr/local/bin/cdxgen-generate-and-report.sh
# Wraps cdxgen with monitoring; use this in CI instead of calling cdxgen directly
HEARTBEAT_URL="https://vigilmon.online/heartbeat/def456"
PROJECT_PATH="${1:?Usage: $0 <project-path> [output-path]}"
OUTPUT_PATH="${2:-./bom.json}"
CDXGEN_BIN="${CDXGEN_BIN:-cdxgen}"
TIMEOUT=300 # 5 minutes max for generation
echo "Starting cdxgen SBOM generation: $PROJECT_PATH"
START_TIME=$(date +%s)
# Run cdxgen with strict mode and timeout
CDXGEN_OUTPUT=$(timeout "$TIMEOUT" "$CDXGEN_BIN" \
--type auto \
--output "$OUTPUT_PATH" \
--validate \
"$PROJECT_PATH" 2>&1)
CDXGEN_EXIT=$?
END_TIME=$(date +%s)
DURATION=$(( END_TIME - START_TIME ))
if [ "$CDXGEN_EXIT" -eq 124 ]; then
echo "cdxgen TIMED OUT after ${TIMEOUT}s on $PROJECT_PATH"
exit 1
elif [ "$CDXGEN_EXIT" -ne 0 ]; then
echo "cdxgen FAILED (exit $CDXGEN_EXIT) on $PROJECT_PATH:"
echo "$CDXGEN_OUTPUT"
exit 1
fi
# Verify output file is valid JSON and non-empty
if [ ! -s "$OUTPUT_PATH" ]; then
echo "cdxgen produced no output file at $OUTPUT_PATH"
exit 1
fi
COMPONENT_COUNT=$(python3 -c "
import json, sys
with open('$OUTPUT_PATH') as f:
data = json.load(f)
components = data.get('components', [])
print(len(components))
" 2>/dev/null || echo -1)
if [ "$COMPONENT_COUNT" -eq -1 ]; then
echo "cdxgen output at $OUTPUT_PATH is not valid JSON"
exit 1
fi
echo "cdxgen OK: ${DURATION}s — $COMPONENT_COUNT components in $OUTPUT_PATH"
curl -s "$HEARTBEAT_URL"
Add to CI as a wrapper:
# In your CI pipeline (GitHub Actions example)
- name: Generate SBOM
run: /usr/local/bin/cdxgen-generate-and-report.sh . ./bom.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: ./bom.json
Set the Vigilmon heartbeat to 35 minutes if builds run hourly. A missing heartbeat means either no build ran or SBOM generation failed.
Step 3: Monitor SBOM Completeness
An SBOM with zero or suspiciously few components is often worse than no SBOM at all — it creates false confidence that the inventory is complete. cdxgen can produce empty SBOMs when the lockfile is missing, when the project uses an ecosystem it does not yet fully support, or when it encounters a permission error reading the project directory. Monitor component counts against historical baselines:
#!/bin/bash
# /usr/local/bin/cdxgen-completeness-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/ghi789"
SBOM_PATH="${1:?Usage: $0 <sbom-path>}"
BASELINE_FILE="/var/lib/cdxgen-monitoring/baselines.json"
PROJECT_NAME="${2:-$(basename $(dirname $SBOM_PATH))}"
MIN_ABSOLUTE_COMPONENTS=5 # Alert if SBOM has fewer than 5 components
REGRESSION_THRESHOLD_PCT=30 # Alert if component count drops > 30% vs baseline
if [ ! -f "$SBOM_PATH" ]; then
echo "SBOM file not found: $SBOM_PATH"
exit 1
fi
COMPONENT_COUNT=$(python3 -c "
import json, sys
with open('$SBOM_PATH') as f:
data = json.load(f)
# Count direct components
components = len(data.get('components', []))
# Also count metadata component if present
metadata_component = data.get('metadata', {}).get('component')
if metadata_component:
total = components # Don't double count the root component
else:
total = components
print(total)
" 2>/dev/null || echo -1)
if [ "$COMPONENT_COUNT" -eq -1 ]; then
echo "Could not parse SBOM at $SBOM_PATH"
exit 1
fi
if [ "$COMPONENT_COUNT" -lt "$MIN_ABSOLUTE_COMPONENTS" ]; then
echo "SBOM SUSPICIOUS: only $COMPONENT_COUNT components (min: $MIN_ABSOLUTE_COMPONENTS) — possible generation failure"
exit 1
fi
# Check against historical baseline if available
if [ -f "$BASELINE_FILE" ]; then
BASELINE=$(python3 -c "
import json, sys
with open('$BASELINE_FILE') as f:
baselines = json.load(f)
baseline = baselines.get('$PROJECT_NAME', {}).get('p90_component_count')
print(baseline if baseline else -1)
" 2>/dev/null || echo -1)
if [ "$BASELINE" -gt 0 ]; then
REGRESSION_COUNT=$(python3 -c "
baseline = $BASELINE
current = $COMPONENT_COUNT
threshold = $REGRESSION_THRESHOLD_PCT / 100
if current < baseline * (1 - threshold):
print(f'REGRESSION: {current} vs baseline {baseline} (>{$REGRESSION_THRESHOLD_PCT}% drop)')
else:
print('OK')
" 2>/dev/null)
if echo "$REGRESSION_COUNT" | grep -q "REGRESSION"; then
echo "SBOM completeness $REGRESSION_COUNT for $PROJECT_NAME"
exit 1
fi
fi
# Update the baseline rolling window
python3 -c "
import json, sys
from datetime import datetime
try:
with open('$BASELINE_FILE') as f:
baselines = json.load(f)
except:
baselines = {}
project = baselines.setdefault('$PROJECT_NAME', {'history': []})
project['history'].append($COMPONENT_COUNT)
project['history'] = project['history'][-20:] # Keep last 20 runs
if len(project['history']) >= 3:
sorted_h = sorted(project['history'])
p90_idx = int(len(sorted_h) * 0.9)
project['p90_component_count'] = sorted_h[p90_idx]
with open('$BASELINE_FILE', 'w') as f:
json.dump(baselines, f)
" 2>/dev/null
fi
echo "SBOM completeness OK: $COMPONENT_COUNT components for $PROJECT_NAME"
curl -s "$HEARTBEAT_URL"
Set the Vigilmon heartbeat to match your build cadence. A 30%+ component count regression usually indicates a lockfile was deleted, a workspace was not checked out before SBOM generation, or cdxgen's parser for a key ecosystem in the project failed.
Step 4: Monitor CycloneDX Schema Compliance
cdxgen generates CycloneDX-compliant SBOMs, but schema compliance can break when cdxgen is updated to a newer CycloneDX spec version while downstream consumers (Dependency-Track, Bomber) support only an older version, or when custom cdxgen plugins or post-processors introduce invalid fields. Monitor schema validity with the official CycloneDX CLI validator:
#!/bin/bash
# /usr/local/bin/cdxgen-schema-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/jkl012"
SBOM_PATH="${1:?Usage: $0 <sbom-path>}"
CDX_CLI_BIN="${CDX_CLI_BIN:-cyclonedx}" # cyclonedx-cli binary
# Check validator is available
if ! command -v "$CDX_CLI_BIN" &>/dev/null; then
# Try npx fallback with @cyclonedx/cyclonedx-cli
if npx @cyclonedx/cyclonedx-cli validate --help &>/dev/null 2>&1; then
CDX_CLI_BIN="npx @cyclonedx/cyclonedx-cli"
else
# Skip schema validation but verify JSON structure manually
VALID=$(python3 -c "
import json, sys
with open('$SBOM_PATH') as f:
data = json.load(f)
required = ['bomFormat', 'specVersion', 'version', 'components']
missing = [k for k in required if k not in data]
if missing:
print(f'INVALID: missing fields: {missing}')
sys.exit(1)
if data.get('bomFormat') != 'CycloneDX':
print(f'INVALID: bomFormat is not CycloneDX: {data.get(\"bomFormat\")}')
sys.exit(1)
# Check all components have at least a name
bad_components = [i for i, c in enumerate(data['components']) if not c.get('name')]
if bad_components:
print(f'INVALID: components at indices {bad_components} have no name')
sys.exit(1)
print(f'OK: CycloneDX {data.get(\"specVersion\")} with {len(data[\"components\"])} components')
" 2>/dev/null)
if echo "$VALID" | grep -q "^OK"; then
echo "SBOM structural check (no CDX CLI): $VALID"
curl -s "$HEARTBEAT_URL"
else
echo "SBOM structural check FAILED: $VALID"
exit 1
fi
exit 0
fi
fi
# Validate against the official CycloneDX JSON schema
VALIDATE_OUTPUT=$("$CDX_CLI_BIN" validate \
--input-file "$SBOM_PATH" \
--input-format json \
--fail-on-errors 2>&1)
VALIDATE_EXIT=$?
if [ "$VALIDATE_EXIT" -eq 0 ]; then
echo "CycloneDX schema validation OK: $SBOM_PATH"
curl -s "$HEARTBEAT_URL"
else
echo "CycloneDX schema validation FAILED: $SBOM_PATH"
echo "$VALIDATE_OUTPUT"
exit 1
fi
Set the Vigilmon heartbeat to match your build cadence. Schema validation failures after a cdxgen upgrade indicate a spec version mismatch — check whether Dependency-Track or your other consumers support the new CycloneDX version before upgrading cdxgen in production.
Step 5: Monitor Container Image SBOM Generation
cdxgen's container image SBOM generation mode (cdxgen --type container) pulls the image, inspects all layers, and resolves OS packages and application dependencies from the image filesystem. This mode depends on Docker daemon availability, registry reachability, and the syft-based container analysis backend that cdxgen uses internally. Container SBOM generation failures are more likely than source-code generation failures and often more consequential (container images frequently have more undeclared transitive dependencies). Monitor container generation specifically:
#!/bin/bash
# /usr/local/bin/cdxgen-container-check.sh
HEARTBEAT_URL="https://vigilmon.online/heartbeat/mno345"
CDXGEN_BIN="${CDXGEN_BIN:-cdxgen}"
TEST_IMAGE="${CDXGEN_TEST_IMAGE:-alpine:3.18}" # Lightweight test image
OUTPUT_PATH="/tmp/cdxgen-container-test-$$.json"
TIMEOUT=120
MIN_CONTAINER_COMPONENTS=3 # Alpine should have at least 3 OS packages
# Check Docker daemon is accessible
if ! docker info &>/dev/null 2>&1; then
echo "Docker daemon not accessible — container SBOM generation unavailable"
exit 1
fi
echo "Generating container SBOM for $TEST_IMAGE"
CDXGEN_OUTPUT=$(timeout "$TIMEOUT" "$CDXGEN_BIN" \
--type container \
--output "$OUTPUT_PATH" \
"$TEST_IMAGE" 2>&1)
CDXGEN_EXIT=$?
if [ "$CDXGEN_EXIT" -eq 124 ]; then
rm -f "$OUTPUT_PATH"
echo "cdxgen container SBOM generation TIMED OUT after ${TIMEOUT}s"
exit 1
elif [ "$CDXGEN_EXIT" -ne 0 ]; then
rm -f "$OUTPUT_PATH"
echo "cdxgen container SBOM generation FAILED (exit $CDXGEN_EXIT):"
echo "$CDXGEN_OUTPUT"
exit 1
fi
if [ ! -s "$OUTPUT_PATH" ]; then
echo "cdxgen produced no output for container image $TEST_IMAGE"
exit 1
fi
COMPONENT_COUNT=$(python3 -c "
import json
with open('$OUTPUT_PATH') as f:
data = json.load(f)
print(len(data.get('components', [])))
" 2>/dev/null || echo 0)
rm -f "$OUTPUT_PATH"
if [ "$COMPONENT_COUNT" -ge "$MIN_CONTAINER_COMPONENTS" ]; then
echo "Container SBOM generation OK: $COMPONENT_COUNT components from $TEST_IMAGE"
curl -s "$HEARTBEAT_URL"
else
echo "Container SBOM SUSPICIOUS: only $COMPONENT_COUNT components from $TEST_IMAGE (min: $MIN_CONTAINER_COMPONENTS)"
exit 1
fi
Run this check every 30 minutes with a Vigilmon heartbeat at 45 minutes. A healthy Alpine 3.18 SBOM should contain at least 15-20 OS packages; fewer than 3 suggests the container analyzer is not traversing image layers correctly. Use a known-small image like Alpine as the test to keep the check fast and predictable.
Step 6: Set Up Alert Channels
Configure Vigilmon to route cdxgen monitoring alerts to your supply chain security and DevOps teams:
- Go to Alert Channels in Vigilmon.
- Add a Slack webhook to
#supply-chain-security— generation failures mean downstream scanners and compliance platforms have no inventory to analyze. - Add PagerDuty for the schema validation and container generation monitors — invalid SBOMs or container generation failures affect all security tooling downstream.
- Add email notification for the completeness regression monitor — a gradual component count decrease warrants investigation but not an immediate page.
- Set Consecutive failures before alert to
1for the binary health and schema validation monitors — these should never fail in a properly configured environment.
Include SBOM context in Slack alerts:
PROJECT_NAME="backend-api"
SBOM_PATH="./bom.json"
CURRENT_COUNT=12
BASELINE_COUNT=47
DROP_PCT=$(( (BASELINE_COUNT - CURRENT_COUNT) * 100 / BASELINE_COUNT ))
ALERT_MSG=":warning: *cdxgen SBOM completeness regression* — \`${PROJECT_NAME}\` has ${CURRENT_COUNT} components (baseline: ${BASELINE_COUNT}, drop: ${DROP_PCT}%). Check that all lockfiles are committed before SBOM generation runs."
curl -X POST "$SUPPLY_CHAIN_WEBHOOK" \
-H 'Content-type: application/json' \
--data "{\"text\": \"$ALERT_MSG\"}"
Add maintenance windows during cdxgen upgrades:
curl -X POST https://vigilmon.online/api/maintenance \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"monitor_id": "CDXGEN_MONITOR_ID",
"duration_minutes": 15,
"reason": "cdxgen version upgrade and ecosystem plugin update"
}'
# Upgrade cdxgen and verify new version
npm install -g @cyclonedx/cdxgen@latest
cdxgen --version
Summary
| Monitor | Target | What It Catches |
|---|---|---|
| Cron heartbeat (binary health) | cdxgen --version + Node.js check | Missing binary, incompatible Node.js runtime |
| Cron heartbeat (generation success) | CI wrapper exit code + JSON output | Timeout, parse failures, empty file output |
| Cron heartbeat (completeness) | Component count vs historical baseline | Missing lockfiles, ecosystem parser failures |
| Cron heartbeat (schema validation) | cyclonedx validate output | Spec version mismatches, invalid field formats |
| Cron heartbeat (container generation) | Alpine image SBOM component count | Docker daemon unavailability, layer traversal bugs |
cdxgen's failure mode is silent inventory truncation — it generates a file, the CI pipeline marks the step as passed, and downstream scanners receive a partial component list and report clean results for components that were never analyzed. A 200-dependency Node.js application that appears in Dependency-Track with 8 components is a monitoring gap masquerading as supply chain health. Vigilmon's external monitoring catches both total generation failures and subtle completeness regressions before they compromise the security signal your entire SBOM pipeline depends on.
Start monitoring for free at vigilmon.online.