From cc518b9fdefa72e0fc7e68acf5a2d27493a3ce9c Mon Sep 17 00:00:00 2001 From: rafi Date: Mon, 15 Jun 2026 13:02:31 +0200 Subject: [PATCH 01/10] refactor: simplify devguard target and processor logic Signed-off-by: rafi --- .gitignore | 1 + .vscode/launch.json | 16 ++ Makefile | 2 +- config.go | 10 +- daemon.go | 6 - devguard_target.go | 491 +++++++------------------------------------- main.go | 1 - processor.go | 25 ++- target.go | 2 - 9 files changed, 107 insertions(+), 447 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 632f0f6c..926f3f22 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ dist work /auth +devguard-operator.yaml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..60c830bd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "." + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index 560daeca..2a654cf5 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ run: - go run ./main.go + go run . diff --git a/config.go b/config.go index 1120e48e..e57b39ac 100644 --- a/config.go +++ b/config.go @@ -12,9 +12,8 @@ type Config struct { RegistryProxies []string `yaml:"registryProxy" env:"SBOM_REGISTRY_PROXY" flag:"registryProxy"` Verbosity string `env:"SBOM_VERBOSITY" flag:"verbosity"` - DevGuardToken string `yaml:"devGuardToken" env:"DEVGUARD_TOKEN" flag:"token"` - DevGuardApiURL string `yaml:"devGuardApiURL" env:"DEVGUARD_API_URL" flag:"apiUrl"` - DevGuardProjectName string `yaml:"devGuardProjectID" env:"DEVGUARD_PROJECT_NAME" flag:"projectName"` + DevGuardToken string `yaml:"devGuardToken" env:"DEVGUARD_TOKEN" flag:"token"` + DevGuardApiURL string `yaml:"devGuardApiURL" env:"DEVGUARD_API_URL" flag:"apiUrl"` } var ( @@ -28,9 +27,8 @@ var ( ConfigKeyFallbackPullSecret = "fallbackPullSecret" ConfigKeyRegistryProxy = "registryProxy" - ConfigDevGuardToken = "token" - ConfigDevGuardApiURL = "apiUrl" - ConfigDevGuardProjectName = "projectName" + ConfigDevGuardToken = "token" + ConfigDevGuardApiURL = "apiUrl" OperatorConfig *Config ) diff --git a/daemon.go b/daemon.go index 85b8bcfc..dbdbf16b 100644 --- a/daemon.go +++ b/daemon.go @@ -60,12 +60,6 @@ func (c *CronService) runBackgroundService() { slog.Info("Execute background-service") for _, t := range c.processor.Targets { - err := t.Initialize() - if err != nil { - slog.Error("Target could not be initialized", "err", err) - continue - } - t.LoadImages() } diff --git a/devguard_target.go b/devguard_target.go index d480c1da..76cd6f03 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -1,15 +1,12 @@ package main import ( - "bytes" "encoding/json" - "fmt" "log/slog" "net/http" "strings" "sync" - "github.com/gosimple/slug" "github.com/l3montree-dev/devguard/pkg/devguard" parser "github.com/novln/docker-parser" @@ -18,399 +15,118 @@ import ( ) type DevGuardTarget struct { - apiUrl string - token string - tags []string - assetNameAnnotationKey string - - rootProjectID string - rootProjectSlug string - organizationSlug string - + apiUrl string + token string + tags []string client devguard.HTTPClient } -func NewDevGuardTarget(token, apiUrl, rootProjectName string, tags []string) *DevGuardTarget { - // fetch the root project id - client := devguard.NewHTTPClient(token, apiUrl) - - arr := strings.Split(rootProjectName, "/") - if len(arr) != 3 { - slog.Error(fmt.Sprintf("invalid root project name: %s. Needs to be /projects/", rootProjectName)) - panic(fmt.Sprintf("invalid root project name: %s. Needs to be /projects/", rootProjectName)) - } - - return &DevGuardTarget{ - apiUrl: apiUrl, - token: token, - tags: tags, - - rootProjectSlug: arr[2], - organizationSlug: arr[0], - client: client, - } +type DevGuardRequest struct { + Verb string `json:"verb"` + ProjectName string `json:"projectName"` + AssetName string `json:"assetName"` + AssetVersion string `json:"assetVersion"` + Sbom json.RawMessage `json:"sbom,omitempty"` } -func (g *DevGuardTarget) ValidateConfig() error { - if g.token == "" { - return fmt.Errorf("%s is empty", ConfigDevGuardToken) - } - - if g.apiUrl == "" { - return fmt.Errorf("%s is empty", ConfigDevGuardApiURL) - } - - if len(g.tags) == 0 { - g.tags = []string{"kubernetes-cluster"} - } - - return nil +type devGuardAssetVersion struct { + Name string `json:"name"` } -func (g *DevGuardTarget) Initialize() error { - // set the root project id - rootProject, err := g.getProjectBySlug(g.rootProjectSlug) - if err != nil { - return err - } - - g.rootProjectID = rootProject["id"].(string) - - // check if already marked as kubernetes cluster - if rootProject["type"].(string) != "kubernetesCluster" { - // update the project type - body := map[string]interface{}{ - "type": "kubernetesCluster", - } - - // to json - jsonBody, err := json.Marshal(body) - if err != nil { - return err - } - - req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/", g.organizationSlug, g.rootProjectSlug), bytes.NewBuffer(jsonBody)) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/json") - - _, err = g.client.Do(req) - if err != nil { - return err - } - - slog.Info("Updated root project to kubernetesCluster", "rootProjectSlug", g.rootProjectSlug) - } - - return nil -} - -func (g *DevGuardTarget) getProjectBySlug(slug string) (map[string]interface{}, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/", g.organizationSlug, slug), nil) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - resp, err := g.client.Do(req) - - if err != nil { - return nil, err - } - - if resp.StatusCode == http.StatusNotFound { - return nil, fmt.Errorf("project not found") - } - - var project map[string]interface{} - // parse the response body - err = json.NewDecoder(resp.Body).Decode(&project) - if err != nil { - return nil, err - } - - return project, nil +type devGuardAssetEntry struct { + Name string `json:"name"` + Versions []devGuardAssetVersion `json:"assetVersions"` } -func (g *DevGuardTarget) getAssetBySlug(projectSlug, slug string) (map[string]interface{}, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/assets/%s/", g.organizationSlug, projectSlug, slug), nil) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - resp, err := g.client.Do(req) - - if err != nil { - return nil, err - } - - if resp.StatusCode == http.StatusNotFound { - return nil, fmt.Errorf("asset not found") - } - - var asset map[string]interface{} - // parse the response body - err = json.NewDecoder(resp.Body).Decode(&asset) - if err != nil { - return nil, err - } - - return asset, nil +type devGuardAsset struct { + ProjectName string `json:"projectName"` + Assets []struct { + Name string `json:"name"` + Versions []string `json:"versions"` + } `json:"assets"` } -func (g *DevGuardTarget) createAssetInsideProject(projectSlug string, assetName string) (map[string]interface{}, error) { - // the asset does not exist, create it - createRequestBody := map[string]interface{}{ - "name": assetName, - "description": fmt.Sprintf("Controlled by an Kubernetes Operator. Asset %s", assetName), - - "confidentialityRequirement": "medium", - "integrityRequirement": "medium", - "availabilityRequirement": "medium", - } - - // to json - jsonBody, err := json.Marshal(createRequestBody) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/assets/", g.organizationSlug, projectSlug), bytes.NewReader(jsonBody)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - resp, err := g.client.Do(req) - if err != nil { - return nil, err - } - - // parse the response body - var asset map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&asset) - if err != nil { - return nil, err - } - - return asset, nil -} - -func (g *DevGuardTarget) createChildNamespaceProject(namespace string) (map[string]interface{}, error) { - // the project does not exist, create it - createRequestBody := map[string]interface{}{ - "name": namespace, - "description": fmt.Sprintf("Controlled by an Kubernetes Operator. Namespace %s", namespace), - "parentId": g.rootProjectID, - "type": "kubernetesNamespace", - } - - // to json - jsonBody, err := json.Marshal(createRequestBody) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", fmt.Sprintf("/api/v1/organizations/%s/projects/", g.organizationSlug), bytes.NewReader(jsonBody)) - - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - resp, err := g.client.Do(req) - if err != nil { - return nil, err - } +func NewDevGuardTarget(token, apiUrl string, tags []string) *DevGuardTarget { + client := devguard.NewHTTPClient(token, apiUrl) - // parse the response body - var project map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&project) - if err != nil { - return nil, err + return &DevGuardTarget{ + apiUrl: apiUrl, + token: token, + tags: tags, + client: client, } - - return project, nil } func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { - // fetch all projects inside the root project - req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/organizations/%s/projects?parentId=%s", g.organizationSlug, g.rootProjectID), nil) + req, err := http.NewRequest("GET", g.apiUrl, nil) if err != nil { - slog.Error("Could not fetch projects", "err", err) return nil, err } - // check all subprojects - // for each subproject, iterate over all assets. - var res []map[string]interface{} resp, err := g.client.Do(req) if err != nil { - slog.Error("Could not fetch projects", "err", err) return nil, err } + defer resp.Body.Close() - err = json.NewDecoder(resp.Body).Decode(&res) - if err != nil { - slog.Error("Could not fetch projects", "err", err) + var assets []devGuardAsset + if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil { return nil, err } - for el := range res { - slog.Info("Project", "name", res[el]["name"]) - } - - // fetch all assets for each project. - // we can do that concurrently - wg := sync.WaitGroup{} - - // channel to collect all images - images := make(chan kubernetes.ImageInNamespace) - - for _, project := range res { - wg.Add(1) - go func(project map[string]interface{}) { - defer wg.Done() - // fetch all assets - req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/assets/", g.organizationSlug, project["slug"].(string)), nil) - if err != nil { - slog.Error("Could not fetch assets", "err", err) - return - } - - resp, err := g.client.Do(req) - if err != nil { - slog.Error("Could not fetch assets", "err", err) - return - } - - var assets []map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&assets) - if err != nil { - slog.Error("Could not fetch assets", "err", err) - return - } - - for _, asset := range assets { - images <- kubernetes.ImageInNamespace{ - Namespace: project["name"].(string), + result := make([]kubernetes.ImageInNamespace, 0) + for _, a := range assets { + for _, asset := range a.Assets { + for _, version := range asset.Versions { + fullImage := asset.Name + ":" + version + result = append(result, kubernetes.ImageInNamespace{ + Namespace: a.ProjectName, Image: &libk8s.RegistryImage{ - ImageID: asset["name"].(string), - Image: asset["name"].(string), + ImageID: fullImage, + Image: fullImage, }, - } + }) } - }(project) - } - - go func() { - wg.Wait() - close(images) - }() - - var imagesInNamespace []kubernetes.ImageInNamespace = []kubernetes.ImageInNamespace{} - - for img := range images { - imagesInNamespace = append(imagesInNamespace, img) + } } - return imagesInNamespace, nil + return result, nil } func (g *DevGuardTarget) ProcessSbom(ctx *TargetContext) error { - assetName := "" - version := "" - - // Set custom project name by kubernetes annotation? - if g.assetNameAnnotationKey != "" { - slog.Debug(`Try to set project name by configured annotationkey`, "assetNameAnnotationKey", g.assetNameAnnotationKey) - for podAnnotationKey, podAnnotationValue := range ctx.Pod.Annotations { - if strings.HasPrefix(podAnnotationKey, g.assetNameAnnotationKey) { - if podAnnotationValue != "" { - // determine container name from annotation key - containerName := getContainerNameFromAnnotationKey(podAnnotationKey, "/") - if containerName != "" { - slog.Debug(`ContainerName found"`, "name", containerName) - // correct container? - if containerName == ctx.Container.Name { - assetName, version = getNameAndVersionFromString(podAnnotationValue, ":") - slog.Info(`Custom project name found`, "podAnnotationKey", podAnnotationKey, "containerName", containerName, "assetName", assetName, "version", version) - break - } - } else { - slog.Error(`Containername could not be determined from annotation. Skip setting project name.`, "podAnnotationKey", podAnnotationKey) - - } - } else { - slog.Error(`Empty value for custom project name annotation. Skip setting custom project name.`, "podAnnotationKey", podAnnotationKey) - } - } - } - } - // If assetNameAnnotationKey is not set or could not be parsed correctly, use image instead - if assetName == "" || version == "" { - assetName, version = getRepoWithVersion(ctx.Image) - } + assetName, version := getRepoWithVersion(ctx.Image) if ctx.Sbom == "" { slog.Info("Empty SBOM - skip image", "image", ctx.Image.ImageID) return nil } - client := devguard.NewHTTPClient(g.token, g.apiUrl) - // make sure the namespace project exists inside the root project - s := slug.Make(ctx.Pod.PodNamespace) - project, err := g.getProjectBySlug(s) - - slog.Debug("checking project existence", "projectSlug", s, "err", err, "project", project) - if err != nil { - // the project does not exist yet - // create it - slog.Debug("Creating project", "projectSlug", s) - project, err = g.createChildNamespaceProject(s) - if err != nil { - slog.Error("Could not create project", "err", err) - return err - } + payload := DevGuardRequest{ + Verb: "update", + ProjectName: ctx.Pod.PodNamespace, + AssetName: assetName, + AssetVersion: version, + Sbom: json.RawMessage(ctx.Sbom), } - // check if the asset does already exist inside the project - asset, err := g.getAssetBySlug(s, slug.Make(assetName)) + jsonBody, err := json.Marshal(payload) if err != nil { - // the asset does not exist yet - // create it now - asset, err = g.createAssetInsideProject(project["slug"].(string), assetName) - if err != nil { - slog.Error("Could not create asset", "err", err) - return err - } + return err } - // asset exists now. - // upload the SBOM to the asset - req, err := http.NewRequest("POST", "/api/v1/scan/", strings.NewReader(ctx.Sbom)) + req, err := http.NewRequest("POST", g.apiUrl, strings.NewReader(string(jsonBody))) if err != nil { - slog.Error("Could not upload BOM", "err", err) return err } req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Risk-Management", "true") - req.Header.Set("X-Asset-Name", fmt.Sprintf("%s/projects/%s/assets/%s", g.organizationSlug, s, asset["slug"].(string))) - req.Header.Set("X-Asset-Version", version) - req.Header.Set("X-Scan-Type", "container-scanning") - req.Header.Set("X-Scanner", "github.com/l3montree-dev/devguard-operator") slog.Info("Sending SBOM to DevGuard", "assetName", assetName, "version", version) - _, err = client.Do(req) + _, err = g.client.Do(req) if err != nil { - slog.Error("Could not upload BOM", "err", err) + slog.Error("Could not upload SBOM", "err", err) return err } @@ -419,7 +135,6 @@ func (g *DevGuardTarget) ProcessSbom(ctx *TargetContext) error { } func (g *DevGuardTarget) Remove(images []kubernetes.ImageInNamespace) error { - wg := sync.WaitGroup{} for _, img := range images { @@ -427,105 +142,41 @@ func (g *DevGuardTarget) Remove(images []kubernetes.ImageInNamespace) error { go func(img kubernetes.ImageInNamespace) { defer wg.Done() - name, _ := getRepoWithVersion(img.Image) - - projectSlug := slug.Make(img.Namespace) - assetSlug := slug.Make(name) + name, version := getRepoWithVersion(img.Image) - req, err := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/assets/%s/", g.organizationSlug, projectSlug, assetSlug), nil) - if err != nil { - slog.Error("could not delete asset", "err", err) - return + payload := DevGuardRequest{ + Verb: "delete", + ProjectName: img.Namespace, + AssetName: name, + AssetVersion: version, } - slog.Info("Deleting asset", "projectSlug", projectSlug, "assetSlug", assetSlug) - - req.Header.Set("Content-Type", "application/json") - _, err = g.client.Do(req) + jsonBody, err := json.Marshal(payload) if err != nil { - slog.Error("could not delete asset", "err", err) + slog.Error("could not marshal delete request", "err", err) return } - }(img) - } - - wg.Wait() - - // check if there are empty projects now. We can archive those too - namespaces := map[string]bool{} - for _, img := range images { - namespaces[img.Namespace] = true - } - - wg = sync.WaitGroup{} - // fetch all assets of those projects. - // if empty, archive the project - for namespace := range namespaces { - wg.Add(1) - go func(namespace string) { - defer wg.Done() - projectSlug := slug.Make(namespace) - req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/assets/", g.organizationSlug, projectSlug), nil) + req, err := http.NewRequest("POST", g.apiUrl, strings.NewReader(string(jsonBody))) if err != nil { - slog.Error("Could not fetch assets", "err", err) + slog.Error("could not create delete request", "err", err) return } - resp, err := g.client.Do(req) - if err != nil { - slog.Error("Could not fetch assets", "err", err) - return - } + req.Header.Set("Content-Type", "application/json") + + slog.Info("Deleting asset", "projectName", img.Namespace, "assetName", name, "assetVersion", version) - var assets []map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&assets) + _, err = g.client.Do(req) if err != nil { - slog.Error("Could not fetch assets", "err", err) + slog.Error("could not delete asset", "err", err) return } - - if len(assets) == 0 { - req, err := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/organizations/%s/projects/%s/", g.organizationSlug, projectSlug), nil) - if err != nil { - slog.Error("Could not delete project", "err", err) - return - } - - req.Header.Set("Content-Type", "application/json") - _, err = g.client.Do(req) - if err != nil { - slog.Error("Could not delete project", "err", err) - return - } - - slog.Info("Deleted project", "projectSlug", projectSlug) - } - }(namespace) + }(img) } wg.Wait() return nil - -} - -func getNameAndVersionFromString(input string, delimiter string) (string, string) { - parts := strings.Split(input, delimiter) - name := parts[0] - version := "latest" - if len(parts) == 2 { - version = parts[1] - } - return name, version -} - -func getContainerNameFromAnnotationKey(annotationKey string, delimiter string) string { - parts := strings.Split(annotationKey, delimiter) - containerName := "" - if len(parts) == 2 { - containerName = parts[1] - } - return containerName } func getRepoWithVersion(image *libk8s.RegistryImage) (string, string) { diff --git a/main.go b/main.go index 7474f51b..ac8f51d8 100644 --- a/main.go +++ b/main.go @@ -86,7 +86,6 @@ func newRootCmd() *cobra.Command { rootCmd.PersistentFlags().String(ConfigDevGuardToken, "", "DevGuard-Token") rootCmd.PersistentFlags().String(ConfigDevGuardApiURL, "", "DevGuard Api URL") - rootCmd.PersistentFlags().String(ConfigDevGuardProjectName, "", "DevGuard Project Name (eg. l3montree-cybersecurity/projects/devguard)") return rootCmd } diff --git a/processor.go b/processor.go index 86967ec1..14ad8aa0 100644 --- a/processor.go +++ b/processor.go @@ -105,7 +105,7 @@ func (p *Processor) scanPod(pod libk8s.PodInfo) { func initTargets() []Target { targets := make([]Target, 0) - t := NewDevGuardTarget(OperatorConfig.DevGuardToken, OperatorConfig.DevGuardApiURL, OperatorConfig.DevGuardProjectName, nil) + t := NewDevGuardTarget(OperatorConfig.DevGuardToken, OperatorConfig.DevGuardApiURL, nil) targets = append(targets, t) return targets @@ -159,9 +159,18 @@ func getChangedContainers(oldPod, newPod libk8s.PodInfo) ([]*libk8s.ContainerInf return addedContainers, removedContainers } -func containsImage(images []kubernetes.ImageInNamespace, image kubernetes.ImageInNamespace) bool { - for _, i := range images { - if i.String() == image.String() { +func containsImage(images []kubernetes.ImageInNamespace, target kubernetes.ImageInNamespace) bool { + targetRef := target.Image.Image + if !strings.Contains(targetRef, "/") { + targetRef = "docker.io/library/" + targetRef + } + + for _, candidate := range images { + candidateRef := candidate.Image.Image + if !strings.Contains(candidateRef, "/") { + candidateRef = "docker.io/library/" + candidateRef + } + if candidate.Namespace == target.Namespace && candidateRef == targetRef { return true } } @@ -226,13 +235,6 @@ func (p *Processor) runInformerAsync(informer cache.SharedIndexInformer) { go func() { - for _, t := range p.Targets { - err := t.Initialize() - if err != nil { - slog.Error("Target could not be initialized", "err", err) - } - } - slog.Info("Start pod-informer") informer.Run(stop) slog.Info("Pod-informer has stopped") @@ -265,6 +267,7 @@ func (p *Processor) runInformerAsync(informer cache.SharedIndexInformer) { info := p.K8s.Client.ExtractPodInfos(*pod) for _, c := range info.Containers { allImages = append(allImages, kubernetes.ImageInNamespace{Namespace: info.PodNamespace, Image: c.Image}) + if !containsImage(targetImages, kubernetes.ImageInNamespace{ Image: c.Image, Namespace: info.PodNamespace, diff --git a/target.go b/target.go index cf2d06d9..b21b793b 100644 --- a/target.go +++ b/target.go @@ -14,8 +14,6 @@ type TargetContext struct { } type Target interface { - Initialize() error - ValidateConfig() error ProcessSbom(ctx *TargetContext) error LoadImages() ([]kubernetes.ImageInNamespace, error) Remove(images []kubernetes.ImageInNamespace) error From c1ae19fc75ca4fa7e66ae404e437ecffbc33580a Mon Sep 17 00:00:00 2001 From: rafi Date: Mon, 15 Jun 2026 13:13:04 +0200 Subject: [PATCH 02/10] delete unused structs Signed-off-by: rafi --- devguard_target.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/devguard_target.go b/devguard_target.go index 76cd6f03..fc2f7c02 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -29,16 +29,7 @@ type DevGuardRequest struct { Sbom json.RawMessage `json:"sbom,omitempty"` } -type devGuardAssetVersion struct { - Name string `json:"name"` -} - -type devGuardAssetEntry struct { - Name string `json:"name"` - Versions []devGuardAssetVersion `json:"assetVersions"` -} - -type devGuardAsset struct { +type devguardAsset struct { ProjectName string `json:"projectName"` Assets []struct { Name string `json:"name"` @@ -69,7 +60,7 @@ func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { } defer resp.Body.Close() - var assets []devGuardAsset + var assets []devguardAsset if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil { return nil, err } From 35e4f8e50efe9df0b59d6190bc9fa550f2337bd8 Mon Sep 17 00:00:00 2001 From: rafi Date: Mon, 15 Jun 2026 13:20:29 +0200 Subject: [PATCH 03/10] update devguard workflow Signed-off-by: rafi --- .github/workflows/devguard.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/devguard.yml b/.github/workflows/devguard.yml index 6412aa74..076f3ba8 100644 --- a/.github/workflows/devguard.yml +++ b/.github/workflows/devguard.yml @@ -5,9 +5,11 @@ on: push: jobs: - devguard-scanner: - uses: l3montree-dev/devguard-action/.github/workflows/full.yml@main - with: - asset-name: "l3montree-cybersecurity/projects/devguard/assets/devguard-operator" - secrets: - devguard-token: ${{ secrets.DEVGUARD_TOKEN }} \ No newline at end of file + call-devsecops: + uses: l3montree-dev/devguard-action/.github/workflows/full.yml@main + with: + asset-name: "l3montree-cybersecurity/projects/devguard/assets/devguard-operator" + api-url: "https://api.main.devguard.org" + web-ui: "https://main.devguard.org" + secrets: + devguard-token: "${{ secrets.DEVGUARD_TOKEN }}" From 12a8575329bf635dd17a0e839d26a50c3bce0b61 Mon Sep 17 00:00:00 2001 From: rafi Date: Mon, 15 Jun 2026 13:35:48 +0200 Subject: [PATCH 04/10] update Dockerfile Signed-off-by: rafi --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2d2f5536..0bca12b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app RUN apk add --no-cache curl git -RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.54.1 +RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.71.1 COPY . . From 4ea50fb41e2e9f1ed36498d220aeac73b5f1319c Mon Sep 17 00:00:00 2001 From: rafi Date: Mon, 15 Jun 2026 17:53:44 +0200 Subject: [PATCH 05/10] rename apiUrl to projectURL and improve image comparison Signed-off-by: rafi --- config.go | 4 ++-- devguard_target.go | 21 +++++++++++---------- main.go | 2 +- processor.go | 17 +++++++++-------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/config.go b/config.go index e57b39ac..e3e9f841 100644 --- a/config.go +++ b/config.go @@ -13,7 +13,7 @@ type Config struct { Verbosity string `env:"SBOM_VERBOSITY" flag:"verbosity"` DevGuardToken string `yaml:"devGuardToken" env:"DEVGUARD_TOKEN" flag:"token"` - DevGuardApiURL string `yaml:"devGuardApiURL" env:"DEVGUARD_API_URL" flag:"apiUrl"` + DevGuardProjectURL string `yaml:"devGuardProjectURL" env:"DEVGUARD_PROJECT_URL" flag:"projectUrl"` } var ( @@ -28,7 +28,7 @@ var ( ConfigKeyRegistryProxy = "registryProxy" ConfigDevGuardToken = "token" - ConfigDevGuardApiURL = "apiUrl" + ConfigDevGuardProjectURL = "projectUrl" OperatorConfig *Config ) diff --git a/devguard_target.go b/devguard_target.go index fc2f7c02..3bc036e9 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -15,7 +15,7 @@ import ( ) type DevGuardTarget struct { - apiUrl string + projectURL string token string tags []string client devguard.HTTPClient @@ -29,7 +29,7 @@ type DevGuardRequest struct { Sbom json.RawMessage `json:"sbom,omitempty"` } -type devguardAsset struct { +type projectAssetsResponse struct { ProjectName string `json:"projectName"` Assets []struct { Name string `json:"name"` @@ -37,11 +37,11 @@ type devguardAsset struct { } `json:"assets"` } -func NewDevGuardTarget(token, apiUrl string, tags []string) *DevGuardTarget { - client := devguard.NewHTTPClient(token, apiUrl) +func NewDevGuardTarget(token, projectURL string, tags []string) *DevGuardTarget { + client := devguard.NewHTTPClient(token, projectURL) return &DevGuardTarget{ - apiUrl: apiUrl, + projectURL: projectURL, token: token, tags: tags, client: client, @@ -49,7 +49,7 @@ func NewDevGuardTarget(token, apiUrl string, tags []string) *DevGuardTarget { } func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { - req, err := http.NewRequest("GET", g.apiUrl, nil) + req, err := http.NewRequest("GET", g.projectURL, nil) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { } defer resp.Body.Close() - var assets []devguardAsset + var assets []projectAssetsResponse if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil { return nil, err } @@ -105,8 +105,9 @@ func (g *DevGuardTarget) ProcessSbom(ctx *TargetContext) error { if err != nil { return err } - - req, err := http.NewRequest("POST", g.apiUrl, strings.NewReader(string(jsonBody))) + providerID := "devguard-operator" + url := g.projectURL + "/dn/:" + providerID + req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonBody))) if err != nil { return err } @@ -148,7 +149,7 @@ func (g *DevGuardTarget) Remove(images []kubernetes.ImageInNamespace) error { return } - req, err := http.NewRequest("POST", g.apiUrl, strings.NewReader(string(jsonBody))) + req, err := http.NewRequest("POST", g.projectURL, strings.NewReader(string(jsonBody))) if err != nil { slog.Error("could not create delete request", "err", err) return diff --git a/main.go b/main.go index ac8f51d8..0add585d 100644 --- a/main.go +++ b/main.go @@ -85,7 +85,7 @@ func newRootCmd() *cobra.Command { rootCmd.PersistentFlags().Int64(ConfigKeyJobTimeout, 60*60, "Job-Timeout") rootCmd.PersistentFlags().String(ConfigDevGuardToken, "", "DevGuard-Token") - rootCmd.PersistentFlags().String(ConfigDevGuardApiURL, "", "DevGuard Api URL") + rootCmd.PersistentFlags().String(ConfigDevGuardProjectURL, "", "DevGuard Project URL") return rootCmd } diff --git a/processor.go b/processor.go index 14ad8aa0..ab028280 100644 --- a/processor.go +++ b/processor.go @@ -10,6 +10,7 @@ import ( libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" "github.com/ckotzbauer/libk8soci/pkg/oci" "github.com/l3montree-dev/devguard-operator/kubernetes" + parser "github.com/novln/docker-parser" "k8s.io/client-go/tools/cache" @@ -105,7 +106,7 @@ func (p *Processor) scanPod(pod libk8s.PodInfo) { func initTargets() []Target { targets := make([]Target, 0) - t := NewDevGuardTarget(OperatorConfig.DevGuardToken, OperatorConfig.DevGuardApiURL, nil) + t := NewDevGuardTarget(OperatorConfig.DevGuardToken, OperatorConfig.DevGuardProjectURL, nil) targets = append(targets, t) return targets @@ -160,17 +161,17 @@ func getChangedContainers(oldPod, newPod libk8s.PodInfo) ([]*libk8s.ContainerInf } func containsImage(images []kubernetes.ImageInNamespace, target kubernetes.ImageInNamespace) bool { - targetRef := target.Image.Image - if !strings.Contains(targetRef, "/") { - targetRef = "docker.io/library/" + targetRef + targetParsed, err := parser.Parse(target.Image.Image) + if err != nil { + return false } for _, candidate := range images { - candidateRef := candidate.Image.Image - if !strings.Contains(candidateRef, "/") { - candidateRef = "docker.io/library/" + candidateRef + candidateParsed, err := parser.Parse(candidate.Image.Image) + if err != nil { + continue } - if candidate.Namespace == target.Namespace && candidateRef == targetRef { + if candidate.Namespace == target.Namespace && candidateParsed.Remote() == targetParsed.Remote() { return true } } From 4df5daa54972a4f65b6354fbf46cb68ec33e6013 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 16 Jun 2026 09:47:14 +0200 Subject: [PATCH 06/10] move provider URL suffix to constructor Signed-off-by: rafi --- devguard_target.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/devguard_target.go b/devguard_target.go index 3bc036e9..550d7e1b 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -16,9 +16,9 @@ import ( type DevGuardTarget struct { projectURL string - token string - tags []string - client devguard.HTTPClient + token string + tags []string + client devguard.HTTPClient } type DevGuardRequest struct { @@ -39,12 +39,12 @@ type projectAssetsResponse struct { func NewDevGuardTarget(token, projectURL string, tags []string) *DevGuardTarget { client := devguard.NewHTTPClient(token, projectURL) - + projectURL = projectURL + "/dn/devguard-operator" return &DevGuardTarget{ projectURL: projectURL, - token: token, - tags: tags, - client: client, + token: token, + tags: tags, + client: client, } } @@ -105,9 +105,8 @@ func (g *DevGuardTarget) ProcessSbom(ctx *TargetContext) error { if err != nil { return err } - providerID := "devguard-operator" - url := g.projectURL + "/dn/:" + providerID - req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonBody))) + + req, err := http.NewRequest("POST", g.projectURL, strings.NewReader(string(jsonBody))) if err != nil { return err } From 9336d0f7172e9e26c0076eb7a364ef166aa8bbb2 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 16 Jun 2026 10:07:13 +0200 Subject: [PATCH 07/10] check HTTP status in LoadImages response Signed-off-by: rafi --- devguard_target.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/devguard_target.go b/devguard_target.go index 550d7e1b..c8c5ee34 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "log/slog" "net/http" "strings" @@ -60,6 +61,10 @@ func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New("failed to load images from DevGuard: " + resp.Status) + } + var assets []projectAssetsResponse if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil { return nil, err From 4e511007771581b11979b2b2f6fd7e4a04f1fecc Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 16 Jun 2026 10:44:15 +0200 Subject: [PATCH 08/10] rename ProjectName and AssetName in structs Signed-off-by: rafi --- devguard_target.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/devguard_target.go b/devguard_target.go index c8c5ee34..21f01b09 100644 --- a/devguard_target.go +++ b/devguard_target.go @@ -23,18 +23,18 @@ type DevGuardTarget struct { } type DevGuardRequest struct { - Verb string `json:"verb"` - ProjectName string `json:"projectName"` - AssetName string `json:"assetName"` - AssetVersion string `json:"assetVersion"` - Sbom json.RawMessage `json:"sbom,omitempty"` + Verb string `json:"verb"` + ProjectExternalEntityID string `json:"projectExternalEntityId"` + AssetExternalEntityID string `json:"assetExternalEntityId"` + AssetVersion string `json:"assetVersion"` + Sbom json.RawMessage `json:"sbom,omitempty"` } type projectAssetsResponse struct { - ProjectName string `json:"projectName"` - Assets []struct { - Name string `json:"name"` - Versions []string `json:"versions"` + ProjectExternalEntityID string `json:"projectExternalEntityId"` + Assets []struct { + AssetExternalEntityID string `json:"assetExternalEntityId"` + Versions []string `json:"versions"` } `json:"assets"` } @@ -74,9 +74,9 @@ func (g *DevGuardTarget) LoadImages() ([]kubernetes.ImageInNamespace, error) { for _, a := range assets { for _, asset := range a.Assets { for _, version := range asset.Versions { - fullImage := asset.Name + ":" + version + fullImage := asset.AssetExternalEntityID + ":" + version result = append(result, kubernetes.ImageInNamespace{ - Namespace: a.ProjectName, + Namespace: a.ProjectExternalEntityID, Image: &libk8s.RegistryImage{ ImageID: fullImage, Image: fullImage, @@ -99,11 +99,11 @@ func (g *DevGuardTarget) ProcessSbom(ctx *TargetContext) error { } payload := DevGuardRequest{ - Verb: "update", - ProjectName: ctx.Pod.PodNamespace, - AssetName: assetName, - AssetVersion: version, - Sbom: json.RawMessage(ctx.Sbom), + Verb: "update", + ProjectExternalEntityID: ctx.Pod.PodNamespace, + AssetExternalEntityID: assetName, + AssetVersion: version, + Sbom: json.RawMessage(ctx.Sbom), } jsonBody, err := json.Marshal(payload) @@ -141,10 +141,10 @@ func (g *DevGuardTarget) Remove(images []kubernetes.ImageInNamespace) error { name, version := getRepoWithVersion(img.Image) payload := DevGuardRequest{ - Verb: "delete", - ProjectName: img.Namespace, - AssetName: name, - AssetVersion: version, + Verb: "delete", + ProjectExternalEntityID: img.Namespace, + AssetExternalEntityID: name, + AssetVersion: version, } jsonBody, err := json.Marshal(payload) From f4f9763b61e0688f6d36242fa500117b7596dbec Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 16 Jun 2026 10:49:58 +0200 Subject: [PATCH 09/10] update Go version to 1.26 Signed-off-by: rafi --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0bca12b0..792a025e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.4-alpine AS golang-builder +FROM golang:1.26.4-alpine3.24@sha256:f1ddd9fe14fffc091dd98cb4bfa999f32c5fc77d2f2305ea9f0e2595c5437c14 AS golang-builder # set the working directory WORKDIR /app diff --git a/go.mod b/go.mod index 30161bf9..9dadd519 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/l3montree-dev/devguard-operator -go 1.23.4 +go 1.26.3 require ( github.com/ckotzbauer/libk8soci v0.0.0-20240810135526-c1ac5a827c6b From 07f5d2295809dab9acd55f203219cc6282f27970 Mon Sep 17 00:00:00 2001 From: rafi Date: Tue, 16 Jun 2026 11:00:44 +0200 Subject: [PATCH 10/10] update go mod Signed-off-by: rafi --- go.mod | 2 -- go.sum | 4 ---- 2 files changed, 6 deletions(-) diff --git a/go.mod b/go.mod index 9dadd519..07d10e0f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.26.3 require ( github.com/ckotzbauer/libk8soci v0.0.0-20240810135526-c1ac5a827c6b github.com/ckotzbauer/libstandard v0.0.0-20240714072944-bb20d4a8e76a - github.com/gosimple/slug v1.14.0 github.com/l3montree-dev/devguard v0.5.15 github.com/lmittmann/tint v1.0.5 github.com/novln/docker-parser v1.0.0 @@ -52,7 +51,6 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/iancoleman/strcase v0.3.0 // indirect diff --git a/go.sum b/go.sum index 7ef99d92..a0ba5f38 100644 --- a/go.sum +++ b/go.sum @@ -150,10 +150,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= -github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= -github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= -github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=