diff --git a/auto-discovery/cloud-aws/README.md b/auto-discovery/cloud-aws/README.md index e37cc4cdd3..711f52b704 100644 --- a/auto-discovery/cloud-aws/README.md +++ b/auto-discovery/cloud-aws/README.md @@ -203,14 +203,14 @@ This means the AWS AutoDiscovery should either be free or cheaper than $1/month | config.aws | object | `{"queueUrl":"","region":""}` | settings to connect to AWS and receive the updates | | config.aws.queueUrl | string | `""` | url of the SQS queue which receives the state changes. Can be overridden by setting the SQS_QUEUE_URL environment variable. | | config.aws.region | string | `""` | aws region to connect to. Can be overridden by setting the AWS_REGION environment variable. | -| config.kubernetes | object | `{"scanConfigs":[{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-image"},{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy-sbom","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-sbom-image"}]}` | settings to configure how scans get created in kubernetes | +| config.kubernetes | object | `{"scanConfigs":[{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-image"},{"annotations":{"dependencytrack.securecodebox.io/project-name":"{{ .Image.ShortName }}","dependencytrack.securecodebox.io/project-version":"{{ .Image.Version }}"},"hookSelector":{},"labels":{},"name":"trivy-sbom","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-sbom-image"}]}` | settings to configure how scans get created in kubernetes | | config.kubernetes.scanConfigs[0].annotations | object | `{}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | | config.kubernetes.scanConfigs[0].hookSelector | object | `{}` | hookSelector allows to specify a LabelSelector with which the hooks are selected, see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors Both matchLabels and matchExpressions are supported. All values in the matchLabels map support templating. MatchExpressions support templating in the `key` field and in every entry in the `values` list. If a value in the list renders to an empty string it is removed from the list. | | config.kubernetes.scanConfigs[0].labels | object | `{}` | labels to be added to the scans started by the auto-discovery, all label values support templating | | config.kubernetes.scanConfigs[0].name | string | `"trivy"` | unique name to distinguish scans | | config.kubernetes.scanConfigs[0].parameters | list | `["{{ .ImageID }}"]` | parameters used for the scans created by the containerAutoDiscovery, all parameters support templating | | config.kubernetes.scanConfigs[0].repeatInterval | string | `"168h"` | interval in which scans are automatically repeated. If the target is updated (meaning a new image revision is deployed) the scan will repeated beforehand and the interval is reset. | -| config.kubernetes.scanConfigs[1].annotations | object | `{}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | +| config.kubernetes.scanConfigs[1].annotations | object | `{"dependencytrack.securecodebox.io/project-name":"{{ .Image.ShortName }}","dependencytrack.securecodebox.io/project-version":"{{ .Image.Version }}"}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | | config.kubernetes.scanConfigs[1].hookSelector | object | `{}` | hookSelector allows to specify a LabelSelector with which the hooks are selected, see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors Both matchLabels and matchExpressions are supported. All values in the matchLabels map support templating. MatchExpressions support templating in the `key` field and in every entry in the `values` list. If a value in the list renders to an empty string it is removed from the list. | | config.kubernetes.scanConfigs[1].labels | object | `{}` | labels to be added to the scans started by the auto-discovery, all label values support templating | | config.kubernetes.scanConfigs[1].name | string | `"trivy-sbom"` | unique name to distinguish scans | diff --git a/auto-discovery/cloud-aws/cmd/service/main_test.go b/auto-discovery/cloud-aws/cmd/service/main_test.go index 0993c856a1..1ca311f709 100644 --- a/auto-discovery/cloud-aws/cmd/service/main_test.go +++ b/auto-discovery/cloud-aws/cmd/service/main_test.go @@ -40,7 +40,7 @@ var _ = Describe("Integration tests", func() { juiceShopScanName2 = juiceShopScanName2[:62] juiceShopScanGoTemplate := scanGoTemplate{ - map[string]string{"testAnnotation": "VeryUniqueId"}, + map[string]string{"testAnnotation": "bkimminich/juice-shop"}, map[string]string{ "testLabel": "VeryUniqueId", "app.kubernetes.io/managed-by": "securecodebox-autodiscovery", @@ -57,7 +57,7 @@ var _ = Describe("Integration tests", func() { helloWorldScanName2 = helloWorldScanName2[:62] helloWorldScanGoTemplate := scanGoTemplate{ - map[string]string{"testAnnotation": "ExtremelyUniqueId"}, + map[string]string{"testAnnotation": "library/hello-world"}, map[string]string{ "testLabel": "ExtremelyUniqueId", "app.kubernetes.io/managed-by": "securecodebox-autodiscovery", diff --git a/auto-discovery/cloud-aws/cmd/service/suite_test.go b/auto-discovery/cloud-aws/cmd/service/suite_test.go index 41f86fa8f4..feafa62f3d 100644 --- a/auto-discovery/cloud-aws/cmd/service/suite_test.go +++ b/auto-discovery/cloud-aws/cmd/service/suite_test.go @@ -90,7 +90,7 @@ var _ = BeforeSuite(func() { { Name: "test-scan", RepeatInterval: metav1.Duration{Duration: time.Hour}, - Annotations: map[string]string{"testAnnotation": "{{ .Target.Id }}"}, + Annotations: map[string]string{"testAnnotation": "{{ .Image.ShortName }}"}, Labels: map[string]string{"testLabel": "{{ .Target.Id }}"}, Parameters: []string{"{{ .ImageID }}"}, ScanType: "trivy-sbom-image", @@ -111,7 +111,7 @@ var _ = BeforeSuite(func() { { Name: "test-scan-two", RepeatInterval: metav1.Duration{Duration: time.Hour}, - Annotations: map[string]string{"testAnnotation": "{{ .Target.Id }}"}, + Annotations: map[string]string{"testAnnotation": "{{ .Image.ShortName }}"}, Labels: map[string]string{"testLabel": "{{ .Target.Id }}"}, Parameters: []string{"{{ .ImageID }}"}, ScanType: "trivy-sbom-image", diff --git a/auto-discovery/cloud-aws/docs/README.ArtifactHub.md b/auto-discovery/cloud-aws/docs/README.ArtifactHub.md index a543b9160c..7d4fea1b22 100644 --- a/auto-discovery/cloud-aws/docs/README.ArtifactHub.md +++ b/auto-discovery/cloud-aws/docs/README.ArtifactHub.md @@ -195,14 +195,14 @@ This means the AWS AutoDiscovery should either be free or cheaper than $1/month | config.aws | object | `{"queueUrl":"","region":""}` | settings to connect to AWS and receive the updates | | config.aws.queueUrl | string | `""` | url of the SQS queue which receives the state changes. Can be overridden by setting the SQS_QUEUE_URL environment variable. | | config.aws.region | string | `""` | aws region to connect to. Can be overridden by setting the AWS_REGION environment variable. | -| config.kubernetes | object | `{"scanConfigs":[{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-image"},{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy-sbom","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-sbom-image"}]}` | settings to configure how scans get created in kubernetes | +| config.kubernetes | object | `{"scanConfigs":[{"annotations":{},"hookSelector":{},"labels":{},"name":"trivy","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-image"},{"annotations":{"dependencytrack.securecodebox.io/project-name":"{{ .Image.ShortName }}","dependencytrack.securecodebox.io/project-version":"{{ .Image.Version }}"},"hookSelector":{},"labels":{},"name":"trivy-sbom","parameters":["{{ .ImageID }}"],"repeatInterval":"168h","scanType":"trivy-sbom-image"}]}` | settings to configure how scans get created in kubernetes | | config.kubernetes.scanConfigs[0].annotations | object | `{}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | | config.kubernetes.scanConfigs[0].hookSelector | object | `{}` | hookSelector allows to specify a LabelSelector with which the hooks are selected, see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors Both matchLabels and matchExpressions are supported. All values in the matchLabels map support templating. MatchExpressions support templating in the `key` field and in every entry in the `values` list. If a value in the list renders to an empty string it is removed from the list. | | config.kubernetes.scanConfigs[0].labels | object | `{}` | labels to be added to the scans started by the auto-discovery, all label values support templating | | config.kubernetes.scanConfigs[0].name | string | `"trivy"` | unique name to distinguish scans | | config.kubernetes.scanConfigs[0].parameters | list | `["{{ .ImageID }}"]` | parameters used for the scans created by the containerAutoDiscovery, all parameters support templating | | config.kubernetes.scanConfigs[0].repeatInterval | string | `"168h"` | interval in which scans are automatically repeated. If the target is updated (meaning a new image revision is deployed) the scan will repeated beforehand and the interval is reset. | -| config.kubernetes.scanConfigs[1].annotations | object | `{}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | +| config.kubernetes.scanConfigs[1].annotations | object | `{"dependencytrack.securecodebox.io/project-name":"{{ .Image.ShortName }}","dependencytrack.securecodebox.io/project-version":"{{ .Image.Version }}"}` | annotations to be added to the scans started by the auto-discovery, all annotation values support templating | | config.kubernetes.scanConfigs[1].hookSelector | object | `{}` | hookSelector allows to specify a LabelSelector with which the hooks are selected, see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors Both matchLabels and matchExpressions are supported. All values in the matchLabels map support templating. MatchExpressions support templating in the `key` field and in every entry in the `values` list. If a value in the list renders to an empty string it is removed from the list. | | config.kubernetes.scanConfigs[1].labels | object | `{}` | labels to be added to the scans started by the auto-discovery, all label values support templating | | config.kubernetes.scanConfigs[1].name | string | `"trivy-sbom"` | unique name to distinguish scans | diff --git a/auto-discovery/cloud-aws/pkg/kubernetes/docker_reference.go b/auto-discovery/cloud-aws/pkg/kubernetes/docker_reference.go index 7e4a51b7f3..a6986bfe21 100644 --- a/auto-discovery/cloud-aws/pkg/kubernetes/docker_reference.go +++ b/auto-discovery/cloud-aws/pkg/kubernetes/docker_reference.go @@ -17,6 +17,17 @@ type ImageInfo struct { parsed *dockerparser.Reference } +// Image details for templating that would otherwise not be accessible because you need to call functions +type ImageDetails struct { + Id string + Name string + Digest string + Version string + Registry string + Repository string + ShortName string +} + // Use dockerparser to normalize the image reference and allow easy access to the properties func (image *ImageInfo) normalize() error { // To prevent misdetection of containers using the same digest but different tags (i.e. none @@ -33,8 +44,21 @@ func (image *ImageInfo) normalize() error { return nil } +// Create object with all the values that would only be accessible by calling functions +func (image *ImageInfo) details() ImageDetails { + return ImageDetails{ + Id: image.reference(), + Name: image.Name, + Digest: image.Digest, + Version: image.version(), + Registry: image.registry(), + Repository: image.repository(), + ShortName: image.shortName(), + } +} + // Get a short, representative name for the image -func (image *ImageInfo) appName() string { +func (image *ImageInfo) shortName() string { // If the image is parsed or parsing works use library function if image.parsed != nil || image.normalize() == nil { return image.parsed.ShortName() @@ -121,3 +145,31 @@ func (image *ImageInfo) reference() string { return image.Name + "@" + image.Digest } } + +// Get the registry of this image, mirrors the dockerparser function +func (image *ImageInfo) registry() string { + // If the image is parsed or parsing works use library function + if image.parsed != nil || image.normalize() == nil { + return image.parsed.Registry() + } + + // Parsing failed, try to salvage this somehow by returning what we have + // If name contains a port, domain or localhost that is the registry + split := strings.Split(image.Name, "/") + if strings.Contains(split[0], ":") || strings.Contains(split[0], ".") || split[0] == "localhost" { + return split[0] + } else { + return "docker.io" + } +} + +// Get the repository of this image, mirrors the dockerparser function +func (image *ImageInfo) repository() string { + // If the image is parsed or parsing works use library function + if image.parsed != nil || image.normalize() == nil { + return image.parsed.Repository() + } + + // Parsing failed, try to salvage this somehow by returning what we have + return image.registry() + "/" + image.shortName() +} diff --git a/auto-discovery/cloud-aws/pkg/kubernetes/kubernetes.go b/auto-discovery/cloud-aws/pkg/kubernetes/kubernetes.go index f8caaa5201..e26373dbd2 100644 --- a/auto-discovery/cloud-aws/pkg/kubernetes/kubernetes.go +++ b/auto-discovery/cloud-aws/pkg/kubernetes/kubernetes.go @@ -65,6 +65,7 @@ type ContainerAutoDiscoveryTemplateArgs struct { Config config.AutoDiscoveryConfig ScanConfig configv1.ScanConfig Target ContainerInfo + Image ImageDetails ImageID string } @@ -207,6 +208,7 @@ func getScheduledScanForRequest(req Request, cfg *config.AutoDiscoveryConfig, sc Config: *cfg, ScanConfig: scanConfig, Target: req.Container, + Image: req.Container.Image.details(), ImageID: req.Container.Image.reference(), } scanSpec := util.GenerateScanSpec(scanConfig, templateArgs) @@ -227,7 +229,7 @@ func getScanName(req Request, name string) string { // adapted from the kubernetes container autodiscovery // function builds string like: _appName_-_customScanName_-at-_imageID_HASH_ eg: nginx-myTrivyScan-at-0123456789 - appName := req.Container.Image.appName() + appName := req.Container.Image.shortName() hash := req.Container.Image.hash() // cutoff appname if it is longer than 20 chars diff --git a/auto-discovery/cloud-aws/values.yaml b/auto-discovery/cloud-aws/values.yaml index aed54f1ed6..56e1783dab 100644 --- a/auto-discovery/cloud-aws/values.yaml +++ b/auto-discovery/cloud-aws/values.yaml @@ -49,7 +49,9 @@ config: # -- labels to be added to the scans started by the auto-discovery, all label values support templating labels: {} # -- annotations to be added to the scans started by the auto-discovery, all annotation values support templating - annotations: {} + annotations: + dependencytrack.securecodebox.io/project-name: "{{ .Image.ShortName }}" + dependencytrack.securecodebox.io/project-version: "{{ .Image.Version }}" # -- hookSelector allows to specify a LabelSelector with which the hooks are selected, see: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors # Both matchLabels and matchExpressions are supported. # All values in the matchLabels map support templating. diff --git a/hooks/persistence-dependencytrack/.helm-docs.gotmpl b/hooks/persistence-dependencytrack/.helm-docs.gotmpl index 3425e839e3..a1eb4627c8 100644 --- a/hooks/persistence-dependencytrack/.helm-docs.gotmpl +++ b/hooks/persistence-dependencytrack/.helm-docs.gotmpl @@ -50,6 +50,13 @@ SBOMs are imported for a project in Dependency-Track. To avoid configuring all of them by hand first and assigning projects to scans somehow, the hook automatically detects name and version from the scan and then creates Dependency-Track projects if they do not exist yet. This requires either the `PORTFOLIO_MANAGEMENT` or `PROJECT_CREATION_UPLOAD` permission for the API key which gets used by the hook (or rather for the team the key is defined for). +The hook extracts name and version from the reference of the scanned docker image. +For more fine grained control how the projects are matched, you can configure the name and version as annotations to the scan. + +| Scan Annotation | Description | Default if not set | +| -------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------- | +| `dependencytrack.securecodebox.io/project-name` | Name of the Project | Repository Name of the Docker Image | +| `dependencytrack.securecodebox.io/project-version` | Version of the Project | Image Tag if avialable, otherwise Image Digest if available, otherwise `latest` | {{- end }} {{- define "extra.scannerLinksSection" -}} diff --git a/hooks/persistence-dependencytrack/README.md b/hooks/persistence-dependencytrack/README.md index 9ad2f636d6..1b65daadc3 100644 --- a/hooks/persistence-dependencytrack/README.md +++ b/hooks/persistence-dependencytrack/README.md @@ -69,6 +69,14 @@ SBOMs are imported for a project in Dependency-Track. To avoid configuring all of them by hand first and assigning projects to scans somehow, the hook automatically detects name and version from the scan and then creates Dependency-Track projects if they do not exist yet. This requires either the `PORTFOLIO_MANAGEMENT` or `PROJECT_CREATION_UPLOAD` permission for the API key which gets used by the hook (or rather for the team the key is defined for). +The hook extracts name and version from the reference of the scanned docker image. +For more fine grained control how the projects are matched, you can configure the name and version as annotations to the scan. + +| Scan Annotation | Description | Default if not set | +| -------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------- | +| `dependencytrack.securecodebox.io/project-name` | Name of the Project | Repository Name of the Docker Image | +| `dependencytrack.securecodebox.io/project-version` | Version of the Project | Image Tag if avialable, otherwise Image Digest if available, otherwise `latest` | + ## Values | Key | Type | Default | Description | diff --git a/hooks/persistence-dependencytrack/hook/hook.js b/hooks/persistence-dependencytrack/hook/hook.js index 54977895e2..27902791e1 100644 --- a/hooks/persistence-dependencytrack/hook/hook.js +++ b/hooks/persistence-dependencytrack/hook/hook.js @@ -24,9 +24,17 @@ async function handle({ console.log(`Persisting SBOM for ${result.metadata.component.name} to Dependency-Track`); - // Get the project name and version from the name attribute of the main component - // This might be a bit brittle, but there is not really a better way to get this information - // Neither Trivy's nor Syft's SBOM contains a useful version attribute (none or sha256) + // Try to get the project name and version from annotations + let name, version + if (scan?.metadata?.annotations) { + name = scan.metadata.annotations["dependencytrack.securecodebox.io/project-name"] + version = scan.metadata.annotations["dependencytrack.securecodebox.io/project-version"] + } + + // Get the project name and version from the name attribute of the main component if the + // annotations are missing. This might be a bit brittle, but there is not really a better way to + // get this information in that case, neither Trivy's nor Syft's SBOM contains a useful version + // attribute (none or sha256) // Get the components of a docker image reference, the regex is a direct JavaScript adaption of // the official Go-implementation available at https://github.com/distribution/reference/blob/main/regexp.go @@ -45,8 +53,8 @@ async function handle({ '(?:@(?[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}))?$', ].join('')); const groups = imageRegex.exec(result.metadata.component.name).groups - const name = groups.name - const version = groups.tag || groups.digest || "latest" + name = name || groups.name + version = version || groups.tag || groups.digest || "latest" // The POST endpoint expects multipart/form-data // Alternatively the PUT endpoint could be used, which requires base64-encoding the SBOM diff --git a/hooks/persistence-dependencytrack/hook/hook.test.js b/hooks/persistence-dependencytrack/hook/hook.test.js index 036b0ef445..2d3f8a7ff3 100644 --- a/hooks/persistence-dependencytrack/hook/hook.test.js +++ b/hooks/persistence-dependencytrack/hook/hook.test.js @@ -89,6 +89,10 @@ test("should send a post request to the url when fired", async () => { metadata: { uid: "69e71358-bb01-425b-9bde-e45653605490", name: "demo-sbom", + annotations: { + "dependencytrack.securecodebox.io/project-name": "Hello World Container", + "dependencytrack.securecodebox.io/project-version": "latest and greatest" + } }, status: { rawResultType: "sbom-cyclonedx" @@ -110,6 +114,8 @@ test("should send a post request to the url when fired", async () => { })); expect(fetch.mock.calls[0][1].body.get("bom")).toBe(JSON.stringify(result)); + expect(fetch.mock.calls[0][1].body.get("projectName")).toBe("Hello World Container"); + expect(fetch.mock.calls[0][1].body.get("projectVersion")).toBe("latest and greatest"); }); // Make sure that the crazy regex to parse the reference parts actually works