diff --git a/cmd/collector/main.go b/cmd/collector/main.go index 79a64d0..c77840b 100644 --- a/cmd/collector/main.go +++ b/cmd/collector/main.go @@ -61,6 +61,9 @@ func newCommand() *cobra.Command { }, } + // Add the new CLI parameter for output format + c.PersistentFlags().StringVar(&cfg.StorageConfig.OutputFormat, "output-format", "json", "Output format for the collected images [json, cyclonedx]") + // Run Configuration c.PersistentFlags().BoolVar(&cfg.Debug, "debug", false, "Set logging level to debug, default logging level is info") c.Flags().StringSliceVarP(&cfg.RunConfig.ImageFilter, "image-filter", "s", []string{}, "Images to set the skip flag to true. Images as regex comma seperated without spaces. e.g. 'mock-service,mongo,openpolicyagent/opa,/istio/") @@ -185,8 +188,19 @@ func run(cfg *config.Config) { log.Debug().Interface("images", images).Msg("") log.Info().Msg("Images collected & converted") + // Determine the marshalling function based on the output format + var marshalFunc func(interface{}) ([]byte, error) + switch cfg.StorageConfig.OutputFormat { + case "json": + marshalFunc = collector.JsonIndentMarshal + case "cyclonedx": + marshalFunc = collector.CycloneDXMarshal + default: + log.Fatal().Msg("Unsupported output format: " + cfg.StorageConfig.OutputFormat) + } + // Store images - err = collector.Store(images, storage, collector.JsonIndentMarshal) + err = collector.Store(images, storage, marshalFunc) if err != nil { log.Fatal().Stack().Err(err).Msg("Could not store collected images") } diff --git a/go.mod b/go.mod index dafe833..1b72300 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect + github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect github.com/cloudflare/circl v1.6.0 // indirect diff --git a/go.sum b/go.sum index 089a9c0..8c65e4d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 0cb82bd..79ce2a6 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -1,7 +1,9 @@ package collector import ( + "encoding/json" "errors" + "fmt" "io" "maps" "regexp" @@ -10,6 +12,8 @@ import ( "github.com/SDA-SE/image-metadata-collector/internal/pkg/kubeclient" "github.com/rs/zerolog/log" + + "github.com/CycloneDX/cyclonedx-go" ) type AnnotationNames struct { @@ -191,3 +195,52 @@ func Store(images *[]CollectorImage, storage io.Writer, jsonMarshal JsonMarshal) return nil } + +func CycloneDXMarshal(v interface{}) ([]byte, error) { + images, ok := v.(*[]CollectorImage) + if !ok { + return nil, fmt.Errorf("invalid type, expected *[]CollectorImage") + } + + bom := cyclonedx.BOM{ + BOMFormat: "CycloneDX", + SpecVersion: cyclonedx.SpecVersion1_5, + Version: 1, + Components: &[]cyclonedx.Component{}, + } + + for _, img := range *images { + component := cyclonedx.Component{ + Type: cyclonedx.ComponentTypeContainer, + Name: img.Image, + Version: img.AppKubernetesIoVersion, + PackageURL: img.ImageId, + Properties: &[]cyclonedx.Property{ + {Name: "namespace", Value: img.Namespace}, + {Name: "environment", Value: img.Environment}, + {Name: "product", Value: img.Product}, + {Name: "description", Value: img.Description}, + {Name: "app_kubernetes_io_name", Value: img.AppKubernetesIoName}, + {Name: "container_type", Value: img.ContainerType}, + {Name: "team", Value: img.Team}, + {Name: "slack", Value: img.Slack}, + {Name: "email", Value: img.Email}, + {Name: "is_scan_baseimage_lifetime", Value: fmt.Sprintf("%v", img.IsScanBaseimageLifetime)}, + {Name: "is_scan_dependency_check", Value: fmt.Sprintf("%v", img.IsScanDependencyCheck)}, + {Name: "is_scan_dependency_track", Value: fmt.Sprintf("%v", img.IsScanDependencyTrack)}, + {Name: "is_scan_distroless", Value: fmt.Sprintf("%v", img.IsScanDistroless)}, + {Name: "is_scan_lifetime", Value: fmt.Sprintf("%v", img.IsScanLifetime)}, + {Name: "is_scan_malware", Value: fmt.Sprintf("%v", img.IsScanMalware)}, + {Name: "is_scan_new_version", Value: fmt.Sprintf("%v", img.IsScanNewVersion)}, + {Name: "is_scan_runasroot", Value: fmt.Sprintf("%v", img.IsScanRunAsRoot)}, + {Name: "is_scan_potentially_running_as_root", Value: fmt.Sprintf("%v", img.IsPotentiallyRunningAsRoot)}, + {Name: "is_scan_run_as_privileged", Value: fmt.Sprintf("%v", img.IsScanRunAsPrivileged)}, + {Name: "is_scan_potentially_running_as_privileged", Value: fmt.Sprintf("%v", img.IsPotentiallyRunningAsPrivileged)}, + {Name: "scan_lifetime_max_days", Value: fmt.Sprintf("%d", img.ScanLifetimeMaxDays)}, + }, + } + *bom.Components = append(*bom.Components, component) + } + + return json.MarshalIndent(bom, "", " ") +} diff --git a/internal/pkg/storage/storage.go b/internal/pkg/storage/storage.go index 7833030..8f4cfc4 100644 --- a/internal/pkg/storage/storage.go +++ b/internal/pkg/storage/storage.go @@ -15,8 +15,9 @@ type StorageConfig struct { git.GitConfig api.ApiConfig - StorageFlag string - FileName string + StorageFlag string + FileName string + OutputFormat string } func NewStorage(cfg *StorageConfig, environment string) (io.Writer, error) {