From 8d4d729345e2fbff07f02e979f01dd446aefe66c Mon Sep 17 00:00:00 2001 From: Christian Kotzbauer Date: Sat, 20 Aug 2022 16:39:00 +0200 Subject: [PATCH] feat: Add Pod-Informer (#151) * feat: added pod-informer Signed-off-by: Christian Kotzbauer * fix: lint errors Signed-off-by: Christian Kotzbauer * fix: added logging and fixed async handling Signed-off-by: Christian Kotzbauer * fix: several refactoring-fixes Signed-off-by: Christian Kotzbauer * feat: add startup-sync Signed-off-by: Christian Kotzbauer * doc: update readme Signed-off-by: Christian Kotzbauer * feat: add new syft-formats Signed-off-by: Christian Kotzbauer * doc: remove link Signed-off-by: Christian Kotzbauer * fix: change reference types Signed-off-by: Christian Kotzbauer * fix: remove add-handler, change to trace Signed-off-by: Christian Kotzbauer * deps: cleanup go.mod Signed-off-by: Christian Kotzbauer Signed-off-by: Christian Kotzbauer --- README.md | 129 ++++++--- go.mod | 8 +- go.sum | 19 +- internal/daemon/daemon.go | 148 +--------- internal/job/job.go | 60 ++-- internal/kubernetes/kubernetes.go | 124 +++++---- internal/processor/processor.go | 348 ++++++++++++++++++++++++ internal/syft/syft.go | 20 +- internal/syft/syft_test.go | 8 +- internal/target/dtrack/dtrack_target.go | 153 ++++++++--- internal/target/git/git_target.go | 108 ++++---- internal/target/oci/oci_target.go | 14 +- internal/target/oci/oci_target_test.go | 6 +- internal/target/target.go | 7 +- main.go | 14 +- 15 files changed, 763 insertions(+), 403 deletions(-) create mode 100644 internal/processor/processor.go diff --git a/README.md b/README.md index 7e538c9f..5c267dc2 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ ## Overview This operator maintains a central place to track all packages and software used in all those images in a Kubernetes cluster. For this a Software Bill of -Materials (SBOM) is generated from each image with Syft. They are all stored in one or more targets. Currently Git and Dependency Track is supported. -With this it is possible to do further analysis, vulnerability scans and much more in a single place. To prevent scans of images that have already been analyzed pods are annotated -with the imageID of the already processed image. +Materials (SBOM) is generated from each image with Syft. They are all stored in one or more targets. Currently Git, Dependency Track and OCI-Registry are supported. +With this it is possible to do further analysis, [vulnerability scans](https://github.com/ckotzbauer/vulnerability-operator) and much more in a single place. +To prevent scans of images that have already been analyzed pods are annotated with the imageID of the already processed image. ## Kubernetes Compatibility @@ -68,38 +68,23 @@ helm install ckotzbauer/sbom-operator -f your-values.yaml ## Configuration -All parameters are cli-flags. +All parameters are cli-flags. The flags can be configured as args or as environment-variables prefixed with `SBOM_` to inject sensitive configs as secret values. + +### Common parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `verbosity` | `false` | `info` | Log-level (debug, info, warn, error, fatal, panic) | -| `cron` | `false` | `@hourly` | Backround-Service interval (CRON). All options from [github.com/robfig/cron](https://github.com/robfig/cron) are allowed | +| `cron` | `false` | `""` | Backround-Service interval (CRON). See [Trigger](#analysis-trigger) for details. | | `ignore-annotations` | `false` | `false` | Force analyzing of all images, including those from annotated pods. | -| `format` | `false` | `json` | SBOM-Format. | -| `targets` | `false` | `git` | Comma-delimited list of targets to sent the generated SBOMs to. Possible targets `git`, `dtrack`, `oci` | -| `git-workingtree` | `false` | `/work` | Directory to place the git-repo. | -| `git-repository` | `true` when `git` target is used. | `""` | Git-Repository-URL (HTTPS). | -| `git-branch` | `false` | `main` | Git-Branch to checkout. | -| `git-path` | `false` | `""` | Folder-Path inside the Git-Repository. | -| `git-access-token` | `true` when `git` target is used. | `""` | Git-Personal-Access-Token with write-permissions. | -| `git-author-name` | `true` when `git` target is used. | `""` | Author name to use for Git-Commits. | -| `git-author-email` | `true` when `git` target is used. | `""` | Author email to use for Git-Commits. | +| `format` | `false` | `json` | SBOM-Format. (One of `json`, `syftjson`, `cyclonedxjson`, `spdxjson`, `github`, `githubjson`, `cyclonedx`, `cyclone`, `cyclonedxxml`, `spdx`, `spdxtv`, `spdxtagvalue`, `text`, `table`) | +| `targets` | `false` | `git` | Comma-delimited list of targets to sent the generated SBOMs to. Possible targets `git`, `dtrack`, `oci`. Ignored with a `job-image` | | `pod-label-selector` | `false` | `""` | Kubernetes Label-Selector for pods. | | `namespace-label-selector` | `false` | `""` | Kubernetes Label-Selector for namespaces. | -| `dtrack-base-url` | `true` when `dtrack` target is used | `""` | Dependency-Track base URL, e.g. 'https://dtrack.example.com' | -| `dtrack-api-key` | `true` when `dtrack` target is used | `""` | Dependency-Track API key | -| `kubernetes-cluster-id` | `false` | `"default"` | Kubernetes Cluster ID (to be used in Dependency-Track or Job-Images) | | `fallback-image-pull-secret` | `false` | `""` | Kubernetes Pull-Secret Name to load as a fallback when all others fail (must be in the same namespace as the sbom-operator) | -| `job-image` | `false` | `""` | Job-Image to process images with instead of Syft | -| `job-image-pull-secret` | `false` | `""` | Pre-existing pull-secret-name for private job-images | -| `job-timeout` | `false` | `3600` | Job-Timeout in seconds (`activeDeadlineSeconds`) | -| `oci-registry` | `true` when `oci` target is used | `""` | OCI-Registry | -| `oci-user` | `true` when `oci` target is used | `""` | OCI-User | -| `oci-token` | `true` when `oci` target is used | `""` | OCI-Token | -The flags can be configured as args or as environment-variables prefixed with `SBOM_` to inject sensitive configs as secret values. -#### Example Helm-Config +### Example Helm-Config ```yaml args: @@ -119,22 +104,58 @@ envVars: key: "accessToken" ``` +## Analysis-Trigger + +### Cron + +With the `cron` flag set, the operator runs with a specified interval and checks for changed images in your cluster. +All options from [github.com/robfig/cron](https://github.com/robfig/cron) are allowed as cron-syntax. + +### Real-Time + +When you omit the `cron` flag, the operator uses a Cache-Informer to process changed pods immediately. In this mode there's also +a one-time analysis at startup to sync the targets with the actual cluster-state. If you configured a job-image there's no initial +startup sync. + ## Targets -It is possible to store the generated SBOMs to different targets (even multple at once). +It is possible to store the generated SBOMs to different targets (even multple at once). All targets are using Syft as analyzer. +If you want to use another tool to analyze your images, then have a look at the [Job image](#job-images) section. Images which are +not present in the cluster anymore are removed from the configured targets (except for the OCI-Target). + +### Dependency Track + +#### Dependency Track Parameter -#### Dependency Track +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `dtrack-base-url` | `true` when `dtrack` target is used | `""` | Dependency-Track base URL, e.g. 'https://dtrack.example.com' | +| `dtrack-api-key` | `true` when `dtrack` target is used | `""` | Dependency-Track API key | +| `kubernetes-cluster-id` | `false` | `"default"` | Kubernetes Cluster ID (to be used in Dependency-Track or Job-Images) | -Each image in the cluster is created as project with the full-image name (registry and image-path without tag) and the image-tag as project-version. +Each image in the cluster is created as project with the full-image name (registry and image-path without tag) and the image-tag as project-version. +When there's no image-tag, but a digest, the digest is used as project-version. The `autoCreate` option of DT is used. You have to set the `--format` flag to `cyclonedx` with this target. -#### Git +### Git + +#### Git Parameter + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `git-workingtree` | `false` | `/work` | Directory to place the git-repo. | +| `git-repository` | `true` when `git` target is used. | `""` | Git-Repository-URL (HTTPS). | +| `git-branch` | `false` | `main` | Git-Branch to checkout. | +| `git-path` | `false` | `""` | Folder-Path inside the Git-Repository. | +| `git-access-token` | `true` when `git` target is used. | `""` | Git-Personal-Access-Token with write-permissions. | +| `git-author-name` | `true` when `git` target is used. | `""` | Author name to use for Git-Commits. | +| `git-author-email` | `true` when `git` target is used. | `""` | Author email to use for Git-Commits. | The operator will save all files with a specific folder structure as described below. When a `git-path` is configured, all folders above this path are not touched from the application. Assuming that `git-path` is set to `dev-cluster/sboms`. When no `git-path` is given, the structure below is directly in the repository-root. -The structure is basically `////sbom.json`. The file-extension may differ when another output-format is configured. A token-based authentication to the git-repository is used. +The structure is basically `////sbom.json`. The file-extension may differ when another output-format is configured. A token-based authentication to the git-repository is used (PAT). ``` dev-cluster @@ -170,7 +191,15 @@ dev-cluster │ sbom.json ``` -#### OCI-Registry +### OCI-Registry + +#### OCI-Registry Parameter + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `oci-registry` | `true` when `oci` target is used | `""` | OCI-Registry | +| `oci-user` | `true` when `oci` target is used | `""` | OCI-User | +| `oci-token` | `true` when `oci` target is used | `""` | OCI-Token | In this mode the operator will generate a SBOM and store it into an OCI-Registry. The SBOM then can be processed by cosign, Kyverno or any other tool. E.g.: @@ -178,9 +207,20 @@ or any other tool. E.g.: COSIGN_REPOSITORY= cosign download sbom ``` +The operator needs the Registry-URL, a user and a token as password to authenticate to the registry. Write-permissions are needed. + ## Job-Images +#### Job-Image Parameter + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `job-image` | `false` | `""` | Job-Image to process images with instead of Syft | +| `job-image-pull-secret` | `false` | `""` | Pre-existing pull-secret-name for private job-images | +| `job-timeout` | `false` | `3600` | Job-Timeout in seconds (`activeDeadlineSeconds`) | +| `kubernetes-cluster-id` | `false` | `"default"` | Kubernetes Cluster ID (to be used in Dependency-Track or Job-Images) | + If you don't want to use Syft to analyze your images, you can give the Job-Image feature a try. The operator creates a Kubernetes-Job which does the analysis with any possible tool inside. There's no target-handling done by the operator, the tool from the job has to process the SBOMs on its own. Currently there are two possible integrations: @@ -214,12 +254,33 @@ All operator-environment variables prefixed with `SBOM_JOB_` are passed to the K The docker-image is based on `scratch` to reduce the attack-surface and keep the image small. Furthermore the image and release-artifacts are signed with [cosign](https://github.com/sigstore/cosign) and attested with provenance-files. The release-process satisfies SLSA Level 2. All of those "metadata files" are also stored in a dedicated repository `ghcr.io/ckotzbauer/sbom-operator-metadata`. -Both, SLSA and the signatures are still experimental for this project. When discovering security issues please refer to the [Security process](https://github.com/ckotzbauer/.github/blob/main/SECURITY.md). +### Signature verification + +```bash +COSIGN_EXPERIMENTAL=1 COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign verify ghcr.io/ckotzbauer/sbom-operator: --certificate-github-workflow-name create-release --certificate-github-workflow-repository ckotzbauer/sbom-operator +``` + +### Attestation verification + +```bash +COSIGN_EXPERIMENTAL=1 COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign verify-attestation ghcr.io/ckotzbauer/sbom-operator: --certificate-github-workflow-name create-release --certificate-github-workflow-repository ckotzbauer/sbom-operator +``` + +### Download attestation + +```bash +COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign download attestation ghcr.io/ckotzbauer/sbom-operator: | jq -r '.payload' | base64 -d +``` + +### Download SBOM + +```bash +COSIGN_REPOSITORY=ghcr.io/ckotzbauer/sbom-operator-metadata cosign download sbom ghcr.io/ckotzbauer/sbom-operator: | jq -r '.payload' | base64 -d +``` + -[Contributing](https://github.com/ckotzbauer/sbom-operator/blob/master/CONTRIBUTING.md) --------- [License](https://github.com/ckotzbauer/sbom-operator/blob/master/LICENSE) -------- [Changelog](https://github.com/ckotzbauer/sbom-operator/blob/master/CHANGELOG.md) diff --git a/go.mod b/go.mod index 65ff72a6..611bac2a 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,19 @@ go 1.19 require ( github.com/anchore/syft v0.54.0 - github.com/ckotzbauer/libk8soci v0.0.0-20220801045234-0c88accfdf59 + github.com/ckotzbauer/libk8soci v0.0.0-20220820074711-9ebdb60394e6 github.com/ckotzbauer/libstandard v0.0.0-20220801044619-e3c9900286ea + github.com/google/uuid v1.3.0 github.com/novln/docker-parser v1.0.0 github.com/nscuro/dtrack-client v0.6.0 github.com/robfig/cron v1.2.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.0 + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e k8s.io/api v0.24.4 k8s.io/apimachinery v0.24.4 + k8s.io/client-go v0.24.4 ) require ( @@ -52,7 +55,6 @@ require ( github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect @@ -107,7 +109,6 @@ require ( github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect golang.org/x/tools v0.1.11 // indirect @@ -115,7 +116,6 @@ require ( google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect google.golang.org/grpc v1.48.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/client-go v0.24.3 // indirect lukechampine.com/uint128 v1.1.1 // indirect modernc.org/cc/v3 v3.36.0 // indirect modernc.org/ccgo/v3 v3.16.6 // indirect diff --git a/go.sum b/go.sum index b52756db..4346933a 100644 --- a/go.sum +++ b/go.sum @@ -145,12 +145,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7 h1:kDrYkTSM9uIxaX/P9s0F4nKYNM+hnSgLJdLpqvsaQ/g= github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= -github.com/anchore/stereoscope v0.0.0-20220803153229-c55b13fee7e4 h1:OMc0B7MxfjfqagdgboPFVJzsDJbFk7J7NXhgTTnhvuo= -github.com/anchore/stereoscope v0.0.0-20220803153229-c55b13fee7e4/go.mod h1:90tB0wMdDe2V8fB52tPf1xjg/ieLoWayRu8YJNd9c7w= github.com/anchore/stereoscope v0.0.0-20220808115346-84004345484e h1:W13WKIHqgENdcIg49GsG2GJx2BnIg5rpI/gE2Bp/IRQ= github.com/anchore/stereoscope v0.0.0-20220808115346-84004345484e/go.mod h1:90tB0wMdDe2V8fB52tPf1xjg/ieLoWayRu8YJNd9c7w= -github.com/anchore/syft v0.53.4 h1:tRiKa8ZL2FpDzzrcBHms4E6lgsmOkVbITrlcM35aKc0= -github.com/anchore/syft v0.53.4/go.mod h1:Ms14EskPVOazbPO1j38sxR8kki0uL13xzV5fcuoQDBY= github.com/anchore/syft v0.54.0 h1:PNG6KHsO/KIGEivZgWTgAVgwISK/TfrRzocCEwK8WCQ= github.com/anchore/syft v0.54.0/go.mod h1:T+cDACkaFFl+ARWxLUS2Iri5agi4017wBRQUq5ZAAmE= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= @@ -232,8 +228,8 @@ github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLI github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/ckotzbauer/libk8soci v0.0.0-20220801045234-0c88accfdf59 h1:xQP71LgWiWEemWGejG3qcWuELsnpIbZqhGCsWkUqB2I= -github.com/ckotzbauer/libk8soci v0.0.0-20220801045234-0c88accfdf59/go.mod h1:TvU933QZzcyaCRyYl8cEK7RbKY/nPlSscc7mq9jueOU= +github.com/ckotzbauer/libk8soci v0.0.0-20220820074711-9ebdb60394e6 h1:ZqQ3DO7vBTpfWZNZAVpqsbVT1q07IoEUbNaxbWPQTcU= +github.com/ckotzbauer/libk8soci v0.0.0-20220820074711-9ebdb60394e6/go.mod h1:IBQEVvyUDhkBa15BRG/Bx0iojbJA+4fqj8IpppI1Ylw= github.com/ckotzbauer/libstandard v0.0.0-20220801044619-e3c9900286ea h1:gofNE9np8uUttsOnPSOhKrMMj0nLn4+59ZwOP+Yu7Qo= github.com/ckotzbauer/libstandard v0.0.0-20220801044619-e3c9900286ea/go.mod h1:u4cKHuLhOIvu/X02luI4k32oH//Hlz9PxjEA7kj0Evs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -418,8 +414,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= @@ -1907,7 +1901,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -1971,15 +1964,11 @@ honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY= k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/api v0.24.3 h1:tt55QEmKd6L2k5DP6G/ZzdMQKvG5ro4H4teClqm0sTY= -k8s.io/api v0.24.3/go.mod h1:elGR/XSZrS7z7cSZPzVWaycpJuGIw57j9b95/1PdJNI= k8s.io/api v0.24.4 h1:I5Y645gJ8zWKawyr78lVfDQkZrAViSbeRXsPZWTxmXk= k8s.io/api v0.24.4/go.mod h1:42pVfA0NRxrtJhZQOvRSyZcJihzAdU59WBtTjYcB0/M= k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apimachinery v0.24.3 h1:hrFiNSA2cBZqllakVYyH/VyEh4B581bQRmqATJSeQTg= -k8s.io/apimachinery v0.24.3/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apimachinery v0.24.4 h1:S0Ur3J/PbivTcL43EdSdPhqCqKla2NIuneNwZcTDeGQ= k8s.io/apimachinery v0.24.4/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2UM= k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= @@ -1988,8 +1977,8 @@ k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/client-go v0.24.3 h1:Nl1840+6p4JqkFWEW2LnMKU667BUxw03REfLAVhuKQY= -k8s.io/client-go v0.24.3/go.mod h1:AAovolf5Z9bY1wIg2FZ8LPQlEdKHjLI7ZD4rw920BJw= +k8s.io/client-go v0.24.4 h1:hIAIJZIPyaw46AkxwyR0FRfM/pRxpUNTd3ysYu9vyRg= +k8s.io/client-go v0.24.4/go.mod h1:+AxlPWw/H6f+EJhRSjIeALaJT4tbeB/8g9BNvXGPd0Y= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 45caf5f9..b96ceb6b 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -3,40 +3,31 @@ package daemon import ( "time" - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" "github.com/ckotzbauer/libstandard" "github.com/ckotzbauer/sbom-operator/internal" - "github.com/ckotzbauer/sbom-operator/internal/job" "github.com/ckotzbauer/sbom-operator/internal/kubernetes" + "github.com/ckotzbauer/sbom-operator/internal/processor" "github.com/ckotzbauer/sbom-operator/internal/syft" - "github.com/ckotzbauer/sbom-operator/internal/target" - "github.com/ckotzbauer/sbom-operator/internal/target/dtrack" - "github.com/ckotzbauer/sbom-operator/internal/target/git" - "github.com/ckotzbauer/sbom-operator/internal/target/oci" "github.com/robfig/cron" "github.com/sirupsen/logrus" ) type CronService struct { - cron string - targets []target.Target + cron string + processor *processor.Processor } var running = false func Start(cronTime string) { cr := libstandard.Unescape(cronTime) - targetKeys := internal.OperatorConfig.Targets - logrus.Debugf("Cron set to: %v", cr) - targets := make([]target.Target, 0) - if !hasJobImage() { - logrus.Debugf("Targets set to: %v", targetKeys) - targets = initTargets(targetKeys) - } + k8s := kubernetes.NewClient(internal.OperatorConfig.IgnoreAnnotations, internal.OperatorConfig.FallbackPullSecret) + sy := syft.New(internal.OperatorConfig.Format) + processor := processor.New(k8s, sy) - cs := CronService{cron: cr, targets: targets} + cs := CronService{cron: cr, processor: processor} cs.printNextExecution() c := cron.New() @@ -64,138 +55,27 @@ func (c *CronService) runBackgroundService() { } running = true - logrus.Info("Execute background-service") - format := internal.OperatorConfig.Format - if !hasJobImage() { - for _, t := range c.targets { + if !processor.HasJobImage() { + for _, t := range c.processor.Targets { t.Initialize() + t.LoadImages() } } - k8s := kubernetes.NewClient(internal.OperatorConfig.IgnoreAnnotations, internal.OperatorConfig.FallbackPullSecret) namespaceSelector := internal.OperatorConfig.NamespaceLabelSelector - namespaces, err := k8s.Client.ListNamespaces(namespaceSelector) + namespaces, err := c.processor.K8s.Client.ListNamespaces(namespaceSelector) if err != nil { logrus.WithError(err).Errorf("failed to list namespaces with selector: %s, abort background-service", namespaceSelector) running = false return } - logrus.Debugf("Discovered %v namespaces", len(namespaces)) - containerImages, allImages := k8s.LoadImageInfos(namespaces, internal.OperatorConfig.PodLabelSelector) - if !hasJobImage() { - c.executeSyftScans(format, k8s, containerImages, allImages) - } else { - executeJobImage(k8s, containerImages) - } + logrus.Debugf("Discovered %v namespaces", len(namespaces)) + pods, allImages := c.processor.K8s.LoadImageInfos(namespaces, internal.OperatorConfig.PodLabelSelector) + c.processor.ProcessAllPods(pods, allImages) c.printNextExecution() running = false } - -func (c *CronService) executeSyftScans(format string, k8s *kubernetes.KubeClient, containerImages []libk8s.KubeImage, allImages []libk8s.KubeImage) { - sy := syft.New(format) - - for _, image := range containerImages { - sbom, err := sy.ExecuteSyft(image.Image) - if err != nil { - // Error is already handled from syft module. - continue - } - - errOccurred := false - - for _, t := range c.targets { - err = t.ProcessSbom(image, sbom) - errOccurred = errOccurred || err != nil - } - - if !errOccurred { - for _, pod := range image.Pods { - k8s.UpdatePodAnnotation(pod) - } - } - } - - for _, t := range c.targets { - t.Cleanup(allImages) - } -} - -func executeJobImage(k8s *kubernetes.KubeClient, containerImages []libk8s.KubeImage) { - jobClient := job.New( - k8s, - internal.OperatorConfig.JobImage, - internal.OperatorConfig.JobImagePullSecret, - internal.OperatorConfig.KubernetesClusterId, - internal.OperatorConfig.JobTimeout) - - j, err := jobClient.StartJob(containerImages) - if err != nil { - // Already handled from job-module - return - } - - if jobClient.WaitForJob(j) { - for _, i := range containerImages { - for _, pod := range i.Pods { - k8s.UpdatePodAnnotation(pod) - } - } - } -} - -func initTargets(targetKeys []string) []target.Target { - targets := make([]target.Target, 0) - - for _, ta := range targetKeys { - var err error - - if ta == "git" { - workingTree := internal.OperatorConfig.GitWorkingTree - workPath := internal.OperatorConfig.GitPath - repository := internal.OperatorConfig.GitRepository - branch := internal.OperatorConfig.GitBranch - format := internal.OperatorConfig.Format - token := internal.OperatorConfig.GitAccessToken - name := internal.OperatorConfig.GitAuthorName - email := internal.OperatorConfig.GitAuthorEmail - t := git.NewGitTarget(workingTree, workPath, repository, branch, token, name, email, format) - err = t.ValidateConfig() - targets = append(targets, t) - } else if ta == "dtrack" { - baseUrl := internal.OperatorConfig.DtrackBaseUrl - apiKey := internal.OperatorConfig.DtrackApiKey - k8sClusterId := internal.OperatorConfig.KubernetesClusterId - t := dtrack.NewDependencyTrackTarget(baseUrl, apiKey, k8sClusterId) - err = t.ValidateConfig() - targets = append(targets, t) - } else if ta == "oci" { - registry := internal.OperatorConfig.OciRegistry - username := internal.OperatorConfig.OciUser - token := internal.OperatorConfig.OciToken - format := internal.OperatorConfig.Format - t := oci.NewOciTarget(registry, username, token, format) - err = t.ValidateConfig() - targets = append(targets, t) - } else { - logrus.Fatalf("Unknown target %s", ta) - } - - if err != nil { - logrus.WithError(err).Fatal("Config-Validation failed!") - } - } - - if len(targets) == 0 { - logrus.Fatalf("Please specify at least one target.") - } - - return targets -} - -func hasJobImage() bool { - return internal.OperatorConfig.JobImage != "" -} diff --git a/internal/job/job.go b/internal/job/job.go index a812d3fa..c96e702b 100644 --- a/internal/job/job.go +++ b/internal/job/job.go @@ -7,8 +7,8 @@ import ( "regexp" "time" + "golang.org/x/exp/maps" batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" @@ -49,27 +49,35 @@ func New(k8s *kubernetes.KubeClient, image, imagePullSecret, clusterId string, t } } -func (j JobClient) StartJob(images []libk8s.KubeImage) (*batchv1.Job, error) { - configs := make([]imageConfig, 0) +func (j JobClient) StartJob(pods []libk8s.PodInfo) (*batchv1.Job, error) { podNamespace := os.Getenv("POD_NAMESPACE") - - for _, image := range images { - cfg, err := oci.ResolveAuthConfig(oci.RegistryImage{ImageID: image.Image.ImageID, PullSecrets: image.Image.PullSecrets}) - if err != nil { - logrus.WithError(err).Error("Error occurred during auth-resolve") - return nil, err + images := make(map[string]imageConfig, 0) + + for _, pod := range pods { + for _, container := range pod.Containers { + cfg, err := oci.ResolveAuthConfig(*container.Image) + if err != nil { + logrus.WithError(err).Error("Error occurred during auth-resolve") + return nil, err + } + + img, ok := images[container.Image.ImageID] + if !ok { + img = imageConfig{ + Host: cfg.ServerAddress, + User: cfg.Username, + Password: cfg.Password, + Image: container.Image.ImageID, + Pods: []imagePod{}, + } + } + + img.Pods = append(img.Pods, j.convertPod(pod)) + images[container.Image.ImageID] = img } - - configs = append(configs, imageConfig{ - Host: cfg.ServerAddress, - User: cfg.Username, - Password: cfg.Password, - Image: image.Image.ImageID, - Pods: j.convertPods(image.Pods), - }) } - bytes, err := json.Marshal(configs) + bytes, err := json.Marshal(maps.Values(images)) if err != nil { logrus.WithError(err).Error("Error occurred during config-marshal") return nil, err @@ -138,16 +146,10 @@ func getJobEnvs() map[string]string { return m } -func (j JobClient) convertPods(pods []corev1.Pod) []imagePod { - ips := make([]imagePod, 0) - - for _, p := range pods { - ips = append(ips, imagePod{ - Pod: p.Name, - Namespace: p.Namespace, - Cluster: j.clusterId, - }) +func (j JobClient) convertPod(pod libk8s.PodInfo) imagePod { + return imagePod{ + Pod: pod.PodName, + Namespace: pod.PodNamespace, + Cluster: j.clusterId, } - - return ips } diff --git a/internal/kubernetes/kubernetes.go b/internal/kubernetes/kubernetes.go index e5d4cd2c..29413efc 100644 --- a/internal/kubernetes/kubernetes.go +++ b/internal/kubernetes/kubernetes.go @@ -11,16 +11,16 @@ import ( "k8s.io/apimachinery/pkg/api/errors" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" "github.com/ckotzbauer/libk8soci/pkg/oci" ) type KubeClient struct { - Client *libk8s.KubeClient - ignoreAnnotations bool - fallbackPullSecret string - SbomOperatorNamespace string + Client *libk8s.KubeClient + ignoreAnnotations bool + fallbackPullSecret []*oci.KubeCreds } var ( @@ -30,81 +30,85 @@ var ( JobName = "sbom-operator-job" ) -func NewClient(ignoreAnnotations bool, fallbackPullSecret string) *KubeClient { +func NewClient(ignoreAnnotations bool, fallbackPullSecretName string) *KubeClient { client := libk8s.NewClient() sbomOperatorNamespace := os.Getenv("POD_NAMESPACE") - return &KubeClient{Client: client, ignoreAnnotations: ignoreAnnotations, fallbackPullSecret: fallbackPullSecret, SbomOperatorNamespace: sbomOperatorNamespace} + fallbackPullSecret := loadFallbackPullSecret(client, sbomOperatorNamespace, fallbackPullSecretName) + return &KubeClient{Client: client, ignoreAnnotations: ignoreAnnotations, fallbackPullSecret: fallbackPullSecret} } -func (client *KubeClient) LoadImageInfos(namespaces []corev1.Namespace, podLabelSelector string) ([]libk8s.KubeImage, []libk8s.KubeImage) { - var fallbackPullSecret []oci.KubeCreds +func (client *KubeClient) StartPodInformer(podLabelSelector string, handler cache.ResourceEventHandlerFuncs) (cache.SharedIndexInformer, error) { + informer := client.Client.CreatePodInformer(podLabelSelector) + informer.AddEventHandler(handler) + err := informer.SetTransform(func(x interface{}) (interface{}, error) { + pod := x.(*corev1.Pod).DeepCopy() + logrus.Tracef("Transform %s/%s", pod.Namespace, pod.Name) - if client.fallbackPullSecret != "" { - if client.SbomOperatorNamespace == "" { + return &corev1.Pod{ + ObjectMeta: meta.ObjectMeta{ + Name: pod.Name, + Namespace: pod.Namespace, + Annotations: pod.Annotations, + }, + Status: corev1.PodStatus{ + InitContainerStatuses: pod.Status.InitContainerStatuses, + EphemeralContainerStatuses: pod.Status.EphemeralContainerStatuses, + ContainerStatuses: pod.Status.ContainerStatuses, + }, + Spec: corev1.PodSpec{ + ImagePullSecrets: pod.Spec.ImagePullSecrets, + }, + }, + nil + }) + + return informer, err +} + +func loadFallbackPullSecret(client *libk8s.KubeClient, namespace, name string) []*oci.KubeCreds { + var fallbackPullSecret []*oci.KubeCreds + + if name != "" { + if namespace == "" { logrus.Debugf("please specify the environment variable 'POD_NAMESPACE' in order to use the fallbackPullSecret") } else { - fallbackPullSecret = client.Client.LoadSecrets(client.SbomOperatorNamespace, []corev1.LocalObjectReference{{Name: client.fallbackPullSecret}}) + fallbackPullSecret = client.LoadSecrets(namespace, []corev1.LocalObjectReference{{Name: name}}) } } - allImages := client.Client.LoadImageInfos(namespaces, podLabelSelector) - imagesToProcess := make([]libk8s.KubeImage, 0) + return fallbackPullSecret +} - for _, img := range allImages { - if fallbackPullSecret != nil { - img.Image.PullSecrets = append(img.Image.PullSecrets, fallbackPullSecret...) - } +func (client *KubeClient) InjectPullSecrets(pod libk8s.PodInfo) { + for _, container := range pod.Containers { + container.Image.PullSecrets = client.Client.LoadSecrets(pod.PodNamespace, pod.PullSecretNames) - annotationFound := false - for _, pod := range img.Pods { - containers := getContainerStatuses(pod, img.Image.ImageID) - for _, c := range containers { - x := client.hasAnnotation(pod.Annotations, c) - if x { - annotationFound = true - break - } - } - - if annotationFound { - break - } - } - - if !annotationFound { - imagesToProcess = append(imagesToProcess, img) - } else { - logrus.Debugf("Skip image %s", img.Image.ImageID) + if client.fallbackPullSecret != nil { + container.Image.PullSecrets = append(container.Image.PullSecrets, client.fallbackPullSecret...) } } - - return imagesToProcess, allImages } -func getContainerStatuses(pod corev1.Pod, imageID string) []corev1.ContainerStatus { - found := make([]corev1.ContainerStatus, 0) - - statuses := []corev1.ContainerStatus{} - statuses = append(statuses, pod.Status.ContainerStatuses...) - statuses = append(statuses, pod.Status.InitContainerStatuses...) - statuses = append(statuses, pod.Status.EphemeralContainerStatuses...) +func (client *KubeClient) LoadImageInfos(namespaces []corev1.Namespace, podLabelSelector string) ([]libk8s.PodInfo, []*oci.RegistryImage) { + podInfos := client.Client.LoadPodInfos(namespaces, podLabelSelector) + allImages := make([]*oci.RegistryImage, 0) - for _, s := range statuses { - if s.ImageID == imageID { - found = append(found, s) + for _, pod := range podInfos { + for _, container := range pod.Containers { + allImages = append(allImages, container.Image) } } - return found + return podInfos, allImages } -func (client *KubeClient) UpdatePodAnnotation(pod corev1.Pod) { - newPod, err := client.Client.Client.CoreV1().Pods(pod.Namespace).Get(context.Background(), pod.Name, meta.GetOptions{}) +func (client *KubeClient) UpdatePodAnnotation(pod libk8s.PodInfo) { + newPod, err := client.Client.Client.CoreV1().Pods(pod.PodNamespace).Get(context.Background(), pod.PodName, meta.GetOptions{}) if err != nil { if !errors.IsNotFound(err) { - logrus.WithError(err).Errorf("Pod %s/%s could not be fetched!", pod.Namespace, pod.Name) + logrus.WithError(err).Errorf("Pod %s/%s could not be fetched!", pod.PodNamespace, pod.PodName) } return @@ -115,15 +119,15 @@ func (client *KubeClient) UpdatePodAnnotation(pod corev1.Pod) { ann = make(map[string]string) } - for _, c := range pod.Status.ContainerStatuses { + for _, c := range newPod.Status.ContainerStatuses { ann[fmt.Sprintf(annotationTemplate, c.Name)] = c.ImageID } - for _, c := range pod.Status.InitContainerStatuses { + for _, c := range newPod.Status.InitContainerStatuses { ann[fmt.Sprintf(annotationTemplate, c.Name)] = c.ImageID } - for _, c := range pod.Status.EphemeralContainerStatuses { + for _, c := range newPod.Status.EphemeralContainerStatuses { ann[fmt.Sprintf(annotationTemplate, c.Name)] = c.ImageID } @@ -131,17 +135,17 @@ func (client *KubeClient) UpdatePodAnnotation(pod corev1.Pod) { _, err = client.Client.Client.CoreV1().Pods(newPod.Namespace).Update(context.Background(), newPod, meta.UpdateOptions{}) if err != nil { - logrus.WithError(err).Errorf("Pod %s/%s could not be updated!", pod.Namespace, pod.Name) + logrus.WithError(err).Warnf("Pod %s/%s could not be updated!", newPod.Namespace, newPod.Name) } } -func (client *KubeClient) hasAnnotation(annotations map[string]string, status corev1.ContainerStatus) bool { +func (client *KubeClient) HasAnnotation(annotations map[string]string, container *libk8s.ContainerInfo) bool { if annotations == nil || client.ignoreAnnotations { return false } - if val, ok := annotations[fmt.Sprintf(annotationTemplate, status.Name)]; ok { - return val == status.ImageID + if val, ok := annotations[fmt.Sprintf(annotationTemplate, container.Name)]; ok { + return val == container.Image.ImageID } return false diff --git a/internal/processor/processor.go b/internal/processor/processor.go new file mode 100644 index 00000000..1d4d836d --- /dev/null +++ b/internal/processor/processor.go @@ -0,0 +1,348 @@ +package processor + +import ( + "os" + "os/signal" + "syscall" + + libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" + liboci "github.com/ckotzbauer/libk8soci/pkg/oci" + "github.com/ckotzbauer/sbom-operator/internal" + "github.com/ckotzbauer/sbom-operator/internal/job" + "github.com/ckotzbauer/sbom-operator/internal/kubernetes" + "github.com/ckotzbauer/sbom-operator/internal/syft" + "github.com/ckotzbauer/sbom-operator/internal/target" + "github.com/ckotzbauer/sbom-operator/internal/target/dtrack" + "github.com/ckotzbauer/sbom-operator/internal/target/git" + "github.com/ckotzbauer/sbom-operator/internal/target/oci" + "github.com/sirupsen/logrus" + "k8s.io/client-go/tools/cache" + + corev1 "k8s.io/api/core/v1" +) + +type Processor struct { + K8s *kubernetes.KubeClient + sy *syft.Syft + Targets []target.Target + imageMap map[string]bool +} + +func New(k8s *kubernetes.KubeClient, sy *syft.Syft) *Processor { + targets := make([]target.Target, 0) + if !HasJobImage() { + logrus.Debugf("Targets set to: %v", internal.OperatorConfig.Targets) + targets = initTargets() + } + + return &Processor{K8s: k8s, sy: sy, Targets: targets, imageMap: make(map[string]bool)} +} + +func (p *Processor) ListenForPods() { + if !HasJobImage() { + for _, t := range p.Targets { + t.Initialize() + } + } + + var informer cache.SharedIndexInformer + informer, err := p.K8s.StartPodInformer(internal.OperatorConfig.PodLabelSelector, cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(old, new interface{}) { + oldPod := old.(*corev1.Pod) + newPod := new.(*corev1.Pod) + oldInfo := p.K8s.Client.ExtractPodInfos(*oldPod) + newInfo := p.K8s.Client.ExtractPodInfos(*newPod) + logrus.Tracef("Pod %s/%s was updated.", newInfo.PodNamespace, newInfo.PodName) + + var removedContainers []*libk8s.ContainerInfo + newInfo.Containers, removedContainers = getChangedContainers(oldInfo, newInfo) + p.scanPod(newInfo) + p.cleanupImagesIfNeeded(removedContainers, informer.GetStore().List()) + }, + DeleteFunc: func(obj interface{}) { + pod := obj.(*corev1.Pod) + info := p.K8s.Client.ExtractPodInfos(*pod) + logrus.Tracef("Pod %s/%s was removed.", info.PodNamespace, info.PodName) + p.cleanupImagesIfNeeded(info.Containers, informer.GetStore().List()) + }, + }) + + if err != nil { + logrus.WithError(err).Fatalf("Can't listen for pod-changes.") + return + } + + p.runInformerAsync(informer) +} + +func (p *Processor) ProcessAllPods(pods []libk8s.PodInfo, allImages []*liboci.RegistryImage) { + if !HasJobImage() { + p.executeSyftScans(pods, allImages) + } else { + p.executeJobImage(pods) + } +} + +func (p *Processor) scanPod(pod libk8s.PodInfo) { + errOccurred := false + p.K8s.InjectPullSecrets(pod) + + for _, container := range pod.Containers { + alreadyScanned := p.imageMap[container.Image.ImageID] + if p.K8s.HasAnnotation(pod.Annotations, container) || alreadyScanned { + logrus.Debugf("Skip image %s", container.Image.ImageID) + continue + } + + p.imageMap[container.Image.ImageID] = true + sbom, err := p.sy.ExecuteSyft(container.Image) + if err != nil { + // Error is already handled from syft module. + continue + } + + for _, t := range p.Targets { + err = t.ProcessSbom(container.Image, sbom) + errOccurred = errOccurred || err != nil + } + } + + if !errOccurred && len(pod.Containers) > 0 { + p.K8s.UpdatePodAnnotation(pod) + } +} + +func initTargets() []target.Target { + targets := make([]target.Target, 0) + + for _, ta := range internal.OperatorConfig.Targets { + var err error + + if ta == "git" { + workingTree := internal.OperatorConfig.GitWorkingTree + workPath := internal.OperatorConfig.GitPath + repository := internal.OperatorConfig.GitRepository + branch := internal.OperatorConfig.GitBranch + format := internal.OperatorConfig.Format + token := internal.OperatorConfig.GitAccessToken + name := internal.OperatorConfig.GitAuthorName + email := internal.OperatorConfig.GitAuthorEmail + t := git.NewGitTarget(workingTree, workPath, repository, branch, token, name, email, format) + err = t.ValidateConfig() + targets = append(targets, t) + } else if ta == "dtrack" { + baseUrl := internal.OperatorConfig.DtrackBaseUrl + apiKey := internal.OperatorConfig.DtrackApiKey + k8sClusterId := internal.OperatorConfig.KubernetesClusterId + t := dtrack.NewDependencyTrackTarget(baseUrl, apiKey, k8sClusterId) + err = t.ValidateConfig() + targets = append(targets, t) + } else if ta == "oci" { + registry := internal.OperatorConfig.OciRegistry + username := internal.OperatorConfig.OciUser + token := internal.OperatorConfig.OciToken + format := internal.OperatorConfig.Format + t := oci.NewOciTarget(registry, username, token, format) + err = t.ValidateConfig() + targets = append(targets, t) + } else { + logrus.Fatalf("Unknown target %s", ta) + } + + if err != nil { + logrus.WithError(err).Fatal("Config-Validation failed!") + } + } + + if len(targets) == 0 { + logrus.Fatalf("Please specify at least one target.") + } + + return targets +} + +func HasJobImage() bool { + return internal.OperatorConfig.JobImage != "" +} + +func (p *Processor) executeSyftScans(pods []libk8s.PodInfo, allImages []*liboci.RegistryImage) { + for _, pod := range pods { + p.scanPod(pod) + } + + for _, t := range p.Targets { + targetImages := t.LoadImages() + removableImages := make([]*liboci.RegistryImage, 0) + for _, t := range targetImages { + if !containsImage(allImages, t.ImageID) { + removableImages = append(removableImages, t) + delete(p.imageMap, t.ImageID) + logrus.Debugf("Image %s marked for removal", t.ImageID) + } + } + + if len(removableImages) > 0 { + t.Remove(removableImages) + } + } +} + +func (p *Processor) executeJobImage(pods []libk8s.PodInfo) { + jobClient := job.New( + p.K8s, + internal.OperatorConfig.JobImage, + internal.OperatorConfig.JobImagePullSecret, + internal.OperatorConfig.KubernetesClusterId, + internal.OperatorConfig.JobTimeout) + + filteredPods := make([]libk8s.PodInfo, 0) + for _, pod := range pods { + filteredContainers := make([]*libk8s.ContainerInfo, 0) + for _, container := range pod.Containers { + if p.K8s.HasAnnotation(pod.Annotations, container) { + logrus.Debugf("Skip image %s", container.Image.ImageID) + continue + } + + filteredContainers = append(filteredContainers, container) + } + + if len(filteredContainers) > 0 { + filteredPods = append(filteredPods, pod) + } + } + + j, err := jobClient.StartJob(filteredPods) + if err != nil { + // Already handled from job-module + return + } + + if jobClient.WaitForJob(j) { + for _, pod := range filteredPods { + p.K8s.UpdatePodAnnotation(pod) + } + } +} + +func getChangedContainers(oldPod, newPod libk8s.PodInfo) ([]*libk8s.ContainerInfo, []*libk8s.ContainerInfo) { + addedContainers := make([]*libk8s.ContainerInfo, 0) + removedContainers := make([]*libk8s.ContainerInfo, 0) + for _, c := range newPod.Containers { + if !containsContainerImage(oldPod.Containers, c.Image.ImageID) { + addedContainers = append(addedContainers, c) + } + } + + for _, c := range oldPod.Containers { + if !containsContainerImage(newPod.Containers, c.Image.ImageID) { + removedContainers = append(removedContainers, c) + } + } + + return addedContainers, removedContainers +} + +func containsImage(images []*liboci.RegistryImage, image string) bool { + for _, i := range images { + if i.ImageID == image { + return true + } + } + + return false +} + +func containsContainerImage(containers []*libk8s.ContainerInfo, image string) bool { + for _, c := range containers { + if c.Image.ImageID == image { + return true + } + } + + return false +} + +func (p *Processor) cleanupImagesIfNeeded(removedContainers []*libk8s.ContainerInfo, allPods []interface{}) { + images := make([]*liboci.RegistryImage, 0) + + for _, c := range removedContainers { + found := false + for _, po := range allPods { + pod := po.(*corev1.Pod) + info := p.K8s.Client.ExtractPodInfos(*pod) + found = found || containsContainerImage(info.Containers, c.Image.ImageID) + } + + if !found { + images = append(images, c.Image) + delete(p.imageMap, c.Image.ImageID) + logrus.Debugf("Image %s marked for removal", c.Image.ImageID) + } + } + + if len(images) > 0 { + for _, t := range p.Targets { + t.Remove(images) + } + } +} + +func (p *Processor) runInformerAsync(informer cache.SharedIndexInformer) { + stop := make(chan struct{}) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + run := true + for run { + sig := <-sigs + switch sig { + case syscall.SIGTERM, syscall.SIGINT: + logrus.Infof("Received signal %s", sig) + close(stop) + run = false + } + } + }() + + go func() { + logrus.Info("Start pod-informer") + informer.Run(stop) + logrus.Info("Pod-informer has stopped") + os.Exit(0) + }() + + go func() { + if !HasJobImage() { + logrus.Info("Wait for cache to be synced") + if !cache.WaitForCacheSync(stop, informer.HasSynced) { + logrus.Fatal("Timed out waiting for the cache to sync") + } + + logrus.Info("Finished cache sync") + pods := informer.GetStore().List() + missingPods := make([]libk8s.PodInfo, 0) + allImages := make([]*liboci.RegistryImage, 0) + + for _, t := range p.Targets { + targetImages := t.LoadImages() + for _, po := range pods { + pod := po.(*corev1.Pod) + info := p.K8s.Client.ExtractPodInfos(*pod) + for _, c := range info.Containers { + allImages = append(allImages, c.Image) + if !containsImage(targetImages, c.Image.ImageID) && !p.K8s.HasAnnotation(info.Annotations, c) { + missingPods = append(missingPods, info) + logrus.Debugf("Pod %s/%s needs to be analyzed", info.PodNamespace, info.PodName) + break + } + } + } + } + + if len(missingPods) > 0 { + p.executeSyftScans(missingPods, allImages) + } + } + }() +} diff --git a/internal/syft/syft.go b/internal/syft/syft.go index 123dcf84..a29bd28f 100644 --- a/internal/syft/syft.go +++ b/internal/syft/syft.go @@ -23,8 +23,8 @@ type Syft struct { resolveVersion func() string } -func New(sbomFormat string) Syft { - return Syft{ +func New(sbomFormat string) *Syft { + return &Syft{ sbomFormat: sbomFormat, resolveVersion: getSyftVersion, } @@ -35,7 +35,7 @@ func (s Syft) WithVersion(version string) Syft { return s } -func (s *Syft) ExecuteSyft(img oci.RegistryImage) (string, error) { +func (s *Syft) ExecuteSyft(img *oci.RegistryImage) (string, error) { logrus.Infof("Processing image %s", img.ImageID) fullRef, err := parser.Parse(img.ImageID) @@ -106,18 +106,14 @@ func (s *Syft) ExecuteSyft(img oci.RegistryImage) (string, error) { func GetFileName(sbomFormat string) string { switch sbomFormat { - case "json": + case "json", "syftjson", "cyclonedxjson", "spdxjson", "github", "githubjson": return "sbom.json" - case "text": - return "sbom.txt" - case "cyclonedx": + case "cyclonedx", "cyclone", "cyclonedxxml": return "sbom.xml" - case "cyclonedxjson": - return "sbom.json" - case "spdx": + case "spdx", "spdxtv", "spdxtagvalue": return "sbom.spdx" - case "spdxjson": - return "sbom.json" + case "text": + return "sbom.txt" case "table": return "sbom.txt" default: diff --git a/internal/syft/syft_test.go b/internal/syft/syft_test.go index e3393e15..ef918c13 100644 --- a/internal/syft/syft_test.go +++ b/internal/syft/syft_test.go @@ -60,7 +60,7 @@ func marshalCyclonedx(t *testing.T, x interface{}) string { func testJsonSbom(t *testing.T, name, imageID string) { format := "json" s := syft.New(format).WithVersion("v9.9.9") - sbom, err := s.ExecuteSyft(oci.RegistryImage{ImageID: imageID, PullSecrets: []oci.KubeCreds{}}) + sbom, err := s.ExecuteSyft(&oci.RegistryImage{ImageID: imageID, PullSecrets: []*oci.KubeCreds{}}) assert.NoError(t, err) @@ -84,7 +84,7 @@ func testJsonSbom(t *testing.T, name, imageID string) { func testCyclonedxSbom(t *testing.T, name, imageID string) { format := "cyclonedx" s := syft.New(format).WithVersion("v9.9.9") - sbom, err := s.ExecuteSyft(oci.RegistryImage{ImageID: imageID, PullSecrets: []oci.KubeCreds{}}) + sbom, err := s.ExecuteSyft(&oci.RegistryImage{ImageID: imageID, PullSecrets: []*oci.KubeCreds{}}) assert.NoError(t, err) var output syftCyclonedxOutput @@ -104,7 +104,7 @@ func testCyclonedxSbom(t *testing.T, name, imageID string) { func testSpdxSbom(t *testing.T, name, imageID string) { format := "spdxjson" s := syft.New(format).WithVersion("v9.9.9") - sbom, err := s.ExecuteSyft(oci.RegistryImage{ImageID: imageID, PullSecrets: []oci.KubeCreds{}}) + sbom, err := s.ExecuteSyft(&oci.RegistryImage{ImageID: imageID, PullSecrets: []*oci.KubeCreds{}}) assert.NoError(t, err) var output syftSpdxOutput @@ -128,7 +128,7 @@ func testSpdxSbom(t *testing.T, name, imageID string) { func testCyclonedxSbomWithoutPullSecrets(t *testing.T, name, imageID string) { format := "cyclonedx" s := syft.New(format).WithVersion("v9.9.9") - sbom, err := s.ExecuteSyft(oci.RegistryImage{ImageID: imageID, PullSecrets: []oci.KubeCreds{}}) + sbom, err := s.ExecuteSyft(&oci.RegistryImage{ImageID: imageID, PullSecrets: []*oci.KubeCreds{}}) assert.NoError(t, err) var output syftCyclonedxOutput diff --git a/internal/target/dtrack/dtrack_target.go b/internal/target/dtrack/dtrack_target.go index e9a07499..8c3ca0e0 100644 --- a/internal/target/dtrack/dtrack_target.go +++ b/internal/target/dtrack/dtrack_target.go @@ -6,23 +6,26 @@ import ( "fmt" "strings" + "github.com/google/uuid" parser "github.com/novln/docker-parser" dtrack "github.com/nscuro/dtrack-client" "github.com/sirupsen/logrus" - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" + libk8s "github.com/ckotzbauer/libk8soci/pkg/oci" "github.com/ckotzbauer/sbom-operator/internal" ) type DependencyTrackTarget struct { - baseUrl string - apiKey string - k8sClusterId string + baseUrl string + apiKey string + k8sClusterId string + imageProjectMap map[string]uuid.UUID } const ( kubernetesCluster = "kubernetes-cluster" sbomOperator = "sbom-operator" + rawImageId = "raw-image-id" ) func NewDependencyTrackTarget(baseUrl, apiKey, k8sClusterId string) *DependencyTrackTarget { @@ -46,18 +49,11 @@ func (g *DependencyTrackTarget) ValidateConfig() error { func (g *DependencyTrackTarget) Initialize() { } -func (g *DependencyTrackTarget) ProcessSbom(image libk8s.KubeImage, sbom string) error { - imageRef, err := parser.Parse(image.Image.Image) - if err != nil { - logrus.WithError(err).Errorf("Could not parse image %s", image.Image.Image) - return nil - } - - projectName := imageRef.Repository() - version := imageRef.Tag() +func (g *DependencyTrackTarget) ProcessSbom(image *libk8s.RegistryImage, sbom string) error { + projectName, version := getRepoWithVersion(image) if sbom == "" { - logrus.Infof("Empty SBOM - skip image (image=%s)", image.Image.ImageID) + logrus.Infof("Empty SBOM - skip image (image=%s)", image.ImageID) return nil } @@ -94,6 +90,9 @@ func (g *DependencyTrackTarget) ProcessSbom(image libk8s.KubeImage, sbom string) if !containsTag(project.Tags, sbomOperator) { project.Tags = append(project.Tags, dtrack.Tag{Name: sbomOperator}) } + if !containsTag(project.Tags, rawImageId) { + project.Tags = append(project.Tags, dtrack.Tag{Name: fmt.Sprintf("%s=%s", rawImageId, image.ImageID)}) + } _, err = client.Project.Update(context.Background(), project) if err != nil { @@ -103,14 +102,19 @@ func (g *DependencyTrackTarget) ProcessSbom(image libk8s.KubeImage, sbom string) return nil } -func (g *DependencyTrackTarget) Cleanup(allImages []libk8s.KubeImage) { +func (g *DependencyTrackTarget) LoadImages() []*libk8s.RegistryImage { client, _ := dtrack.NewClient(g.baseUrl, dtrack.WithAPIKey(g.apiKey)) + if g.imageProjectMap == nil { + g.imageProjectMap = make(map[string]uuid.UUID) + } + var ( pageNumber = 1 pageSize = 50 ) + images := make([]*libk8s.RegistryImage, 0) for { projectsPage, err := client.Project.GetAll(context.Background(), dtrack.PageOptions{ PageNumber: pageNumber, @@ -120,47 +124,32 @@ func (g *DependencyTrackTarget) Cleanup(allImages []libk8s.KubeImage) { logrus.Errorf("Could not load projects: %v", err) } - projectLoop: - for _, project := range projectsPage.Items { - currentImageName := fmt.Sprintf("%v:%v", project.Name, project.Version) - - // Image used in current cluster - for _, image := range allImages { - if image.Image.Image == currentImageName { - continue projectLoop - } - } + var imageId string - // check all tags, remove the current cluster and aggregate a list of other clusters - otherClusterIds := []string{} + for _, project := range projectsPage.Items { sbomOperatorPropFound := false + imageRelatesToCluster := false for _, tag := range project.Tags { + imageId = "" if strings.Index(tag.Name, kubernetesCluster) == 0 { clusterId := string(tag.Name[len(kubernetesCluster)+1:]) if clusterId == g.k8sClusterId { - logrus.Infof("Removing %v=%v tag from project %v", kubernetesCluster, g.k8sClusterId, currentImageName) - project.Tags = removeTag(project.Tags, kubernetesCluster+"="+g.k8sClusterId) - _, err := client.Project.Update(context.Background(), project) - if err != nil { - logrus.WithError(err).Warnf("Project %s could not be updated", project.UUID.String()) - } - } else { - otherClusterIds = append(otherClusterIds, clusterId) + imageRelatesToCluster = true } } if tag.Name == sbomOperator { sbomOperatorPropFound = true } - } - // if not in other cluster delete the project - if sbomOperatorPropFound && len(otherClusterIds) == 0 { - logrus.Infof("Image not running in any cluster - removing %v", currentImageName) - err := client.Project.Delete(context.Background(), project.UUID) - if err != nil { - logrus.WithError(err).Warnf("Project %s could not be deleted", project.UUID.String()) + if strings.Index(tag.Name, rawImageId) == 0 { + imageId = string(tag.Name[len(rawImageId)+1:]) } } + + if imageRelatesToCluster && sbomOperatorPropFound && len(imageId) > 0 { + images = append(images, &libk8s.RegistryImage{ImageID: imageId}) + g.imageProjectMap[imageId] = project.UUID + } } if pageNumber*pageSize >= projectsPage.TotalCount { @@ -169,11 +158,68 @@ func (g *DependencyTrackTarget) Cleanup(allImages []libk8s.KubeImage) { pageNumber++ } + + return images +} + +func (g *DependencyTrackTarget) Remove(images []*libk8s.RegistryImage) { + if g.imageProjectMap == nil { + // prepropulate imageProjectMap + g.LoadImages() + } + + client, _ := dtrack.NewClient(g.baseUrl, dtrack.WithAPIKey(g.apiKey)) + + for _, img := range images { + uuid := g.imageProjectMap[img.ImageID] + if uuid.String() == "" { + logrus.Warnf("No project found for imageID: %s", img.ImageID) + continue + } + + project, err := client.Project.Get(context.Background(), uuid) + if err != nil { + logrus.Errorf("Could not load project: %v", err) + continue + } + + // check all tags, remove the current cluster and aggregate a list of other clusters + currentImageName := fmt.Sprintf("%v:%v", project.Name, project.Version) + otherClusterIds := []string{} + sbomOperatorPropFound := false + for _, tag := range project.Tags { + if strings.Index(tag.Name, kubernetesCluster) == 0 { + clusterId := string(tag.Name[len(kubernetesCluster)+1:]) + if clusterId == g.k8sClusterId { + logrus.Infof("Removing %v=%v tag from project %v", kubernetesCluster, g.k8sClusterId, currentImageName) + project.Tags = removeTag(project.Tags, kubernetesCluster+"="+g.k8sClusterId) + _, err := client.Project.Update(context.Background(), project) + if err != nil { + logrus.WithError(err).Warnf("Project %s could not be updated", project.UUID.String()) + } + } else { + otherClusterIds = append(otherClusterIds, clusterId) + } + } + if tag.Name == sbomOperator { + sbomOperatorPropFound = true + } + } + + // if not in other cluster delete the project + if sbomOperatorPropFound && len(otherClusterIds) == 0 { + logrus.Infof("Image not running in any cluster - removing %v", currentImageName) + err := client.Project.Delete(context.Background(), project.UUID) + if err != nil { + logrus.WithError(err).Warnf("Project %s could not be deleted", project.UUID.String()) + } + } + } } func containsTag(tags []dtrack.Tag, tagString string) bool { for _, tag := range tags { - if tag.Name == tagString { + if tag.Name == tagString || strings.Index(tag.Name, tagString) == 0 { return true } } @@ -189,3 +235,24 @@ func removeTag(tags []dtrack.Tag, tagString string) []dtrack.Tag { } return newTags } + +func getRepoWithVersion(image *libk8s.RegistryImage) (string, string) { + imageRef, err := parser.Parse(image.ImageID) + if err != nil { + logrus.WithError(err).Errorf("Could not parse image %s", image.ImageID) + return "", "" + } + + projectName := imageRef.Repository() + + if strings.Index(image.Image, "sha256") != 0 { + imageRef, err = parser.Parse(image.Image) + if err != nil { + logrus.WithError(err).Errorf("Could not parse image %s", image.Image) + return "", "" + } + } + + version := imageRef.Tag() + return projectName, version +} diff --git a/internal/target/git/git_target.go b/internal/target/git/git_target.go index 92d4b152..adc800d7 100644 --- a/internal/target/git/git_target.go +++ b/internal/target/git/git_target.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" + libk8s "github.com/ckotzbauer/libk8soci/pkg/oci" "github.com/ckotzbauer/sbom-operator/internal" "github.com/ckotzbauer/sbom-operator/internal/syft" "github.com/sirupsen/logrus" @@ -67,8 +67,8 @@ func (g *GitTarget) Initialize() { g.gitAccount.PrepareRepository(g.repository, g.workingTree, g.branch) } -func (g *GitTarget) ProcessSbom(image libk8s.KubeImage, sbom string) error { - imageID := image.Image.ImageID +func (g *GitTarget) ProcessSbom(image *libk8s.RegistryImage, sbom string) error { + imageID := image.ImageID filePath := g.ImageIDToFilePath(imageID) dir := filepath.Dir(filePath) @@ -86,41 +86,13 @@ func (g *GitTarget) ProcessSbom(image libk8s.KubeImage, sbom string) error { return g.gitAccount.CommitAll(g.workingTree, fmt.Sprintf("Created new SBOM for image %s", imageID)) } -func (g *GitTarget) Cleanup(allImages []libk8s.KubeImage) { - logrus.Debug("Start to remove old SBOMs") +func (g *GitTarget) LoadImages() []*libk8s.RegistryImage { ignoreDirs := []string{".git"} - - fileName := syft.GetFileName(g.sbomFormat) - allProcessedFiles := g.mapToFiles(allImages) - - err := filepath.Walk(filepath.Join(g.workingTree, g.workPath), g.deleteObsoleteFiles(fileName, ignoreDirs, allProcessedFiles)) - if err != nil { - logrus.WithError(err).Error("Could not cleanup old SBOMs") - } else { - err := g.gitAccount.CommitAndPush(g.workingTree, "Deleted old SBOMs") - if err != nil { - logrus.WithError(err).Error("Could not commit SBOM removal to git") - } - } -} - -func (g *GitTarget) mapToFiles(allImages []libk8s.KubeImage) []string { - paths := []string{} - for _, img := range allImages { - paths = append(paths, g.ImageIDToFilePath(img.Image.ImageID)) - } - - return paths -} - -func (g *GitTarget) ImageIDToFilePath(id string) string { fileName := syft.GetFileName(g.sbomFormat) - filePath := strings.ReplaceAll(id, "@", "/") - return strings.ReplaceAll(path.Join(g.workingTree, g.workPath, filePath, fileName), ":", "_") -} + basePath := filepath.Join(g.workingTree, g.workPath) + images := make([]*libk8s.RegistryImage, 0) -func (g *GitTarget) deleteObsoleteFiles(fileName string, ignoreDirs, allProcessedFiles []string) filepath.WalkFunc { - return func(p string, info os.FileInfo, err error) error { + err := filepath.Walk(basePath, func(p string, info os.FileInfo, err error) error { if err != nil { logrus.WithError(err).Errorf("An error occurred while processing %s", p) return nil @@ -135,27 +107,55 @@ func (g *GitTarget) deleteObsoleteFiles(fileName string, ignoreDirs, allProcesse } } - if info.Name() == fileName { - found := false - for _, f := range allProcessedFiles { - if f == p { - found = true - break - } - } - - if !found { - rel, _ := filepath.Rel(g.workingTree, p) - dir := filepath.Dir(rel) - err = g.gitAccount.Remove(g.workingTree, dir) - if err != nil { - logrus.WithError(err).Errorf("File could not be deleted %s", p) - } else { - logrus.Debugf("Deleted old SBOM: %s", p) - } - } + if filepath.Base(p) == fileName { + sbomPath, _ := filepath.Rel(basePath, p) + s := filepath.Dir(sbomPath) + images = append(images, &libk8s.RegistryImage{ImageID: strings.Replace(s, "/sha256_", "@sha256:", 1)}) } return nil + }) + + if err != nil { + logrus.WithError(err).Error("Could not list all SBOMs") + return []*libk8s.RegistryImage{} + } + + return images +} + +func (g *GitTarget) Remove(images []*libk8s.RegistryImage) { + logrus.Debug("Start to remove old SBOMs") + sbomFiles := g.mapToFiles(images) + + for _, f := range sbomFiles { + rel, _ := filepath.Rel(g.workingTree, f) + dir := filepath.Dir(rel) + err := g.gitAccount.Remove(g.workingTree, dir) + if err != nil { + logrus.WithError(err).Errorf("File could not be deleted %s", f) + } else { + logrus.Debugf("Deleted old SBOM: %s", f) + } + } + + err := g.gitAccount.CommitAndPush(g.workingTree, "Deleted old SBOMs") + if err != nil { + logrus.WithError(err).Error("Could not commit SBOM removal to git") } } + +func (g *GitTarget) mapToFiles(allImages []*libk8s.RegistryImage) []string { + paths := []string{} + for _, img := range allImages { + paths = append(paths, g.ImageIDToFilePath(img.ImageID)) + } + + return paths +} + +func (g *GitTarget) ImageIDToFilePath(id string) string { + fileName := syft.GetFileName(g.sbomFormat) + filePath := strings.ReplaceAll(id, "@", "/") + return strings.ReplaceAll(path.Join(g.workingTree, g.workPath, filePath, fileName), ":", "_") +} diff --git a/internal/target/oci/oci_target.go b/internal/target/oci/oci_target.go index ed432530..e50e2aad 100644 --- a/internal/target/oci/oci_target.go +++ b/internal/target/oci/oci_target.go @@ -3,7 +3,7 @@ package oci import ( "fmt" - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" + libk8s "github.com/ckotzbauer/libk8soci/pkg/oci" "github.com/ckotzbauer/sbom-operator/internal" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -50,10 +50,10 @@ func (g *OciTarget) ValidateConfig() error { func (g *OciTarget) Initialize() { } -func (g *OciTarget) ProcessSbom(image libk8s.KubeImage, sbom string) error { - ref, err := name.ParseReference(image.Image.ImageID) +func (g *OciTarget) ProcessSbom(image *libk8s.RegistryImage, sbom string) error { + ref, err := name.ParseReference(image.ImageID) if err != nil { - logrus.WithError(err).Errorf("failed to parse reference %s", image.Image.ImageID) + logrus.WithError(err).Errorf("failed to parse reference %s", image.ImageID) return err } @@ -90,5 +90,9 @@ func (g *OciTarget) ProcessSbom(image libk8s.KubeImage, sbom string) error { return err } -func (g *OciTarget) Cleanup(allImages []libk8s.KubeImage) { +func (g *OciTarget) LoadImages() []*libk8s.RegistryImage { + return []*libk8s.RegistryImage{} +} + +func (g *OciTarget) Remove(allImages []*libk8s.RegistryImage) { } diff --git a/internal/target/oci/oci_target_test.go b/internal/target/oci/oci_target_test.go index f4f140c6..459f867d 100644 --- a/internal/target/oci/oci_target_test.go +++ b/internal/target/oci/oci_target_test.go @@ -5,7 +5,6 @@ import ( "os" "testing" - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" liboci "github.com/ckotzbauer/libk8soci/pkg/oci" "github.com/stretchr/testify/assert" ) @@ -16,8 +15,7 @@ func TestOci(t *testing.T) { sbom, err := os.ReadFile("./fixtures/sbom.json") assert.NoError(t, err) - img := libk8s.KubeImage{Image: liboci.RegistryImage{ImageID: os.Getenv("TEST_DIGEST")}} - - err = oci.ProcessSbom(img, string(sbom)) + img := liboci.RegistryImage{ImageID: os.Getenv("TEST_DIGEST")} + err = oci.ProcessSbom(&img, string(sbom)) assert.NoError(t, err) } diff --git a/internal/target/target.go b/internal/target/target.go index 10ae94e6..2c44bccf 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -1,12 +1,13 @@ package target import ( - libk8s "github.com/ckotzbauer/libk8soci/pkg/kubernetes" + libk8s "github.com/ckotzbauer/libk8soci/pkg/oci" ) type Target interface { Initialize() ValidateConfig() error - ProcessSbom(image libk8s.KubeImage, sbom string) error - Cleanup(allImages []libk8s.KubeImage) + ProcessSbom(image *libk8s.RegistryImage, sbom string) error + LoadImages() []*libk8s.RegistryImage + Remove(images []*libk8s.RegistryImage) } diff --git a/main.go b/main.go index 5ec8fc65..645b1d24 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,9 @@ import ( "github.com/ckotzbauer/libstandard" "github.com/ckotzbauer/sbom-operator/internal" "github.com/ckotzbauer/sbom-operator/internal/daemon" + "github.com/ckotzbauer/sbom-operator/internal/kubernetes" + "github.com/ckotzbauer/sbom-operator/internal/processor" + "github.com/ckotzbauer/sbom-operator/internal/syft" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -31,7 +34,14 @@ func newRootCmd() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { printVersion() - daemon.Start(internal.OperatorConfig.Cron) + if internal.OperatorConfig.Cron != "" { + daemon.Start(internal.OperatorConfig.Cron) + } else { + k8s := kubernetes.NewClient(internal.OperatorConfig.IgnoreAnnotations, internal.OperatorConfig.FallbackPullSecret) + sy := syft.New(internal.OperatorConfig.Format) + p := processor.New(k8s, sy) + p.ListenForPods() + } logrus.Info("Webserver is running at port 8080") http.HandleFunc("/health", health) @@ -41,7 +51,7 @@ func newRootCmd() *cobra.Command { libstandard.AddConfigFlag(rootCmd) libstandard.AddVerbosityFlag(rootCmd) - rootCmd.PersistentFlags().String(internal.ConfigKeyCron, "@hourly", "Backround-Service interval (CRON)") + rootCmd.PersistentFlags().String(internal.ConfigKeyCron, "", "Backround-Service interval (CRON)") rootCmd.PersistentFlags().String(internal.ConfigKeyFormat, "json", "SBOM-Format.") rootCmd.PersistentFlags().StringSlice(internal.ConfigKeyTargets, []string{"git"}, "Targets for created SBOMs (git, dtrack).") rootCmd.PersistentFlags().Bool(internal.ConfigKeyIgnoreAnnotations, false, "Force analyzing of all images, including those from annotated pods.")