sbom-sentinel
v0.8.1
Published
Automated SBOM generation and vulnerability scanning for multiple repositories. Generates CycloneDX SBOMs, scans with Trivy, and notifies via Slack/email.
Maintainers
Readme
sbom-sentinel
Automated SBOM generation and vulnerability scanning for multiple Git repositories. Generates CycloneDX 1.6 SBOMs with cdxgen, scans with Trivy, and notifies via Slack or email when critical or high vulnerabilities are found.
Designed to run as a scheduled task (Kubernetes CronJob, cron, CI/CD pipeline) across any number of repositories — GitHub, GitLab, Bitbucket or any HTTPS-accessible Git host.
Features
- Clones multiple repositories and generates CycloneDX 1.6 SBOMs with cdxgen
- Scans each SBOM with Trivy and extracts structured findings
- Deduplicates findings across targets within each repository
- Consolidates results across all repositories into a single report
- Generates reports in JSON, HTML (standalone, no external CSS) and plain text
- Notifies via Slack webhook and/or email (SMTP) on CRITICAL/HIGH findings or scan errors
- Supports private repositories on GitHub and Bitbucket with per-platform token validation at startup
- Warns 15 days before a configured token expires and sends a notification via all enabled channels
- Supports custom SBOM generation commands per repository (
mode: "command") - Reads pre-generated SBOMs from a centralised SBOM repository without cloning or running cdxgen (
mode: "sbom-repository") - Detects pre-existing SBOMs in the repository (
sbom/sbom-*.json) and uses them directly instead of re-generating with cdxgen - Exports a consolidated CSV of all scanned components across every repository (
sbomExportconfig key) - Uploads HTML and JSON reports to IBM Cloud Object Storage or Google Drive and includes a direct link in notifications
- Zero required npm runtime dependencies — native Node 20 fetch, no axios, no dotenv
- Supports
node,swift,gradle,python,go,rustecosystems via cdxgen
Quick start
# 1. Install prerequisites (see Prerequisites section)
npm install -g @cyclonedx/cdxgen # or use npx
brew install aquasecurity/trivy/trivy
# 2. Install sbom-sentinel
npm install -g sbom-sentinel
# 3. Scaffold a new project with the interactive wizard
sbom-sentinel init # in the current directory
sbom-sentinel init ./my-audit-proj # or create a new directory
# 4. Fill in your credentials (.env) and run:
sbom-sentinel scan --dry-run
sbom-sentinel scanPrerequisites
sbom-sentinel requires these tools to be installed and available in PATH:
| Tool | Install | Purpose |
|---|---|---|
| Node.js ≥ 20 | nodejs.org | Runtime |
| git | git-scm.com | Clone repositories |
| cdxgen | npm install -g @cyclonedx/cdxgen | Generate CycloneDX SBOMs |
| trivy | trivy.dev | Scan SBOMs for vulnerabilities |
Verify that everything is in place:
sbom-sentinel checkInstallation
Global install (recommended for CLI use):
npm install -g sbom-sentinel
sbom-sentinel --versionOne-off execution without installing:
npx sbom-sentinel scanAs a library in your Node.js project:
npm install sbom-sentinelimport { scan, loadConfig } from 'sbom-sentinel';
const config = loadConfig(['scan'], process.cwd());
const { summary, exitCode } = await scan(config);Configuration
Config file
Run sbom-sentinel init to scaffold the full project interactively, or create sbom-sentinel.config.json manually:
{
"$schema": "https://raw.githubusercontent.com/pbojeda/sbom-sentinel/main/schema.json",
"manufacturer": "My Company, S.L.",
"outputDir": "./artifacts",
"notifications": {
"onVulnerabilities": true,
"onErrors": true,
"slack": { "enabled": true },
"email": { "enabled": false }
},
"repos": [
{
"name": "my-backend",
"cloneUrl": "https://github.com/myorg/my-backend.git",
"branch": "main",
"type": "node"
},
{
"name": "my-library",
"cloneUrl": "https://github.com/myorg/my-library.git",
"branch": "main",
"type": "node",
"mode": "command",
"sbomCommand": "npm ci && npm run sbom",
"sbomOutput": "sbom/bom.json"
},
{
"name": "my-ios-app",
"cloneUrl": "https://github.com/myorg/my-ios-app.git",
"branch": "main",
"type": "swift",
"enabled": false,
"notes": "Enable when Package.resolved is committed"
}
]
}Repository fields
| Field | Required | Description |
|---|---|---|
| name | Yes | Unique identifier used in reports and artifact filenames |
| cloneUrl | Yes* | HTTPS clone URL (not used when mode: "sbom-repository") |
| branch | Yes* | Branch to clone (not used when mode: "sbom-repository") |
| type | Yes* | Ecosystem type: node, swift, gradle, python, go, rust (not used when mode: "sbom-repository") |
| mode | No | "cdxgen" (default), "command" (custom SBOM script), or "sbom-repository" (read from local directory) |
| path | Yes** | Local directory containing sbom-DD-MM-YYYY folders. Required for sbom-repository mode. Not used in cdxgen or command modes. |
| sbomCommand | No | Shell command to generate the SBOM (required when mode: "command") |
| sbomOutput | No | Path to the SBOM file produced by sbomCommand (default: bom.json) |
| enabled | No | Set to false to skip this repo without removing it from the config |
| private | No | Set to true if the repo requires authentication. sbom-sentinel validates that the appropriate token is set before starting the scan |
| notes | No | Free-text notes, ignored by the tool |
* Required for mode: "cdxgen" and mode: "command". Not used in mode: "sbom-repository".
** Required for mode: "sbom-repository".
sbom-repository mode
mode: "sbom-repository" lets sbom-sentinel scan a centralised SBOM repository — a single directory that aggregates CycloneDX JSON files from multiple microservices — without cloning any source code or running cdxgen. Ideal for teams that already generate and store SBOMs as part of their CI/CD pipeline.
How it works:
- sbom-sentinel reads the
pathdirectory and looks for subdirectories matchingsbom-DD-MM-YYYY - It selects the most recent dated folder
- Every
*.jsonfile in that folder is treated as a CycloneDX SBOM for one microservice - Each SBOM is scanned with Trivy independently; results are consolidated into the global report
Config example:
{
"repos": [
{
"name": "i360-sbom-repository",
"mode": "sbom-repository",
"path": "/sbom-repo"
}
]
}In Kubernetes: use an init container to clone the SBOM repository before sbom-sentinel starts, mounting the clone into /sbom-repo via a shared emptyDir volume. sbom-sentinel itself only needs Trivy — no git, no cdxgen.
initContainers:
- name: clone-sbom-repo
image: alpine/git:latest
env:
- name: SBOM_REPO_TOKEN
valueFrom:
secretKeyRef: { name: my-secrets, key: SBOM_REPO_TOKEN }
command:
- sh
- -c
- |
git clone --depth 1 \
"https://x-token-auth:${SBOM_REPO_TOKEN}@bitbucket.org/myorg/sbom-repository.git" \
/sbom-repo
volumeMounts:
- { name: sbom-repo, mountPath: /sbom-repo }
# Share the volume with the main container via emptyDir
volumes:
- { name: sbom-repo, emptyDir: {} }Notes:
cloneUrl,branch,type, andprivateare not used and should be omitted- The
pathmust exist and contain at least onesbom-DD-MM-YYYYdirectory, otherwise the entry is skipped with an error - Microservice version is read from
metadata.component.versionin each SBOM file; if absent, it is left blank in the report - Each microservice's name in reports is derived from its JSON filename — e.g.
payment-service.json→payment-service. The top-levelnamefield only appears in configuration-level error messages - JSON files are not pre-validated before Trivy — an invalid or non-CycloneDX file will produce a Trivy scan error for that microservice
- When all repos use
mode: "sbom-repository",gitandcdxgenare skipped duringscan. Thecheckcommand always checks all three tools regardless of config - When mixing
sbom-repositoryentries withcdxgenorcommandentries in the same config,gitandcdxgenremain required
Pre-existing SBOM detection
If the cloned repository contains a sbom/sbom-*.json file, sbom-sentinel uses it directly and skips cdxgen. This is useful for repositories that maintain and commit their own SBOM.
Behaviour:
- Checked automatically before cdxgen runs — no configuration required
- The file is copied to the artifact directory under the standard naming pattern
- It still goes through
validateSbom(): must be valid CycloneDX JSON with acomponentsarray - If multiple files match (
sbom-v1.0.json,sbom-v1.5.json, …), the first alphabetically is used - If the
sbom/directory does not exist or contains no matching files, cdxgen runs as normal
A message is logged when a pre-existing SBOM is found:
Using pre-existing SBOM: sbom/sbom-v1.2.json (cdxgen skipped)Private repositories
Mark private repos with "private": true. sbom-sentinel validates that the appropriate token is set at startup — before any clone is attempted.
Token resolution order (highest to lowest priority)
| Priority | Variable | Platform | Username |
|---|---|---|---|
| 1 | BITBUCKET_TOKEN_<REPO_NAME> | bitbucket.org | x-token-auth |
| 1 | GITHUB_TOKEN_<REPO_NAME> | github.com | GITHUB_USER |
| 1 | GIT_TOKEN_<REPO_NAME> | other hosts | GIT_USER |
| 2 | BITBUCKET_TOKEN | bitbucket.org | BITBUCKET_USER |
| 2 | GITHUB_TOKEN | github.com | GITHUB_USER |
| 3 | GIT_TOKEN | any | GIT_USER |
<REPO_NAME> is derived from the name field in the config: uppercased, with non-alphanumeric characters replaced by _. For example my-backend → MY_BACKEND.
Bitbucket (free accounts — per-repo tokens)
Free Bitbucket accounts cannot create workspace-level tokens. Create a Repository HTTP access token for each repo (repo → Settings → Security → Access tokens, permission: Repository Read):
# In your .env — one entry per private Bitbucket repo
BITBUCKET_TOKEN_MY_BACKEND=ATBB...
BITBUCKET_TOKEN_MY_FRONTEND=ATBB...
BITBUCKET_TOKEN_MY_SERVICE=ATBB...No BITBUCKET_USER needed for per-repo tokens — the username is always x-token-auth.
Bitbucket (workspace token — paid plans)
If your plan supports workspace-level tokens, one token covers all repos in the workspace:
BITBUCKET_TOKEN=ATBB...
# BITBUCKET_USER defaults to x-token-auth — only set if your workspace uses a different usernameGitHub
A personal access token (classic or fine-grained with repository read access) works with the default GITHUB_USER=x-token-auth:
GITHUB_TOKEN=ghp_...For per-repo GitHub tokens (fine-grained PATs scoped to a single repo):
GITHUB_TOKEN_MY_BACKEND=github_pat_...Token expiry warnings
Tokens expire. Configure expiry dates so sbom-sentinel notifies you before they do:
{
"tokenExpiry": {
"BITBUCKET_TOKEN": "2027-04-15",
"GITHUB_TOKEN": "2027-06-01"
}
}Behaviour:
- If a token expires within 15 days: logs a warning to the console and sends a notification via all configured channels (Slack, email)
- If a token has already expired: same — the notification marks it as
EXPIREDwith a prompt to renew - Tokens beyond the 15-day window: shown in
--dry-runoutput with days remaining, no notification sent - Invalid date strings are silently skipped
The --dry-run command shows the status of all configured token expiry dates without executing any scans.
SBOM component export
After all SBOMs are generated (and before vulnerability scanning), sbom-sentinel can write a consolidated CSV listing every component from every repository. Useful for traceability audits in regulated environments.
{
"sbomExport": {
"enabled": true,
"filePrefix": "sbom-export"
}
}| Field | Default | Description |
|---|---|---|
| enabled | true | Set to false to skip the export entirely |
| filePrefix | "sbom-export" | Prefix for the CSV filename. Only [a-zA-Z0-9._-] characters allowed. Result: {prefix}-YYYY_MM_DD.csv |
The CSV is written to {outputDir}/reports/ and uploaded to all configured storage providers alongside the HTML and JSON reports. Generation is non-fatal — if it fails for any reason, vulnerability scanning continues and a warning is logged.
CSV columns: repo, name, version, type, purl, licenses, group. Multiple licenses are joined with |.
Environment variables
All credentials and sensitive settings are passed via environment variables. The config file contains only non-sensitive configuration.
| Variable | Required | Default | Description |
|---|---|---|---|
| GITHUB_TOKEN_<REPO> | * | — | Per-repo GitHub token (highest priority for github.com repos) |
| GITHUB_TOKEN | * | — | Shared token for all github.com repositories |
| GITHUB_USER | No | x-token-auth | Username for GitHub token auth |
| BITBUCKET_TOKEN_<REPO> | * | — | Per-repo Bitbucket token; <REPO> is the uppercased repo name (e.g. MY_BACKEND) |
| BITBUCKET_TOKEN | * | — | Shared token for all bitbucket.org repositories |
| BITBUCKET_USER | No | x-token-auth | Username for shared Bitbucket token |
| GIT_TOKEN_<REPO> | * | — | Per-repo generic token for other hosts |
| GIT_TOKEN | * | — | Fallback token for any platform not covered above |
| GIT_USER | No | x-token-auth | Fallback git username |
| SLACK_WEBHOOK_URL | No | — | Slack incoming webhook URL |
| SMTP_HOST | No | — | SMTP server hostname |
| SMTP_PORT | No | 587 | SMTP port |
| SMTP_USER | No | — | SMTP username |
| SMTP_PASS | No | — | SMTP password |
| EMAIL_FROM | No | — | Sender address |
| EMAIL_TO | No | — | Comma-separated recipient addresses |
| STORAGE_PROVIDER | No | — | Enable persistent storage. Comma-separated list: ibm-cos, google-drive, or both |
| IBM_COS_ENDPOINT | * | — | IBM COS S3 endpoint URL |
| IBM_COS_BUCKET | * | — | IBM COS bucket name |
| IBM_COS_ACCESS_KEY_ID | * | — | IBM COS HMAC access key ID |
| IBM_COS_SECRET_ACCESS_KEY | * | — | IBM COS HMAC secret access key |
| IBM_COS_REGION | No | us-south | IBM COS region |
| IBM_COS_PUBLIC_URL | No | — | Virtual-hosted public base URL for IBM COS (bucket name in domain) |
| GOOGLE_DRIVE_CREDENTIALS | * | — | Path to service-account.json or inline JSON string |
| GOOGLE_DRIVE_FOLDER_ID | No | — | Google Drive folder ID for uploaded files |
| SENTINEL_CONFIG | No | ./sbom-sentinel.config.json | Path to the config file |
| SENTINEL_OUTPUT_DIR | No | ./artifacts | Output directory (overrides outputDir in config) |
| SENTINEL_REPO | No | — | Scan only the named repository (overrides --repo) |
| LOG_LEVEL | No | info | Log verbosity: debug, info, warn, error |
* At least one token must be set for any private repository in the config.
Configuration priority
Environment variables always win:
- Environment variables ← highest priority
- CLI flags (
--config,--repo,--dry-run) sbom-sentinel.config.json- Defaults
Usage
sbom-sentinel <command> [options]Commands
scan — Run the full pipeline
# Full scan using default config
GIT_TOKEN=ghp_xxx sbom-sentinel scan
# Preview what would happen without executing anything
sbom-sentinel scan --dry-run
# Scan a single repository by name
sbom-sentinel scan --repo my-backend
# Use a custom config file
sbom-sentinel scan --config /path/to/my-config.jsoninit — Scaffold a new project
Interactive wizard that asks questions and generates a complete, tailored project.
sbom-sentinel init # scaffold in the current directory
sbom-sentinel init ./my-audit-proj # create a new directory and scaffold inside itThe wizard asks:
- Project name — used as
manufacturerin the config - Repositories — add as many as needed (name, clone URL, branch, type, private flag)
- Slack notifications — yes/no
- Report storage —
none,ibm-cos,google-drive, orboth - Kubernetes manifests — yes/no (namespace, cron schedule, container image)
- Docker files — yes/no — generates
Dockerfileanddocker-compose.yml - CI pipeline —
none,bitbucket, orgithub-actions(auto-detected from repo URLs)
Generated files:
sbom-sentinel.config.json— fully populated with your repos and settings.env.example— only the credential vars relevant to your platforms and storage choices.gitignore— pre-configured to exclude.envandartifacts/kubernetes/cronjob.yaml,configmap.yaml,secrets.yaml— if Kubernetes was selectedDockerfile+docker-compose.yml— if Docker was selected; storage vars activated when a provider is configuredbitbucket-pipelines.yml— if CI=bitbucket; includessbom-scanandsbom-scan-singlepipelines, per-repo token hints in the header.github/workflows/sbom-sentinel.yml— if CI=github-actions; per-repo token env vars, storage vars activated when configured
Platform credential sections are derived automatically from each repo's clone URL — a project with only Bitbucket repos gets only the Bitbucket credential sections; mixed repos get all relevant sections.
sbom-repositoryentries must be added manually tosbom-sentinel.config.jsonafter runninginit— the wizard does not scaffold this mode.
check — Verify external tools
sbom-sentinel check
# Checks that git, cdxgen and trivy are installed and in PATHGlobal flags
sbom-sentinel --version # Print version
sbom-sentinel --help # Print usageExit codes
| Code | Meaning |
|---|---|
| 0 | All repositories scanned, no CRITICAL or HIGH vulnerabilities |
| 1 | CRITICAL or HIGH vulnerabilities found (all scans completed) |
| 2 | One or more repositories could not be scanned (partial results) |
Output
Directory structure
{outputDir}/
├── {YYYY-MM-DD}/
│ ├── {repo}__{branch}__{commitSha}__{timestamp}__bom.cdx.json
│ ├── {repo}__{branch}__{commitSha}__{timestamp}__trivy.json
│ └── ...
└── reports/
├── summary__{YYYY-MM-DD}.json
├── summary__{YYYY-MM-DD}.html
└── summary__{YYYY-MM-DD}.txtFilename conventions:
commitSha— 7-character short SHA of the cloned commit. Insbom-repositorymode, bothbranchandcommitShaare set to the dated folder name (e.g.sbom-25-04-2026)timestamp— UTC timestamp inYYYYMMDDTHHMMSSzformat- Slashes in branch names are replaced with
-(e.g.feature/auth→feature-auth)
Report formats
| Format | Content |
|---|---|
| JSON | Machine-readable GlobalSummary object — useful for post-processing or feeding into other tools |
| HTML | Standalone report with status banner, severity badges, per-repo table, and CVE details with links — no external CSS/JS required |
| TXT | Plain text summary suitable for Slack messages, email bodies, or terminal review |
Notifications
Slack
- Create an Incoming Webhook in your Slack workspace.
- Set the
SLACK_WEBHOOK_URLenvironment variable. - Enable Slack in the config:
"notifications": {
"onVulnerabilities": true,
"onErrors": true,
"slack": { "enabled": true }
}sbom-sentinel uses Node 20's native fetch — no extra dependencies.
Slack message format:
- Status headline (CRITICAL/HIGH DETECTED or SCAN ERRORS)
- Global totals by severity
- Affected repositories with CRITICAL and HIGH counts
- Failed repositories with error message
- Optional link to the full HTML report (when
reportUrlis set — see persistent storage)
Email (SMTP)
Email requires the optional nodemailer package:
npm install nodemailerThen configure:
"notifications": {
"email": { "enabled": true }
}export SMTP_HOST=smtp.example.com
export SMTP_PORT=587
export [email protected]
export SMTP_PASS=secret
export [email protected]
export [email protected],[email protected]Persistent report storage
After each scan, sbom-sentinel can automatically upload the HTML and JSON summary reports to a cloud storage provider. The public URL is appended to Slack and email notifications as a "View full report" link.
Both providers are optional dependencies — the base install is unaffected. Install only the package you need.
IBM Cloud Object Storage (S3-compatible)
Install the AWS SDK v3:
npm install @aws-sdk/client-s3Configure:
| Variable | Required | Description |
|---|---|---|
| STORAGE_PROVIDER | Yes | Set to ibm-cos (or ibm-cos,google-drive for both) |
| IBM_COS_ENDPOINT | Yes | S3 endpoint URL (e.g. https://s3.eu-de.cloud-object-storage.appdomain.cloud) |
| IBM_COS_BUCKET | Yes | Target bucket name |
| IBM_COS_ACCESS_KEY_ID | Yes | HMAC access key ID |
| IBM_COS_SECRET_ACCESS_KEY | Yes | HMAC secret access key |
| IBM_COS_REGION | No | Region (default: us-south) |
| IBM_COS_PUBLIC_URL | No | Virtual-hosted public base URL (e.g. https://my-bucket.s3.eu-de.cloud-object-storage.appdomain.cloud). When set, the bucket name is omitted from the object path. |
IBM Cloud setup:
- Create a bucket in IBM Cloud Object Storage
- Under Access policies → Public access, enable Object Reader to allow anonymous reads by URL
- Create a Service credential with Writer role and enable HMAC credentials
- Use the generated
access_key_idandsecret_access_keyas your env vars
STORAGE_PROVIDER=ibm-cos
IBM_COS_ENDPOINT=https://s3.eu-de.cloud-object-storage.appdomain.cloud
IBM_COS_BUCKET=sbom-sentinel-reports
IBM_COS_ACCESS_KEY_ID=<hmac_access_key_id>
IBM_COS_SECRET_ACCESS_KEY=<hmac_secret_access_key>
IBM_COS_REGION=eu-de
# optional virtual-hosted public URL (bucket name is in the domain):
IBM_COS_PUBLIC_URL=https://sbom-sentinel-reports.s3.eu-de.cloud-object-storage.appdomain.cloudGoogle Drive
Install the Google APIs client:
npm install googleapisConfigure:
| Variable | Required | Description |
|---|---|---|
| STORAGE_PROVIDER | Yes | Set to google-drive (or ibm-cos,google-drive for both) |
| GOOGLE_DRIVE_CREDENTIALS | Yes | Path to a service-account.json file, or the JSON content as an inline string |
| GOOGLE_DRIVE_FOLDER_ID | No | Target folder ID. Defaults to the service account's root drive. |
Reports are organised under a YYYY-MM-DD/ subfolder inside GOOGLE_DRIVE_FOLDER_ID (or Drive root). The subfolder is created automatically on first use and reused on subsequent runs the same day.
Google Cloud setup:
- In Google Cloud Console, create a service account
- Enable the Google Drive API and grant the
drivescope - Download the service account key as
service-account.json - Share the target Drive folder with the service account email as Editor
STORAGE_PROVIDER=google-drive
# Local / Docker: path to a service-account.json file
GOOGLE_DRIVE_CREDENTIALS=/path/to/service-account.json
# Kubernetes: inline JSON (no file mount needed — store minified JSON as a Secret value)
# GOOGLE_DRIVE_CREDENTIALS={"type":"service_account","client_email":"[email protected]","private_key":"..."}
GOOGLE_DRIVE_FOLDER_ID=1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbsGoogle Workspace organisations: If you see
Service Accounts do not have storage quota, your org restricts service accounts from using personal Drive storage. Fix: create a Shared Drive (formerly Team Drive), add the service account email as a Contributor, and use the Shared Drive ID (or a folder within it) asGOOGLE_DRIVE_FOLDER_ID. Shared Drives have their own storage quota independent of user accounts.
Storage behaviour
- Multi-provider: set
STORAGE_PROVIDER=ibm-cos,google-driveto upload to both simultaneously. The first successful URL is used for notifications. - IBM COS: reports are uploaded to
reports/<filename>within the configured bucket - Google Drive: reports are uploaded to
YYYY-MM-DD/<filename>inside the configured folder (or root) - The HTML report URL is appended to Slack and email notifications as a "View full report" link
- If the optional package is not installed, sbom-sentinel warns and continues without uploading
- If the upload fails for any reason, a warning is logged and the scan continues without a report URL
- If
STORAGE_PROVIDERis set but required credentials are missing, the scan aborts at startup with a clear error listing the missing variables
Deployment
Docker
Run sbom-sentinel init and answer yes to "Generate Dockerfile and docker-compose.yml?" — the wizard generates both files tailored to your repos and storage choice.
Or use the template directly:
FROM node:20-alpine
RUN apk add --no-cache git bash curl jq
# cdxgen
RUN npm install -g @cyclonedx/cdxgen@11
# Trivy
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
# sbom-sentinel
RUN npm install -g sbom-sentinel
WORKDIR /app
VOLUME ["/app/artifacts"]
ENTRYPOINT ["sbom-sentinel"]
CMD ["scan"]# Build and run with docker compose (after sbom-sentinel init --docker)
docker compose build
docker compose run --rm sbom-sentinel scan
# Or with plain docker
docker build -t sbom-sentinel .
docker run --rm \
--env-file .env \
-v "$(pwd)/sbom-sentinel.config.json:/app/sbom-sentinel.config.json:ro" \
-v "$(pwd)/artifacts:/app/artifacts" \
sbom-sentinel scanKubernetes CronJob
apiVersion: batch/v1
kind: CronJob
metadata:
name: sbom-sentinel
namespace: security
spec:
schedule: "0 2 * * *" # 02:00 UTC daily
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 7
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 3600
template:
spec:
restartPolicy: Never
# imagePullSecrets: # uncomment if using a private container registry
# - name: registry-pull-secret
containers:
- name: sentinel
image: ghcr.io/pbojeda/sbom-sentinel:latest
args: ["scan"]
envFrom:
- secretRef:
name: sbom-sentinel-secrets
volumeMounts:
- name: config
mountPath: /app/sbom-sentinel.config.json
subPath: sbom-sentinel.config.json
- name: output
mountPath: /app/artifacts
volumes:
- name: config
configMap:
name: sbom-sentinel-config
- name: output
emptyDir: {} # reports are uploaded to cloud storage; no persistent volume neededSecrets (GIT_TOKEN, SLACK_WEBHOOK_URL, …) should be stored in a Kubernetes Secret named sbom-sentinel-secrets.
Private container registries (ICR, ECR, GCR): If your image is in a private registry, create an image pull secret and uncomment
imagePullSecretsabove. Example for IBM Container Registry:kubectl create secret docker-registry registry-pull-secret \ --namespace <your-namespace> \ --docker-server=de.icr.io \ --docker-username=iamapikey \ --docker-password=<IBM_API_KEY>
GitHub Actions
Run sbom-sentinel init and choose github-actions for the CI question — the wizard generates .github/workflows/sbom-sentinel.yml with per-repo token env vars and storage vars pre-configured. Or use the full example in examples/ci/github-actions.yml.
Minimal example:
# .github/workflows/sbom-scan.yml
name: SBOM Vulnerability Scan
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
- name: Install cdxgen and sbom-sentinel
run: npm install -g @cyclonedx/cdxgen@11 sbom-sentinel
- name: Run scan
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BITBUCKET_TOKEN: ${{ secrets.BITBUCKET_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
run: sbom-sentinel scan
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: sbom-reports-${{ github.run_id }}
path: artifacts/reports/
retention-days: 30Bitbucket Pipelines
Run sbom-sentinel init and choose bitbucket — the wizard generates bitbucket-pipelines.yml with sbom-scan (full) and sbom-scan-single (single-repo) custom pipelines, and per-repo token hints in the header. Or see the full example at examples/ci/bitbucket-pipelines.yml.
Schedule configuration: Settings → Pipelines → Schedules → select the sbom-scan custom pipeline.
# bitbucket-pipelines.yml
image: node:20
pipelines:
custom:
sbom-scan:
- step:
name: SBOM Vulnerability Scan
script:
- apt-get update && apt-get install -y curl
- curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh
| sh -s -- -b /usr/local/bin
- npm install -g @cyclonedx/cdxgen@11 sbom-sentinel
- sbom-sentinel check
- sbom-sentinel scan
artifacts:
- artifacts/reports/**Supported ecosystems
sbom-sentinel passes the type field directly to cdxgen. The following types are supported:
| Type | Ecosystem | cdxgen analyses |
|---|---|---|
| node | Node.js / npm / yarn / pnpm | package-lock.json, yarn.lock, pnpm-lock.yaml, node_modules/ |
| swift | Swift / iOS / macOS | Package.resolved, Package.swift |
| gradle | Java / Kotlin / Android | build.gradle, build.gradle.kts, gradle.lockfile |
| python | Python | requirements.txt, Pipfile.lock, poetry.lock, pyproject.toml |
| go | Go | go.sum, go.mod |
| rust | Rust | Cargo.lock, Cargo.toml |
For a full list of supported types and options, see the cdxgen documentation.
Testing
# Run tests once
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage report
npm run test:coverage
# Type-check without building
npm run lintThe test suite uses Vitest with real fixtures in tests/fixtures/. External tools (cdxgen, trivy, git) are never called in unit tests — shell commands are mocked at the module level.
Contributing
See CONTRIBUTING.md for guidelines on how to submit issues, propose changes, and open pull requests.
Technologies
| Technology | Version | Purpose | |---|---|---| | Node.js | ≥ 20 | Runtime | | TypeScript | ≥ 5.0 | Language | | cdxgen | ≥ 11 | CycloneDX SBOM generation (external tool) | | Trivy | ≥ 0.50 | Vulnerability scanning (external tool) | | Vitest | ≥ 2.0 | Test framework | | nodemailer | ≥ 8.0 | Email notifications (optional dependency) |
License
MIT — see LICENSE.
Author
pbojeda — github.com/pbojeda
