diff --git a/agent/kubviz/plugins/trivy/trivy_sbom.go b/agent/kubviz/plugins/trivy/trivy_sbom.go index 08358c32..62027761 100644 --- a/agent/kubviz/plugins/trivy/trivy_sbom.go +++ b/agent/kubviz/plugins/trivy/trivy_sbom.go @@ -8,58 +8,42 @@ import ( "log" "os" "os/exec" + "strings" - "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" "github.com/google/uuid" - "github.com/intelops/kubviz/agent/kubviz/plugins/outdated" "github.com/intelops/kubviz/constants" "github.com/intelops/kubviz/model" "github.com/intelops/kubviz/pkg/opentelemetry" "github.com/nats-io/nats.go" + "github.com/pkg/errors" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) -func PublishTrivySbomReport(report cyclonedx.BOM, js nats.JetStreamContext) error { - - for _, packageinfo := range report.Packages { - for _, pkg := range packageinfo.Packages { - - metrics := model.SbomData{ - ID: uuid.New().String(), - ClusterName: ClusterName, - ComponentName: report.CycloneDX.Metadata.Component.Name, - PackageName: pkg.Name, - PackageUrl: report.CycloneDX.Metadata.Component.PackageURL, - BomRef: report.CycloneDX.Metadata.Component.BOMRef, - SerialNumber: report.CycloneDX.SerialNumber, - CycloneDxVersion: report.CycloneDX.Version, - BomFormat: report.CycloneDX.BOMFormat, - } - metricsJson, err := json.Marshal(metrics) - if err != nil { - log.Println("error occurred while marshalling sbom metrics in agent", err.Error()) - return err - } - _, err = js.Publish(constants.TRIVY_SBOM_SUBJECT, metricsJson) - if err != nil { - return err - } - log.Printf("Trivy sbom report with Id %v has been published\n", metrics.ID) - } +func PublishTrivySbomReport(report map[string]interface{}, js nats.JetStreamContext) error { + + metrics := model.Sbom{ + ID: uuid.New().String(), + ClusterName: ClusterName, + Report: report, + } + metricsJson, err := json.Marshal(metrics) + if err != nil { + log.Println("error occurred while marshalling sbom metrics in agent", err.Error()) + return err + } + _, err = js.Publish(constants.TRIVY_SBOM_SUBJECT, metricsJson) + if err != nil { + return err } + log.Printf("Trivy sbom report with Id %v has been published\n", metrics.ID) return nil } func executeCommandSbom(command string) ([]byte, error) { - ctx := context.Background() - tracer := otel.Tracer("trivy-sbom") - _, span := tracer.Start(opentelemetry.BuildContext(ctx), "executeCommandSbom") - span.SetAttributes(attribute.String("trivy-sbom-agent", "sbom-command-running")) - defer span.End() - cmd := exec.Command("/bin/sh", "-c", command) var outc, errc bytes.Buffer cmd.Stdout = &outc @@ -84,10 +68,9 @@ func RunTrivySbomScan(config *rest.Config, js nats.JetStreamContext) error { ctx := context.Background() tracer := otel.Tracer("trivy-sbom") _, span := tracer.Start(opentelemetry.BuildContext(ctx), "RunTrivySbomScan") - span.SetAttributes(attribute.String("sbom", "sbom-creation")) defer span.End() - images, err := outdated.ListImages(config) + images, err := ListImagesforSbom(config) if err != nil { log.Printf("failed to list images: %v", err) @@ -111,13 +94,79 @@ func RunTrivySbomScan(config *rest.Config, js nats.JetStreamContext) error { continue // Move on to the next image } - var report cyclonedx.BOM + var report map[string]interface{} err = json.Unmarshal(out, &report) if err != nil { log.Printf("Error unmarshaling JSON data for image sbom %s: %v", image.PullableImage, err) continue // Move on to the next image in case of an error } - PublishTrivySbomReport(report, js) + err = PublishTrivySbomReport(report, js) + if err != nil { + log.Printf("Error publishing Trivy SBOM report for image %s: %v", image.PullableImage, err) + continue + } } return nil } + +func ListImagesforSbom(config *rest.Config) ([]model.RunningImage, error) { + var err error + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, errors.Wrap(err, "failed to create clientset") + } + ctx := context.Background() + namespaces, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to list namespaces") + } + + runningImages := []model.RunningImage{} + for _, namespace := range namespaces.Items { + pods, err := clientset.CoreV1().Pods(namespace.Name).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, errors.Wrap(err, "failed to list pods") + } + + for _, pod := range pods.Items { + for _, initContainerStatus := range pod.Status.InitContainerStatuses { + pullable := initContainerStatus.ImageID + pullable = strings.TrimPrefix(pullable, "docker-pullable://") + runningImage := model.RunningImage{ + Pod: pod.Name, + Namespace: pod.Namespace, + InitContainer: &initContainerStatus.Name, + Image: initContainerStatus.Image, + PullableImage: pullable, + } + runningImages = append(runningImages, runningImage) + } + + for _, containerStatus := range pod.Status.ContainerStatuses { + pullable := containerStatus.ImageID + pullable = strings.TrimPrefix(pullable, "docker-pullable://") + + runningImage := model.RunningImage{ + Pod: pod.Name, + Namespace: pod.Namespace, + Container: &containerStatus.Name, + Image: containerStatus.Image, + PullableImage: pullable, + } + runningImages = append(runningImages, runningImage) + } + } + } + + // Remove exact duplicates + cleanedImages := []model.RunningImage{} + seenImages := make(map[string]bool) + for _, runningImage := range runningImages { + if !seenImages[runningImage.PullableImage] { + cleanedImages = append(cleanedImages, runningImage) + seenImages[runningImage.PullableImage] = true + } + } + + return cleanedImages, nil +} diff --git a/client/pkg/clickhouse/db_client.go b/client/pkg/clickhouse/db_client.go index 284b359a..065631f6 100644 --- a/client/pkg/clickhouse/db_client.go +++ b/client/pkg/clickhouse/db_client.go @@ -38,7 +38,7 @@ type DBInterface interface { InsertGitEvent(string) InsertKubeScoreMetrics(model.KubeScoreRecommendations) InsertTrivyImageMetrics(metrics model.TrivyImage) - InsertTrivySbomMetrics(metrics model.SbomData) + InsertTrivySbomMetrics(metrics model.Sbom) InsertTrivyMetrics(metrics model.Trivy) RetriveKetallEvent() ([]model.Resource, error) RetriveOutdatedEvent() ([]model.CheckResultfinal, error) @@ -841,11 +841,10 @@ func (c *DBClient) InsertTrivyImageMetrics(metrics model.TrivyImage) { } } -func (c *DBClient) InsertTrivySbomMetrics(metrics model.SbomData) { +func (c *DBClient) InsertTrivySbomMetrics(metrics model.Sbom) { ctx := context.Background() tracer := otel.Tracer("insert-trivy-sbom") _, span := tracer.Start(opentelemetry.BuildContext(ctx), "InsertTrivySbomMetrics") - span.SetAttributes(attribute.String("trivy-sbom-client", "insert")) defer span.End() tx, err := c.conn.Begin() @@ -857,16 +856,73 @@ func (c *DBClient) InsertTrivySbomMetrics(metrics model.SbomData) { log.Fatalf("error preparing statement: %v", err) } + data := metrics.Report + bomFormat, _ := data["bomFormat"].(string) //CycloneDX + serialNumber, _ := data["serialNumber"].(string) // exmplvalue:urn:uuid:146625a5-531a-40fa-a205-174448c6c569 + + // fetching metadata + metadata, ok := data["metadata"].(map[string]interface{}) + if !ok { + log.Println("error: metadata not found or not in expected format") + return + } + + // inside metadata + // taking component + component, ok := metadata["component"].(map[string]interface{}) + if !ok { + log.Println("error: component not found or not in expected format") + return + } + //timestamp, _ := metadata["timestamp"].(time.Time) + var eventTime time.Time + rawTimestamp, ok := metadata["timestamp"].(string) + if !ok { + log.Println("error: timestamp not found or not in expected format") + return + } + eventTime, err = time.Parse(time.RFC3339, rawTimestamp) + if err != nil { + log.Println("error parsing timestamp:", err) + return + } + // inside metadata + // taking component + // inside component taking bomRef, componentType, componentName, packageURL + bomRef, _ := component["bom-ref"].(string) //pkg:oci/redis@sha256:873c49204b64258778a1f34d23a962de526021e9a63b09236d6d7c86e2dd13e9?repository_url=public.ecr.aws%2Fdocker%2Flibrary%2Fredis\u0026arch=amd64 + componentType, _ := component["type"].(string) //container + componentName, _ := component["name"].(string) //public.ecr.aws/docker/library/redis@sha256:873c49204b64258778a1f34d23a962de526021e9a63b09236d6d7c86e2dd13e9 + packageURL, _ := component["purl"].(string) //pkg:oci/redis@sha256:873c49204b64258778a1f34d23a962de526021e9a63b09236d6d7c86e2dd13e9?repository_url=public.ecr.aws%2Fdocker%2Flibrary%2Fredis\u0026arch=amd64 + // fetching other componets + Components, ok := data["components"].([]interface{}) + if !ok { + log.Println("error: components not found or not in expected format") + } + var otherComponentName string + // Iterate over the components to find the desired name + for _, otherComponent := range Components { + componentsMap, ok := otherComponent.(map[string]interface{}) + if !ok { + log.Println("error: component not in expected format") + continue + } + if name, ok := componentsMap["name"].(string); ok { + otherComponentName = name // alpine + break + } + } + if _, err := stmt.Exec( metrics.ID, metrics.ClusterName, - metrics.ComponentName, - metrics.PackageName, - metrics.PackageUrl, - metrics.BomRef, - metrics.SerialNumber, - int32(metrics.CycloneDxVersion), - metrics.BomFormat, + bomFormat, + serialNumber, + bomRef, + componentName, + componentType, + packageURL, + eventTime, + otherComponentName, ); err != nil { log.Fatal(err) } diff --git a/client/pkg/clickhouse/statements.go b/client/pkg/clickhouse/statements.go index a728ceec..22406fbe 100644 --- a/client/pkg/clickhouse/statements.go +++ b/client/pkg/clickhouse/statements.go @@ -230,7 +230,7 @@ const InsertTrivyVul string = "INSERT INTO trivy_vul (id, cluster_name, namespac const InsertTrivyImage string = "INSERT INTO trivyimage (id, cluster_name, artifact_name, vul_id, vul_pkg_id, vul_pkg_name, vul_installed_version, vul_fixed_version, vul_title, vul_severity, vul_published_date, vul_last_modified_date) VALUES ( ?, ?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" const InsertTrivyMisconfig string = "INSERT INTO trivy_misconfig (id, cluster_name, namespace, kind, name, misconfig_id, misconfig_avdid, misconfig_type, misconfig_title, misconfig_desc, misconfig_msg, misconfig_query, misconfig_resolution, misconfig_severity, misconfig_status, EventTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" const InsertAzureContainerPushEvent DBStatement = "INSERT INTO azurecontainerpush (RegistryURL, RepositoryName, Tag, ImageName, Event, Size, SHAID, EventTime) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)" -const InsertTrivySbom string = "INSERT INTO trivysbom (id, cluster_name, image_name, package_name, package_url, bom_ref, serial_number, version, bom_format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" +const InsertTrivySbom string = "INSERT INTO trivysbom (id, cluster_name, bom_format, serial_number, bom_ref, image_name, component_type, package_url, event_time, other_component_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" const InsertQuayContainerPushEvent DBStatement = "INSERT INTO quaycontainerpush (name, repository, nameSpace, dockerURL, homePage, tag, Event, EventTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" const InsertJfrogContainerPushEvent DBStatement = "INSERT INTO jfrogcontainerpush (Domain, EventType, RegistryURL, RepositoryName, SHAID, Size, ImageName, Tag, Event, EventTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" const InsertKuberhealthy string = "INSERT INTO kuberhealthy (CurrentUUID, CheckName, OK, Errors, RunDuration, Namespace, Node, LastRun, AuthoritativePod) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" \ No newline at end of file diff --git a/client/pkg/clients/kubviz_client.go b/client/pkg/clients/kubviz_client.go index b126aaa7..2e83a8b5 100644 --- a/client/pkg/clients/kubviz_client.go +++ b/client/pkg/clients/kubviz_client.go @@ -151,7 +151,7 @@ func (n *NATSContext) SubscribeAllKubvizNats(conn clickhouse.DBInterface) { Consumer: cfg.TrivySbomConsumer, Handler: func(msg *nats.Msg) { msg.Ack() - var metrics model.SbomData + var metrics model.Sbom err := json.Unmarshal(msg.Data, &metrics) if err != nil { log.Println("failed to unmarshal from nats", err) diff --git a/model/trivy_sbom.go b/model/trivy_sbom.go index 647e34a4..e763418c 100644 --- a/model/trivy_sbom.go +++ b/model/trivy_sbom.go @@ -1,24 +1,7 @@ package model -import ( - "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" -) - type Sbom struct { - ID string - Report cyclonedx.BOM -} - -type SbomData struct { - ID string + ID string ClusterName string - ComponentName string - PackageName string - PackageUrl string - BomRef string - SerialNumber string - CycloneDxVersion int - BomFormat string + Report map[string]interface{} } - - diff --git a/sql/0000015_trivysbom.up.sql b/sql/0000015_trivysbom.up.sql index 7bc749ac..d93aa1bb 100644 --- a/sql/0000015_trivysbom.up.sql +++ b/sql/0000015_trivysbom.up.sql @@ -1,13 +1,14 @@ CREATE TABLE IF NOT EXISTS trivysbom ( id UUID, - cluster_name String, + cluster_name String, + bom_format String, + serial_number String, + bom_ref String, image_name String, - package_name String, + component_type String, package_url String, - bom_ref String, - serial_number String, - version INTEGER, - bom_format String, + event_time DateTime('UTC'), + other_component_name String, ExpiryDate DateTime DEFAULT now() + INTERVAL {{.TTLValue}} {{.TTLUnit}}, ExportedAt DateTime DEFAULT NULL ) ENGINE = MergeTree()