Skip to content
Merged
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

## 🎯 Why xbom?

Modern applications rely on much more than just open-source libraries. They often include:
Modern applications rely on so much more than just open-source libraries. They often include:

- AI SDKs 🧠
- ML models 🤖
- 3rd party SaaS APIs ☁️
- Cryptographic algorithms 🔑

`xbom` is designed to build comprehensive bill of material (BOM) for software dependencies
beyond just 3rd party libraries, using semantic code analysis and simple YAML based signatures.

✅ **Beyond Manifests** - `xbom` builds inventory using actual evidence from your codebase

✅ **Extensible Signatures** - add your own signatures over community maintained repository
Expand Down
89 changes: 79 additions & 10 deletions cmd/generate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"os"
"path"
Expand All @@ -16,9 +17,12 @@ import (
)

var (
packageURL string
appName string
codeDirectory string
cyclonedxReportPath string
htmlReportPath string
markdownReportPath string
summaryMaxResults int
summaryNoStats bool
summaryNoColor bool
Expand All @@ -41,10 +45,16 @@ func NewGenerateCommand() *cobra.Command {

cmd.Flags().StringVarP(&codeDirectory, "dir", "D", wd,
"Directory for analysing and generating BOM")
cmd.Flags().StringVarP(&packageURL, "purl", "P", "",
"Package URL of a supported OSS package (eg. pkg:/npm/express@4.17.1")
cmd.Flags().StringVarP(&appName, "app-name", "", "",
"App name to include in CycloneDX BOM")
cmd.Flags().StringVarP(&cyclonedxReportPath, "bom", "", "",
"Generate CycloneDX BOM to file")
cmd.Flags().StringVarP(&htmlReportPath, "html", "", "",
cmd.Flags().StringVarP(&htmlReportPath, "report-html", "", "",
"Generate HTML report to file")
cmd.Flags().StringVarP(&markdownReportPath, "report-markdown", "", "",
"Generate Markdown report to file")
cmd.Flags().IntVarP(&summaryMaxResults, "summary-limit", "", 20,
"Maximum number of results to display in summary (0 for unlimited)")
cmd.Flags().BoolVarP(&summaryNoStats, "summary-no-stats", "", false,
Expand All @@ -66,17 +76,64 @@ func NewGenerateCommand() *cobra.Command {

func generate() {
analytics.TrackCommandGenerate()
command.FailOnError("generate", internalGenerate())
command.FailOnError("generate", internalGenerateMulti())
}

func internalGenerate() error {
log.Infof("Generating BOM for source - %s", codeDirectory)
// internalGenerateMulti handles multiple input adapters before invoking the
// core scanning workflow
func internalGenerateMulti() error {
// Start with different supported adapters based on args
if packageURL != "" {
return internalGeneratePurl()
}

// Fallback to the last option ie. local directory
if appName == "" {
appName = path.Base(codeDirectory)
}

return internalGenerateDirectory(appName, codeDirectory)
}

// internalGeneratePurl setup a local cache for a package
// identified by its PURL for scanning. It also cleanup the local
// cache after the scanning process.
func internalGeneratePurl() error {
pullResponse, err := command.PackagePull(context.Background(), command.PackagePullRequest{
PURL: packageURL,
})
if err != nil {
return fmt.Errorf("failed to pull package: %w", err)
}

defer func() {
if err := pullResponse.Close(); err != nil {
log.Errorf("failed to cleanup package: %v", err)
}
}()

localPath, err := pullResponse.LocalPath()
if err != nil {
return fmt.Errorf("failed to find local path for package: %w", err)
}

if appName == "" {
appName = packageURL
}

return internalGenerateDirectory(appName, localPath)
}

// internalGenerate executes the core scanning workflow to generate an XBOM report
func internalGenerateDirectory(appName, codeDir string) error {
log.Infof("Generating BOM for source - %s", codeDir)

// provide grouping filters using signatures.LoadSignatures("microsoft", "azure", "servicebus")
signaturesToMatch, err := signatures.LoadAllSignatures()
if err != nil {
return fmt.Errorf("failed to load signatures: %w", err)
}

log.Debugf("Loaded %d signatures", len(signaturesToMatch))

reporters := []reporter.Reporter{}
Expand All @@ -95,7 +152,7 @@ func internalGenerate() error {
cdxReporter, err := reporter.NewCycloneDXBomReporter(reporter.CycloneDXReporterConfig{
Tool: xbomTool,
Path: cyclonedxReportPath,
ApplicationComponentName: path.Base(codeDirectory),
ApplicationComponentName: appName,
})
if err != nil {
return fmt.Errorf("failed to create CycloneDX reporter: %w", err)
Expand All @@ -105,18 +162,28 @@ func internalGenerate() error {

if htmlReportPath != "" {
htmlReporter, err := reporter.NewHTMLReporter(reporter.HTMLReporterConfig{
HtmlReportPath: htmlReportPath,
HTMLReportPath: htmlReportPath,
})
if err != nil {
return fmt.Errorf("failed to create HTML reporter: %w", err)
}
reporters = append(reporters, htmlReporter)
}

if markdownReportPath != "" {
markdownReporter, err := reporter.NewMarkdownReporter(reporter.MarkdownReporterConfig{
OutputPath: markdownReportPath,
})
if err != nil {
return fmt.Errorf("failed to create Markdown reporter: %w", err)
}
reporters = append(reporters, markdownReporter)
}

workflow := codeanalysis.NewCodeAnalysisWorkflow(
codeanalysis.CodeAnalysisWorkflowConfig{
Tool: xbomTool,
SourcePath: codeDirectory,
SourcePath: codeDir,
SignaturesToMatch: signaturesToMatch,
Callbacks: codeanalysis.CodeAnalysisCallbackRegistry{
OnStart: func() error {
Expand All @@ -143,10 +210,12 @@ func internalGenerate() error {
}

// Nudge user to visualise the results
if htmlReportPath == "" {
if htmlReportPath == "" && markdownReportPath == "" {
ui.Println()
ui.Println("Tip: You can visualise the report as HTML using \"--html\" flag.")
ui.Println("Example: xbom generate --html /tmp/report.html")
ui.Println("Tip: You can save the report to a file using \"--report-html\" or \"--report-markdown\" flags.")
ui.Println("Examples:")
ui.Println(" xbom generate --report-html /tmp/report.html")
ui.Println(" xbom generate --report-markdown /tmp/report.md")
}

return nil
Expand Down
66 changes: 66 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
description = "xbom - Development Environment";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Go toolchain
go_1_25

# Build essentials
gcc
pkg-config
gnumake

# SSL/TLS libraries
openssl
openssl.dev

# Git hooks manager
lefthook

# Required for pre-commit hook
gitleaks

# Go linting
golangci-lint

# Additional development tools
git
];

# Environment variables
CGO_ENABLED = "1";
GOEXPERIMENT = "greenteagc";

# Setup instructions and environment
shellHook = ''
echo "🔧 xBom development environment"
echo ""
echo "Available commands:"
echo " make - Build the project"
echo " make clean - Clean build artifacts"
echo " lefthook install - Install git hooks"
echo ""
echo "Go version: $(go version)"
echo "golangci-lint: $(golangci-lint --version 2>/dev/null || echo 'not found')"
echo "lefthook: $(lefthook version 2>/dev/null || echo 'not found')"
echo ""

# Ensure bin directory exists
mkdir -p bin
'';
};
}
);
}
62 changes: 47 additions & 15 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ module github.com/safedep/xbom
go 1.25.1

require (
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250704090109-f29b2dffa5c5.1
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.6-20250705071048-7ad8e6be7c05.1
github.com/CycloneDX/cyclonedx-go v0.9.2
github.com/PuerkitoBio/goquery v1.10.3
github.com/fatih/color v1.18.0
github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/posthog/posthog-go v1.5.12
github.com/safedep/code v0.0.0-20251005172610-55bf15cb03ec
github.com/safedep/dry v0.0.0-20250618113059-9f8b677e299c
github.com/safedep/dry v0.0.0-20251025050813-25b3d2836927
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.43.0
Expand All @@ -22,25 +22,40 @@ require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 // indirect
buf.build/go/protovalidate v0.13.1 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.121.2 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/profiler v0.4.3 // indirect
cloud.google.com/go/storage v1.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/briandowns/spinner v1.23.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/evilmartians/lefthook v1.13.6 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10-rc1 // indirect
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand All @@ -50,7 +65,11 @@ require (
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.25.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/google/pprof v0.0.0-20250602020802-c6617b811d0e // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kaptinlin/go-i18n v0.1.7 // indirect
Expand All @@ -74,7 +93,9 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/package-url/packageurl-go v0.1.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
Expand All @@ -85,29 +106,40 @@ require (
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.235.0 // indirect
google.golang.org/genproto v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
Expand Down
Loading
Loading