From 6a458f5694c34610b9f654510f19c83823c1c5a5 Mon Sep 17 00:00:00 2001 From: 030 Date: Sun, 6 Jul 2025 16:21:45 +0200 Subject: [PATCH] feat: sbom generator golang --- README.md | 2 +- cmd/nononsec/main.go | 212 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +- go.sum | 18 +++- 4 files changed, 233 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 81d37aa..0720fe1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NoNonSec — No-nonsense Security +# NoNonSec - No-nonsense Security ## Overview diff --git a/cmd/nononsec/main.go b/cmd/nononsec/main.go index 992c792..691d09d 100644 --- a/cmd/nononsec/main.go +++ b/cmd/nononsec/main.go @@ -2,12 +2,16 @@ package main import ( "bufio" + "bytes" + "encoding/json" "fmt" "io/fs" "os" + "os/exec" "path/filepath" "strings" + cdx "github.com/CycloneDX/cyclonedx-go" log "github.com/sirupsen/logrus" ) @@ -100,4 +104,212 @@ func main() { } log.Infof("Detected project type: %s", projectType) + + GenerateAllSBOMs() +} + +type GoModule struct { + Path string + Version string + Indirect bool + Replace *struct { + Path string + Version string + } +} + +type GoPackage struct { + Module *GoModule +} + +// loadModuleIndirectMap loads the indirect flag for all modules in the repo (go list -m -json all) +func loadModuleIndirectMap(root string) (map[string]bool, error) { + cmd := exec.Command("go", "list", "-m", "-json", "all") + cmd.Dir = root + cmd.Env = append(os.Environ(), "GO111MODULE=on") + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("go list -m -json all failed: %w", err) + } + + dec := json.NewDecoder(bytes.NewReader(out)) + indirectMap := make(map[string]bool) + for dec.More() { + var mod GoModule + if err := dec.Decode(&mod); err != nil { + return nil, err + } + path := mod.Path + version := mod.Version + if mod.Replace != nil { + path = mod.Replace.Path + version = mod.Replace.Version + } + key := path + "@" + version + indirectMap[key] = mod.Indirect + } + + return indirectMap, nil +} + +// findApps looks for dirs under cmd/ with main.go file +func findApps(cmdDir string) ([]string, error) { + entries, err := os.ReadDir(cmdDir) + if err != nil { + return nil, err + } + var apps []string + for _, e := range entries { + if e.IsDir() { + mainGo := filepath.Join(cmdDir, e.Name(), "main.go") + if fi, err := os.Stat(mainGo); err == nil && !fi.IsDir() { + apps = append(apps, filepath.Join(cmdDir, e.Name())) + } + } + } + return apps, nil +} + +// listUsedModules runs `go list -json -deps ./...` in dir and collects all used modules +func listUsedModules(dir string) (map[string]GoModule, error) { + cmd := exec.Command("go", "list", "-json", "-deps", "./...") + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GO111MODULE=on") + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("go list failed in %s: %w", dir, err) + } + + dec := json.NewDecoder(bytes.NewReader(out)) + modules := make(map[string]GoModule) + + for dec.More() { + var pkg GoPackage + if err := dec.Decode(&pkg); err != nil { + return nil, err + } + if pkg.Module == nil { + continue + } + mod := *pkg.Module + // Use replaced module path/version if any + if mod.Replace != nil { + mod.Path = mod.Replace.Path + mod.Version = mod.Replace.Version + } + if mod.Version == "" { + mod.Version = "unknown" + } + key := mod.Path + "@" + mod.Version + if _, exists := modules[key]; !exists { + modules[key] = mod + } + } + + return modules, nil +} + +func generateSBOM(appDir string, modules map[string]GoModule, indirectMap map[string]bool) error { + appName := filepath.Base(appDir) + + var comps []cdx.Component + for _, mod := range modules { + key := mod.Path + "@" + mod.Version + indirect := indirectMap[key] + + componentsType := cdx.ComponentTypeLibrary + name := mod.Path + version := mod.Version + + c := cdx.Component{ + Type: componentsType, + Name: name, + Version: version, + PackageURL: fmt.Sprintf("pkg:golang/%s@%s", name, version), + } + + if indirect { + // Add indirect info as evidence in the properties + c.Properties = &[]cdx.Property{ + { + Name: "indirect", + Value: "true", + }, + } + } + + comps = append(comps, c) + } + + bom := &cdx.BOM{ + SpecVersion: cdx.SpecVersion1_6, + Version: 1, + Components: &comps, + Metadata: &cdx.Metadata{ + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{{ + Type: cdx.ComponentTypeApplication, + Name: appName, + Version: "1.0.0", + Supplier: &cdx.OrganizationalEntity{ + Name: "SBOM Generator", + }, + }}, + }, + }, + } + + outFile := fmt.Sprintf("sbom-%s.json", appName) + f, err := os.Create(outFile) + if err != nil { + return err + } + defer f.Close() + + enc := cdx.NewBOMEncoder(f, cdx.BOMFileFormatJSON) + enc.SetPretty(true) + return enc.Encode(bom) +} + +func GenerateAllSBOMs() { + root, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get working dir: %v\n", err) + os.Exit(1) + } + + cmdDir := filepath.Join(root, "cmd") + apps, err := findApps(cmdDir) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to find apps in %s: %v\n", cmdDir, err) + os.Exit(1) + } + + if len(apps) == 0 { + fmt.Fprintf(os.Stderr, "no apps found in %s\n", cmdDir) + os.Exit(1) + } + + indirectMap, err := loadModuleIndirectMap(root) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to load indirect modules map: %v\n", err) + os.Exit(1) + } + + for _, app := range apps { + fmt.Printf("Generating SBOM for app: %s\n", filepath.Base(app)) + modules, err := listUsedModules(app) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to list used modules for %s: %v\n", app, err) + continue + } + err = generateSBOM(app, modules, indirectMap) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to generate SBOM for %s: %v\n", app, err) + } else { + fmt.Printf("SBOM for %s written\n", filepath.Base(app)) + } + } } diff --git a/go.mod b/go.mod index 9379541..06fec8e 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/030/nononsec go 1.24.3 -require github.com/sirupsen/logrus v1.9.3 +require ( + github.com/CycloneDX/cyclonedx-go v0.9.2 + github.com/sirupsen/logrus v1.9.3 +) require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/go.sum b/go.sum index 21f9bfb..9ffaadf 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/CycloneDX/cyclonedx-go v0.9.2 h1:688QHn2X/5nRezKe2ueIVCt+NRqf7fl3AVQk+vaFcIo= +github.com/CycloneDX/cyclonedx-go v0.9.2/go.mod h1:vcK6pKgO1WanCdd61qx4bFnSsDJQ6SbM2ZuMIgq86Jg= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -6,10 +10,20 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= +github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=