tutorial

Monitoring cdxgen (OWASP CycloneDX SBOM Generator) with Vigilmon

cdxgen is the OWASP CycloneDX SBOM generator supporting 20+ ecosystems including Node.js, Python, Java, Go, .NET, and container images — but silent generation failures, incomplete component inventories, and schema validation errors leave your supply chain visibility gaps undetected. Here's how to monitor cdxgen availability, generation success rates, SBOM completeness, and schema compliance with Vigilmon.

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/cdxgen or via container ghcr.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-cli for 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:

  1. In Vigilmon, click Add MonitorCron Heartbeat.
  2. Set Expected ping interval to 15 minutes.
  3. 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:

  1. Go to Alert Channels in Vigilmon.
  2. Add a Slack webhook to #supply-chain-security — generation failures mean downstream scanners and compliance platforms have no inventory to analyze.
  3. Add PagerDuty for the schema validation and container generation monitors — invalid SBOMs or container generation failures affect all security tooling downstream.
  4. Add email notification for the completeness regression monitor — a gradual component count decrease warrants investigation but not an immediate page.
  5. Set Consecutive failures before alert to 1 for 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →