From 1a8ab2449fca335851c20882164562003f62b996 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 17 Oct 2023 11:02:37 +1100 Subject: [PATCH] feat: replace plugin with example --- README.md | 133 ++------------ catalog-info-component.yaml | 10 +- catalog-info.yaml | 2 +- docker-compose.yml | 2 +- hooks/{post-command => command} | 0 lib/download.bash | 20 +-- plugin.yml | 21 +-- src/.goreleaser.yaml | 7 +- src/Dockerfile | 6 +- src/buildkite/agent.go | 6 +- src/go.mod | 38 +--- src/go.sum | 101 +---------- src/main.go | 164 +----------------- src/plugin/config.go | 18 ++ src/plugin/config_test.go | 35 ++++ src/plugin/example.go | 39 +++++ src/plugin/example_mocks_test.go | 16 ++ src/plugin/example_test.go | 24 +++ src/registry/ecr.go | 148 ---------------- src/registry/ecr_test.go | 55 ------ src/report/annotation.go | 83 --------- src/report/annotation.gohtml | 63 ------- src/report/annotation_test.go | 106 ----------- .../TestReports/findings_included.testza | 1 - .../snapshots/TestReports/image_label.testza | 1 - .../TestReports/no_vulnerabilities.testza | 1 - src/runtimeerrors/error.go | 32 ---- src/runtimeerrors/error_test.go | 50 ------ tests/download.bats | 6 +- 29 files changed, 199 insertions(+), 989 deletions(-) rename hooks/{post-command => command} (100%) create mode 100644 src/plugin/config.go create mode 100644 src/plugin/config_test.go create mode 100644 src/plugin/example.go create mode 100644 src/plugin/example_mocks_test.go create mode 100644 src/plugin/example_test.go delete mode 100644 src/registry/ecr.go delete mode 100644 src/registry/ecr_test.go delete mode 100644 src/report/annotation.go delete mode 100644 src/report/annotation.gohtml delete mode 100644 src/report/annotation_test.go delete mode 100755 src/report/testdata/snapshots/TestReports/findings_included.testza delete mode 100755 src/report/testdata/snapshots/TestReports/image_label.testza delete mode 100755 src/report/testdata/snapshots/TestReports/no_vulnerabilities.testza delete mode 100644 src/runtimeerrors/error.go delete mode 100644 src/runtimeerrors/error_test.go diff --git a/README.md b/README.md index 10dbdde..4a24fe8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ -# ECR Scan Results Buildkite Plugin +# Example Go Buildkite Plugin -Buildkite plugin to retrieve ECR scan results from AWS's ECR image scanning -service. By default the plugin will cause the step to fail if there are critical -or high vulnerabilities reported, but there are configurable thresholds on this -behaviour. +An example Buildkite plugin written in Go, demonstrating on-demand download of the +public release files from Github and execution. -> ℹ️ TIP: if you want the build to continue when vulnerabilities are found, be -> sure to supply values for `max-criticals` and `max-highs` parameters. If these -> are set to high values your build will never fail, but details will be -> supplied in the annotation. -> -> Check out the FAQs below for more information +To demonstrate interaction with the Buildkite agent, the example includes code +to annotate the build with a message. + +This example is reverse engineered from [@cultureamp/ecr-scan-results-buildkite-plugin](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin). ## Example @@ -18,118 +14,13 @@ Add the following lines to your `pipeline.yml`: ```yml steps: - - command: "command which creates an image" - # the docker-compose plugin may be used here instead of a command - plugins: - - cultureamp/ecr-scan-results#v1.2.0: - image-name: "$BUILD_REPO:deploy-$BUILD_TAG" -``` - -In a pipeline this will look something like: - -```yml -steps: - - label: ":docker: Build and push CDK deployment image" - command: "bin/ci_cdk_build_and_push.sh" - agents: - queue: ${BUILD_AGENT} - plugins: - - cultureamp/aws-assume-role: - role: ${BUILD_ROLE} - - cultureamp/ecr-scan-results#v1.2.0: - image-name: "$BUILD_REPO:deploy-$BUILD_TAG" -``` - -If you want the pipeline to pass with some vulnerabilities then set -`max-criticals` and `max-highs` like below. This pipeline will pass if there is -one critical vulenerability but fail if there are two. Similarly it will fail if -there are eleven high vulnerabilities. - -```yml -steps: - - label: ":docker: Build and push CDK deployment image" - command: "bin/ci_cdk_build_and_push.sh" - agents: - queue: ${BUILD_AGENT} - plugins: - - cultureamp/aws-assume-role: - role: ${BUILD_ROLE} - - cultureamp/ecr-scan-results#v1.2.0: - image-name: "$BUILD_REPO:deploy-$BUILD_TAG" - max-criticals: "1" - max-highs: "10" + - plugins: + - cultureamp/example-go#v0.1.0: + message: "This is the message that will be annotated!" ``` ## Configuration -### `image-name` (Required, string) - -The name of the container image in ECR. This should be the same string that is -supplied as an arguement to the `docker push` command used to push the image to -AWS ECR. It should have the form: -`AWS_ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPOSITORY_NAME:IMAGE_TAG` with the -text in capitals replaced with the appropriate values for your environment. - -### `max-criticals` (Optional, string) - -If the number of critical vulnerabilities in the image exceeds this threshold -the build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999) -to allow the build to always pass. - -### `max-highs` (Optional, string) - -If the number of high vulnerabilities in the image exceeds this threshold the -build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999) to -allow the build to always pass. - -### `image-label` (Optional, string) - -When supplied, this is used to title the report annotation in place of the -repository name and tag. Useful sometimes when the repo name and tag make the -reports harder to scan visually. - -## Requirements - -### ECR Scan on Push - -This plugin assumes that the ECR repository has the `ScanOnPush` setting set (see -the [AWS -docs](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html) -for more information). By default this is not set on AWS ECR repositories. -However `Base Infrastructure for Services` configures this for all repostories -that it creates so for `cultureamp` pipelines no change should be required. - -### Agent role requires the ecr:DescribeImages permission - -The Buildkite agent needs the AWS IAM `ecr:DescribeImages` permission to -retrieve the vulnerability scan counts. Culture Amp build-roles created by `Base -Infrastructure for Services` have all been modified to include this permission. - -### Scratch images are not supported - -ECR cannot scan scratch based images, and this should be OK as the underlying -container doesn't contain packages to scan. - -If this plugin is installed and pointed at a scratch image you may receive an -error and it may block the pipeline as a result. The error -`UnsupportedImageError` is expected in this scenario; see [the ECR -docs](https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-troubleshooting.html) -for more information. - -## FAQ - -### I have a vulnerability that isn't resolved yet, but I can wait on fixing. How do I do configure this plugin so I can unblock my builds? - -Refer to how to set your [max-criticals](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-criticals-optional-string), and [max-highs](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-highs-optional-string). - -### Are there guidelines on using up? - -Yes. Changing the `max-criticals` and `max-high` settings should not be taken lightly. - -This option is effectively a deferral of fixing the vulnerability. **Assess the situation first**. If the CVE describes a scenario that aligns with how your project is used, then you should be working to fix it rather than defer it. For help on this, check out the following the steps outlined [here](https://cultureamp.atlassian.net/wiki/spaces/PST/pages/2960916852/Central+SRE+Support+FAQs#I-have-high%2Fcritical-vulnerabilities-for-my-ECR-image%2C-and-its-blocking-my-builds.-What%E2%80%99s-going-on%3F). - -Below are some recommendations if you choose to exercise this option: - -1. Set the thresholds to the number of identified high or critical vulnerabilities. This is so you’re not permitting more vulnerabilities than you should. Especially for those you can fix by updating dependencies or packages. +### `message` (Required, string) -2. Set a scheduled reminder for your team to check if a fix is available for the CVE. If a fix is available, address it, and then lower your threshold for the respective vulnerability severity. +The message to annotate onto the build. \ No newline at end of file diff --git a/catalog-info-component.yaml b/catalog-info-component.yaml index 8d5815b..840fb34 100644 --- a/catalog-info-component.yaml +++ b/catalog-info-component.yaml @@ -1,17 +1,17 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: ecr-scan-results-buildkite-plugin + name: example-go-buildkite-plugin description: | - Buildkite plugin to retrieve ECR scan results + Example Buildkite plugin to demonstate Github release delivery tags: - camp-sre - data-internal-use-only - users-internal annotations: - github.com/project-slug: cultureamp/ecr-scan-results-buildkite-plugin - github.com/team-slug: cultureamp/sre-foundations + github.com/project-slug: cultureamp/example-go-buildkite-plugin + github.com/team-slug: cultureamp/sre-enablement spec: type: library - owner: sre-foundations + owner: sre-enablement lifecycle: production diff --git a/catalog-info.yaml b/catalog-info.yaml index 069a946..8bf5faa 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -1,7 +1,7 @@ apiVersion: backstage.io/v1alpha1 kind: Location metadata: - name: ecr-scan-results-buildkite-plugin-location + name: example-go-buildkite-plugin-location tags: - camp-foundations spec: diff --git a/docker-compose.yml b/docker-compose.yml index 935c491..ac729ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: lint: image: buildkite/plugin-linter - command: ['--id', 'cultureamp/ecr-scan-results'] + command: ["--id", "cultureamp/example-go"] volumes: - ".:/plugin:ro" diff --git a/hooks/post-command b/hooks/command similarity index 100% rename from hooks/post-command rename to hooks/command diff --git a/lib/download.bash b/lib/download.bash index 40fe618..09db3af 100644 --- a/lib/download.bash +++ b/lib/download.bash @@ -65,15 +65,15 @@ downloader() { get_version() { local _plugin=${BUILDKITE_PLUGINS:-""} - local _version;_version=$(echo "$_plugin" | sed -e 's/.*ecr-scan-results-buildkite-plugin//' -e 's/\".*//') + local _version;_version=$(echo "$_plugin" | sed -e 's/.*example-go-buildkite-plugin//' -e 's/\".*//') RETVAL="$_version" } download_binary_and_run() { get_architecture || return 1 local _arch="$RETVAL" - local _executable="ecr-scan-results-buildkite-plugin" - local _repo="https://github.com/cultureamp/ecr-scan-results-buildkite-plugin" + local _executable="example-go-buildkite-plugin" + local _repo="https://github.com/cultureamp/example-go-buildkite-plugin" get_version || return 1 local _version="$RETVAL" @@ -84,16 +84,12 @@ download_binary_and_run() { _url=${_repo}/releases/download/${_version:1}/${_executable}_${_arch} fi -# local test_mode="${BUILDKITE_PLUGIN_ECR_SCAN_RESULTS_BUILDKITE_PLUGIN_TEST_MODE:-false}" - -# if [[ "$test_mode" == "false" ]]; then - if ! downloader "$_url" "$_executable"; then - say "failed to download $_url" - exit 1 - fi + if ! downloader "$_url" "$_executable"; then + say "failed to download $_url" + exit 1 + fi - chmod +x ${_executable} -# fi + chmod +x ${_executable} ./${_executable} } diff --git a/plugin.yml b/plugin.yml index fb8d767..e3cb143 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,20 +1,11 @@ -name: ECR Scan Results +name: Example Go description: > - Retrieves vulnerability scan results from ECR, creating a report as an - annotation on the build. Will fail the step if vulnerabilities exist (though - this is configurable). + Demonstrates an on-demand Github release download + and run pattern for delivery Go-based Buildkite + plugins. author: https://github.com/cultureamp -requirements: - - docker - - jq +requirements: [] configuration: properties: - image-name: + message: type: string - max-criticals: - type: string - max-highs: - type: string - image-label: - type: string - additionalProperties: false diff --git a/src/.goreleaser.yaml b/src/.goreleaser.yaml index 5c67f83..a8e3aca 100644 --- a/src/.goreleaser.yaml +++ b/src/.goreleaser.yaml @@ -1,5 +1,5 @@ builds: - - binary: ecr-scan-results-buildkite-plugin + - binary: example-go-buildkite-plugin env: - CGO_ENABLED=0 goos: @@ -13,10 +13,10 @@ builds: - ppc64le checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" archives: - - format: 'binary' + - format: "binary" name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" snapshot: @@ -24,5 +24,4 @@ snapshot: changelog: use: github-native - # yaml-language-server: $schema=https://goreleaser.com/static/schema.json diff --git a/src/Dockerfile b/src/Dockerfile index 07264ed..89b2f34 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -18,13 +18,13 @@ COPY . ./ # build as a static binary without debug symbols RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags='-w -s -extldflags "-static"' -a \ - -o /dist/ecrscanresults . + -o /dist/examplego . # runtime image using static distroless base # using static nonroot image # user:group is nobody:nobody, uid:gid = 65534:65534 FROM ${DISTROLESS_IMAGE} -COPY --from=builder /dist/ecrscanresults /ecrscanresults +COPY --from=builder /dist/examplego /examplego -ENTRYPOINT ["/ecrscanresults"] +ENTRYPOINT ["/examplego"] diff --git a/src/buildkite/agent.go b/src/buildkite/agent.go index a5ab246..5dc0d7d 100644 --- a/src/buildkite/agent.go +++ b/src/buildkite/agent.go @@ -14,14 +14,10 @@ import ( type Agent struct { } -func (a Agent) Annotate(ctx context.Context, message string, style string, annotationContext string) error { +func (a *Agent) Annotate(ctx context.Context, message string, style string, annotationContext string) error { return execCmd(ctx, "buildkite-agent", &message, "annotate", "--style", style, "--context", annotationContext) } -func (a Agent) ArtifactUpload(ctx context.Context, path string) error { - return execCmd(ctx, "buildkite-agent", nil, "artifact", "upload", path) -} - func execCmd(ctx context.Context, executableName string, stdin *string, args ...string) error { Logf("Executing: %s %s\n", executableName, strings.Join(args, " ")) diff --git a/src/go.mod b/src/go.mod index 99ccc88..81c6463 100644 --- a/src/go.mod +++ b/src/go.mod @@ -1,43 +1,21 @@ -module github.com/cultureamp/ecrscanresults +module github.com/cultureamp/examplego go 1.20 require ( - github.com/MarvinJWendt/testza v0.4.1 - github.com/aws/aws-sdk-go-v2 v1.17.7 - github.com/aws/aws-sdk-go-v2/config v1.18.19 - github.com/aws/aws-sdk-go-v2/service/ecr v1.18.7 - github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d github.com/kelseyhightower/envconfig v1.4.0 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.8.4 ) -require github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect +require ( + github.com/kr/pretty v0.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) require ( - github.com/atomicgo/cursor v0.0.1 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect - github.com/aws/smithy-go v1.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gookit/color v1.5.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/justincampbell/bigduration v0.0.0-20160531141349-e45bf03c0666 // indirect - github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/pterm/pterm v0.12.41 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/sys v0.6.0 - golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect - golang.org/x/text v0.8.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 2a81a83..1e5348a 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,113 +1,28 @@ -github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= -github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= -github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= -github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= -github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= -github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY= -github.com/MarvinJWendt/testza v0.4.1 h1:bqidLqFVtySvyq7D+xIfFKefl+AfJtDpivXC9fx3hm4= -github.com/MarvinJWendt/testza v0.4.1/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU= -github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= -github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= -github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY= -github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c= -github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.7 h1:oQ1Esut3iaL2Dydt2RBd9gbuUevToXpdTI+Uh1xXryI= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.7/go.mod h1:RHhgOMnMIkgB4TmxQat9obSnZ6fF1fuA27+itZKUi1o= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= -github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/justincampbell/bigduration v0.0.0-20160531141349-e45bf03c0666 h1:abLciEiilfMf19Q1TFWDrp9j5z5one60dnnpvc6eabg= -github.com/justincampbell/bigduration v0.0.0-20160531141349-e45bf03c0666/go.mod h1:xqGOmDZzLOG7+q/CgsbXv10g4tgPsbjhmAxyaTJMvis= -github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d h1:qtCcYJK2bebPXEC8Wy+enYxQqmWnT6jlVTHnDGpwvkc= -github.com/justincampbell/timeago v0.0.0-20160528003754-027f40306f1d/go.mod h1:U7FWcK1jzZJnYuSnxP6efX3ZoHbK1CEpD0ThYyGNPNI= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= -github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= -github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= -github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= -github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= -github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= -github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.41 h1:e2BRfFo1H9nL8GY0S3ImbZqfZ/YimOk9XtkhoobKJVs= -github.com/pterm/pterm v0.12.41/go.mod h1:LW/G4J2A42XlTaPTAGRPvbBfF4UXvHWhC6SN7ueU4jU= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= -golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/main.go b/src/main.go index b0a1ca1..82711da 100644 --- a/src/main.go +++ b/src/main.go @@ -2,170 +2,22 @@ package main import ( "context" - "crypto/sha256" - "errors" - "fmt" - "io/fs" "os" - "strings" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/cultureamp/ecrscanresults/buildkite" - "github.com/cultureamp/ecrscanresults/registry" - "github.com/cultureamp/ecrscanresults/report" - "github.com/cultureamp/ecrscanresults/runtimeerrors" - "github.com/kelseyhightower/envconfig" + "github.com/cultureamp/examplego/buildkite" + "github.com/cultureamp/examplego/plugin" ) -const pluginEnvironmentPrefix = "BUILDKITE_PLUGIN_ECR_SCAN_RESULTS" - -type Config struct { - Repository string `envconfig:"IMAGE_NAME" split_words:"true" required:"true"` - ImageLabel string `envconfig:"IMAGE_LABEL" split_words:"true"` - CriticalSeverityThreshold int32 `envconfig:"MAX_CRITICALS" split_words:"true"` - HighSeverityThreshold int32 `envconfig:"MAX_HIGHS" split_words:"true"` -} - func main() { - var pluginConfig Config - if err := envconfig.Process(pluginEnvironmentPrefix, &pluginConfig); err != nil { - buildkite.LogFailuref("plugin configuration error: %s\n", err.Error()) - os.Exit(1) - } - if pluginConfig.CriticalSeverityThreshold < 0 { - buildkite.LogFailuref("max-criticals must be greater than or equal to 0") - os.Exit(1) - } - if pluginConfig.HighSeverityThreshold < 0 { - buildkite.LogFailuref("max-highs must be greater than or equal to 0") - os.Exit(1) - } - ctx := context.Background() - agent := buildkite.Agent{} - - err := runCommand(ctx, pluginConfig, agent) - if err != nil { - buildkite.LogFailuref("plugin execution failed: %s\n", err.Error()) - - // For this plugin, we don't want to block the build on most errors: - // scan access and availability can be quite flakey. For this reason, we - // wrap most issues in a non-fatal error type. - if runtimeerrors.IsFatal(err) { - os.Exit(1) - } else { - // Attempt to annotate the build with the issue, but it's OK if the - // annotation fails. We annotate to notify the user of the issue, - // otherwise it would be lost in the log. - annotation := fmt.Sprintf("ECR scan results plugin could not create a result for the image %s", "") - _ = agent.Annotate(ctx, annotation, "error", hash(pluginConfig.Repository)) - } - } -} + agent := &buildkite.Agent{} + fetcher := plugin.EnvironmentConfigFetcher{} + examplePlugin := plugin.ExamplePlugin{} -func runCommand(ctx context.Context, pluginConfig Config, agent buildkite.Agent) error { - buildkite.Logf("Scan results report requested for %s\n", pluginConfig.Repository) - buildkite.Logf("Thresholds: criticals %d highs %d\n", pluginConfig.CriticalSeverityThreshold, pluginConfig.HighSeverityThreshold) + err := examplePlugin.Run(ctx, fetcher, agent) - imageID, err := registry.RegistryInfoFromURL(pluginConfig.Repository) if err != nil { - return err - } - - awsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(imageID.Region)) - if err != nil { - return runtimeerrors.NonFatal("could not configure AWS access", err) - } - - scan, err := registry.NewRegistryScan(awsConfig) - if err != nil { - return runtimeerrors.NonFatal("could not set up ECR access", err) - } - - buildkite.Logf("Getting image digest for %s\n", imageID) - imageDigest, err := scan.GetLabelDigest(ctx, imageID) - if err != nil { - return runtimeerrors.NonFatal("could not find digest for image", err) - } - - buildkite.Logf("Digest: %s\n", imageDigest) - - buildkite.LogGroupf(":ecr: Creating ECR scan results report for %s\n", imageID) - err = scan.WaitForScanFindings(ctx, imageDigest) - if err != nil { - return runtimeerrors.NonFatal("could not retrieve scan results", err) - } - - buildkite.Log("report ready, retrieving ...") - - findings, err := scan.GetScanFindings(ctx, imageDigest) - if err != nil { - return runtimeerrors.NonFatal("could not retrieve scan results", err) - } - - buildkite.Logf("retrieved. %d findings in report.\n", len(findings.ImageScanFindings.Findings)) - - criticalFindings := findings.ImageScanFindings.FindingSeverityCounts["CRITICAL"] - highFindings := findings.ImageScanFindings.FindingSeverityCounts["HIGH"] - overThreshold := - criticalFindings > pluginConfig.CriticalSeverityThreshold || - highFindings > pluginConfig.HighSeverityThreshold - - buildkite.Logf("Severity counts: critical=%d high=%d overThreshold=%v\n", criticalFindings, highFindings, overThreshold) - - buildkite.Log("Creating report annotation...") - annotationCtx := report.AnnotationContext{ - Image: imageID, - ImageLabel: pluginConfig.ImageLabel, - ScanFindings: *findings.ImageScanFindings, - CriticalSeverityThreshold: pluginConfig.CriticalSeverityThreshold, - HighSeverityThreshold: pluginConfig.HighSeverityThreshold, - } - - annotation, err := annotationCtx.Render() - if err != nil { - return runtimeerrors.NonFatal("could not render report", err) - } - buildkite.Log("done.") - - annotationStyle := "info" - if overThreshold { - annotationStyle = "error" - } else if criticalFindings > 0 || highFindings > 0 { - annotationStyle = "warning" - } - - err = agent.Annotate(ctx, string(annotation), annotationStyle, "scan_results_"+imageDigest.Tag) - if err != nil { - return runtimeerrors.NonFatal("could not annotate build", err) - } - - buildkite.Log("Uploading report as an artifact...") - filename := fmt.Sprintf("result.%s.html", strings.TrimPrefix(imageDigest.Tag, "sha256:")) - err = os.WriteFile(filename, annotation, fs.ModePerm) - if err != nil { - return runtimeerrors.NonFatal("could not write report artifact", err) - } - - err = agent.ArtifactUpload(ctx, "result*.html") - if err != nil { - return runtimeerrors.NonFatal("could not upload report artifact", err) - } - - buildkite.Log("done.") - - // exceeding threshold is a fatal error - if overThreshold { - return errors.New("vulnerability threshold exceeded") - } - - return nil -} - -func hash(data ...string) string { - h := sha256.New() - for _, d := range data { - h.Write([]byte(d)) + buildkite.LogFailuref("plugin execution failed: %s\n", err.Error()) + os.Exit(1) } - return fmt.Sprintf("%x", h.Sum(nil)) } diff --git a/src/plugin/config.go b/src/plugin/config.go new file mode 100644 index 0000000..e3d07fd --- /dev/null +++ b/src/plugin/config.go @@ -0,0 +1,18 @@ +package plugin + +import ( + "github.com/kelseyhightower/envconfig" +) + +type Config struct { + Message string `envconfig:"MESSAGE" required:"true"` +} + +type EnvironmentConfigFetcher struct { +} + +const pluginEnvironmentPrefix = "BUILDKITE_PLUGIN_EXAMPLE_GO" + +func (f EnvironmentConfigFetcher) Fetch(config *Config) error { + return envconfig.Process(pluginEnvironmentPrefix, config) +} diff --git a/src/plugin/config_test.go b/src/plugin/config_test.go new file mode 100644 index 0000000..1ea9476 --- /dev/null +++ b/src/plugin/config_test.go @@ -0,0 +1,35 @@ +package plugin_test + +import ( + "os" + "testing" + + "github.com/cultureamp/examplego/plugin" + "github.com/stretchr/testify/assert" +) + +func TestFailOnMissingEnvironment(t *testing.T) { + + var config plugin.Config + fetcher := plugin.EnvironmentConfigFetcher{} + + t.Setenv("BUILDKITE_PLUGIN_EXAMPLE_GO_MESSAGE", "") + os.Unsetenv("BUILDKITE_PLUGIN_EXAMPLE_GO_MESSAGE") + + err := fetcher.Fetch(&config) + + assert.NotNil(t, err, "fetch should error") +} + +func TestFetchConfigFromEnvironment(t *testing.T) { + + var config plugin.Config + fetcher := plugin.EnvironmentConfigFetcher{} + + t.Setenv("BUILDKITE_PLUGIN_EXAMPLE_GO_MESSAGE", "test-message") + + err := fetcher.Fetch(&config) + + assert.Nil(t, err, "fetch should not error") + assert.Equal(t, config.Message, "test-message", "fetched message should match environment") +} diff --git a/src/plugin/example.go b/src/plugin/example.go new file mode 100644 index 0000000..0fcde5c --- /dev/null +++ b/src/plugin/example.go @@ -0,0 +1,39 @@ +package plugin + +import ( + "context" + + "github.com/cultureamp/examplego/buildkite" +) + +type ExamplePlugin struct { +} + +type ConfigFetcher interface { + Fetch(config *Config) error +} + +type Agent interface { + Annotate(ctx context.Context, message string, style string, annotationContext string) error +} + +func (ep ExamplePlugin) Run(ctx context.Context, fetcher ConfigFetcher, agent Agent) error { + var config Config + err := fetcher.Fetch(&config) + if err != nil { + buildkite.LogFailuref("plugin configuration error: %s\n", err.Error()) + return err + } + annotation := config.Message + + buildkite.Logf("Annotating with message: %s\n", annotation) + + err = agent.Annotate(ctx, annotation, "info", "message") + if err != nil { + buildkite.LogFailuref("buildkite annotation error: %s\n", err.Error()) + return err + } + + buildkite.Log("done.") + return nil +} diff --git a/src/plugin/example_mocks_test.go b/src/plugin/example_mocks_test.go new file mode 100644 index 0000000..efc4640 --- /dev/null +++ b/src/plugin/example_mocks_test.go @@ -0,0 +1,16 @@ +package plugin_test + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type AgentMock struct { + mock.Mock +} + +func (m *AgentMock) Annotate(ctx context.Context, message string, style string, annotationContext string) error { + args := m.Called(ctx, message, style, annotationContext) + return args.Error(0) +} diff --git a/src/plugin/example_test.go b/src/plugin/example_test.go new file mode 100644 index 0000000..72c3873 --- /dev/null +++ b/src/plugin/example_test.go @@ -0,0 +1,24 @@ +package plugin_test + +import ( + "context" + "testing" + + "github.com/cultureamp/examplego/plugin" + "github.com/stretchr/testify/assert" +) + +func TestDoesAnnotate(t *testing.T) { + agent := &AgentMock{} + fetcher := plugin.EnvironmentConfigFetcher{} + examplePlugin := plugin.ExamplePlugin{} + ctx := context.Background() + annotation := "test-message" + + t.Setenv("BUILDKITE_PLUGIN_EXAMPLE_GO_MESSAGE", annotation) + agent.Mock.On("Annotate", ctx, annotation, "info", "message").Return(nil) + + err := examplePlugin.Run(ctx, fetcher, agent) + + assert.Nil(t, err, "should not error") +} diff --git a/src/registry/ecr.go b/src/registry/ecr.go deleted file mode 100644 index 76d7a95..0000000 --- a/src/registry/ecr.go +++ /dev/null @@ -1,148 +0,0 @@ -package registry - -import ( - "context" - "fmt" - "regexp" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" -) - -var registryImageExpr = regexp.MustCompile(`^(?P[^.]+)\.dkr\.ecr\.(?P[^.]+).amazonaws.com/(?P[^:]+)(?::(?P.+))?$`) - -type RegistryInfo struct { - // RegistryID is the AWS ECR account ID of the source registry. - RegistryID string - // Region is the AWS region of the registry. - Region string - // Name is the ECR repository name. - Name string - // Tag is the image label or an image digest. - Tag string -} - -func (i RegistryInfo) String() string { - return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com/%s:%s", i.RegistryID, i.Region, i.Name, i.Tag) -} - -func RegistryInfoFromURL(url string) (RegistryInfo, error) { - info := RegistryInfo{} - names := registryImageExpr.SubexpNames() - match := registryImageExpr.FindStringSubmatch(url) - if match == nil { - return info, fmt.Errorf("invalid registry URL: %s", url) - } - - // build the struct using the named subexpressions from the expression - for i, value := range match { - nm := names[i] - switch nm { - case "registryId": - info.RegistryID = value - case "region": - info.Region = value - case "repoName": - info.Name = value - case "tag": - info.Tag = value - } - } - - return info, nil -} - -type RegistryScan struct { - Client *ecr.Client -} - -func NewRegistryScan(config aws.Config) (*RegistryScan, error) { - client := ecr.NewFromConfig(config) - - return &RegistryScan{ - Client: client, - }, nil -} - -func (r *RegistryScan) GetLabelDigest(ctx context.Context, imageInfo RegistryInfo) (RegistryInfo, error) { - out, err := r.Client.DescribeImages(ctx, &ecr.DescribeImagesInput{ - RegistryId: &imageInfo.RegistryID, - RepositoryName: &imageInfo.Name, - ImageIds: []types.ImageIdentifier{ - { - ImageTag: &imageInfo.Tag, - }, - }, - }) - if err != nil { - return RegistryInfo{}, err - } - if len(out.ImageDetails) == 0 { - return RegistryInfo{}, fmt.Errorf("no image found for image %s", imageInfo) - } - - // copy input and update tag from label to digest - digestInfo := imageInfo - digestInfo.Tag = *out.ImageDetails[0].ImageDigest - - return digestInfo, nil -} - -func (r *RegistryScan) WaitForScanFindings(ctx context.Context, digestInfo RegistryInfo) error { - waiter := ecr.NewImageScanCompleteWaiter(r.Client) - - // wait between attempts for between 3 and 15 secs (exponential backoff) - // wait for a maximum of 3 minutes - minAttemptDelay := 3 * time.Second - maxAttemptDelay := 15 * time.Second - maxTotalDelay := 3 * time.Minute - - return waiter.Wait(ctx, &ecr.DescribeImageScanFindingsInput{ - RegistryId: &digestInfo.RegistryID, - RepositoryName: &digestInfo.Name, - ImageId: &types.ImageIdentifier{ - ImageDigest: &digestInfo.Tag, - }, - }, maxTotalDelay, func(opts *ecr.ImageScanCompleteWaiterOptions) { - opts.LogWaitAttempts = true - opts.MinDelay = minAttemptDelay - opts.MaxDelay = maxAttemptDelay - }) -} - -func (r *RegistryScan) GetScanFindings(ctx context.Context, digestInfo RegistryInfo) (*ecr.DescribeImageScanFindingsOutput, error) { - pg := ecr.NewDescribeImageScanFindingsPaginator(r.Client, &ecr.DescribeImageScanFindingsInput{ - RegistryId: &digestInfo.RegistryID, - RepositoryName: &digestInfo.Name, - ImageId: &types.ImageIdentifier{ - ImageDigest: &digestInfo.Tag, - }, - }) - - var out *ecr.DescribeImageScanFindingsOutput - - for pg.HasMorePages() { - pg, err := pg.NextPage(ctx) - if err != nil { - return nil, err - } - - if out == nil { - out = pg - } else if out.ImageScanFindings != nil { - findings := out.ImageScanFindings - if findings == nil { - findings = &types.ImageScanFindings{} - out.ImageScanFindings = findings - } - - // build the entire set in memory 🤞 - findings.Findings = append(findings.Findings, pg.ImageScanFindings.Findings...) - findings.EnhancedFindings = append(findings.EnhancedFindings, pg.ImageScanFindings.EnhancedFindings...) - } - } - - return out, nil -} diff --git a/src/registry/ecr_test.go b/src/registry/ecr_test.go deleted file mode 100644 index ea5dfa2..0000000 --- a/src/registry/ecr_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package registry - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRegistryInfoFromURLSucceeds(t *testing.T) { - cases := []struct { - test string - url string - expected RegistryInfo - }{ - { - test: "Url with label", - url: "123456789012.dkr.ecr.us-west-2.amazonaws.com/test-repo:latest", - expected: RegistryInfo{ - RegistryID: "123456789012", - Region: "us-west-2", - Name: "test-repo", - Tag: "latest", - }, - }, - { - test: "Url without label", - url: "123456789012.dkr.ecr.us-west-2.amazonaws.com/test-repo", - expected: RegistryInfo{ - RegistryID: "123456789012", - Region: "us-west-2", - Name: "test-repo", - Tag: "", - }, - }, - } - - for _, c := range cases { - t.Run(c.test, func(t *testing.T) { - info, err := RegistryInfoFromURL(c.url) - require.NoError(t, err) - assert.Equal(t, c.expected, info) - }) - } -} - -func TestRegistryInfoFromURLFails(t *testing.T) { - url := "123456789012.dkr.ecr.us-west-2.amazonaws.com" - - info, err := RegistryInfoFromURL(url) - require.Error(t, err) - assert.ErrorContains(t, err, "invalid registry URL") - - assert.Equal(t, RegistryInfo{}, info) -} diff --git a/src/report/annotation.go b/src/report/annotation.go deleted file mode 100644 index 62ce8fa..0000000 --- a/src/report/annotation.go +++ /dev/null @@ -1,83 +0,0 @@ -package report - -import ( - "bytes" - _ "embed" - "fmt" - "html/template" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" - "github.com/cultureamp/ecrscanresults/registry" - "github.com/justincampbell/timeago" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -//go:embed annotation.gohtml -var annotationTemplateSource string - -type AnnotationContext struct { - Image registry.RegistryInfo - ImageLabel string - ScanFindings types.ImageScanFindings - CriticalSeverityThreshold int32 - HighSeverityThreshold int32 -} - -func (c AnnotationContext) Render() ([]byte, error) { - t, err := template. - New("annotation"). - Funcs(template.FuncMap{ - "titleCase": func(s string) string { - c := cases.Title(language.English) - return c.String(s) - }, - "lowerCase": strings.ToLower, - "findingAttribute": findingAttributeValue, - "nbsp": func(input string) any { - if len(input) > 0 { - return input - } else { - return template.HTML(` `) - } - }, - "timeAgo": func(tm *time.Time) string { - if tm == nil { - return "" - } - - return timeago.FromTime(*tm) - }, - "string": func(input any) (string, error) { - if strg, ok := input.(fmt.Stringer); ok { - return strg.String(), nil - } - - return fmt.Sprintf("%s", input), nil - }, - }). - Parse(annotationTemplateSource) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - err = t.Execute(&buf, c) - if err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func findingAttributeValue(name string, finding types.ImageScanFinding) string { - for _, a := range finding.Attributes { - if aws.ToString(a.Key) == name { - return aws.ToString(a.Value) - } - } - return "" -} diff --git a/src/report/annotation.gohtml b/src/report/annotation.gohtml deleted file mode 100644 index 7cf483e..0000000 --- a/src/report/annotation.gohtml +++ /dev/null @@ -1,63 +0,0 @@ -{{/* - -Expects an instance of AnnotationContext as its context. - -This template renders _MARKDOWN_, even though it's mostly HTML. This is why -there is no indentation: indented output can be rendered differently. - -*/}} -{{ $criticalThreshold := .CriticalSeverityThreshold }} -{{ $highThreshold := .HighSeverityThreshold }} -{{ if .ImageLabel }} -

Vulnerability summary for "{{ .ImageLabel }}"

-

{{ .Image.Name }}:{{ .Image.Tag }}

-{{ else }} -

Vulnerability summary for "{{ .Image.Name }}:{{ .Image.Tag }}"

-{{ end }} -{{ if .ScanFindings.FindingSeverityCounts }} -
-{{ range $key, $value := .ScanFindings.FindingSeverityCounts }} -{{ $exceedsThreshold := (or - (and (eq $key "CRITICAL") (gt $value $criticalThreshold)) - (and (eq $key "HIGH") (gt $value $highThreshold)) -) }} -
-
{{ $key | lowerCase | titleCase }}
-

{{ $value }}

-
-
-{{ end }} -
-{{ else }} -

No vulnerabilities reported.

-{{ end }} -{{ if .ScanFindings.Findings }} -
-Vulnerability details -
- - - - - - - - -{{ range $f := .ScanFindings.Findings }} -{{ $vector := $f | findingAttribute "CVSS2_VECTOR"}} - - - - - - - -{{ end }} -
CVESeverityEffectsCVSS scoreVector
{{ if $f.Uri }}{{ $f.Name }}{{ else }}{{ $f.Name }}{{ end }}{{ $f.Severity | string | lowerCase | titleCase }}{{ $f | findingAttribute "package_name" | nbsp }} {{ $f | findingAttribute "package_version" | nbsp }}{{ $f | findingAttribute "CVSS2_SCORE" | nbsp}}{{ if $vector }}{{ $vector }}{{ else }} {{ end }}
-
-
-{{ end }} -

-scan completed: {{ .ScanFindings.ImageScanCompletedAt | timeAgo }} | -source updated: {{ .ScanFindings.VulnerabilitySourceUpdatedAt | timeAgo }} -

diff --git a/src/report/annotation_test.go b/src/report/annotation_test.go deleted file mode 100644 index 4b019a3..0000000 --- a/src/report/annotation_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package report_test - -import ( - "fmt" - "testing" - - "github.com/MarvinJWendt/testza" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/ecr/types" - "github.com/cultureamp/ecrscanresults/registry" - "github.com/cultureamp/ecrscanresults/report" -) - -func TestReports(t *testing.T) { - cases := []struct { - name string - data report.AnnotationContext - }{ - { - name: "no vulnerabilities", - data: report.AnnotationContext{ - Image: registry.RegistryInfo{ - RegistryID: "0123456789", - Region: "us-west-2", - Name: "test-repo", - Tag: "digest-value", - }, - ImageLabel: "", - ScanFindings: types.ImageScanFindings{}, - CriticalSeverityThreshold: 0, - HighSeverityThreshold: 0, - }, - }, - { - name: "image label", - data: report.AnnotationContext{ - Image: registry.RegistryInfo{ - RegistryID: "0123456789", - Region: "us-west-2", - Name: "test-repo", - Tag: "digest-value", - }, - ImageLabel: "label of image", - ScanFindings: types.ImageScanFindings{}, - CriticalSeverityThreshold: 0, - HighSeverityThreshold: 0, - }, - }, - { - name: "findings included", - data: report.AnnotationContext{ - Image: registry.RegistryInfo{ - RegistryID: "0123456789", - Region: "us-west-2", - Name: "test-repo", - Tag: "digest-value", - }, - ImageLabel: "label of image", - ScanFindings: types.ImageScanFindings{ - FindingSeverityCounts: map[string]int32{ - "HIGH": 1, - }, - Findings: []types.ImageScanFinding{ - { - Name: aws.String("CVE-2019-5188"), - Description: aws.String("A code execution vulnerability exists in the directory rehashing functionality of E2fsprogs e2fsck 1.45.4. A specially crafted ext4 directory can cause an out-of-bounds write on the stack, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability."), - Uri: aws.String("http://people.ubuntu.com/~ubuntu-security/cve/CVE-2019-5188"), - Severity: "HIGH", - Attributes: []types.Attribute{ - { - Key: aws.String("package_version"), - Value: aws.String("1.44.1-1ubuntu1.1"), - }, - { - Key: aws.String("package_name"), - Value: aws.String("e2fsprogs"), - }, - { - Key: aws.String("CVSS2_VECTOR"), - Value: aws.String("AV:L/AC:L/Au:N/C:P/I:P/A:P"), - }, - { - Key: aws.String("CVSS2_SCORE"), - Value: aws.String("4.6"), - }, - }, - }, - }, - }, - CriticalSeverityThreshold: 0, - HighSeverityThreshold: 0, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - fmt.Println(c.name, t.Name()) - result, err := c.data.Render() - - testza.AssertNoError(t, err) - err = testza.SnapshotCreateOrValidate(t, t.Name(), string(result)) - testza.AssertNoError(t, err) - }) - } -} diff --git a/src/report/testdata/snapshots/TestReports/findings_included.testza b/src/report/testdata/snapshots/TestReports/findings_included.testza deleted file mode 100755 index 05a83df..0000000 --- a/src/report/testdata/snapshots/TestReports/findings_included.testza +++ /dev/null @@ -1 +0,0 @@ -(string) (len=899) "\n\n\n\n

Vulnerability summary for \"label of image\"

\n

test-repo:digest-value

\n\n\n
\n\n\n
\n
High
\n

1

\n
\n
\n\n
\n\n\n
\nVulnerability details\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
CVESeverityEffectsCVSS scoreVector
CVE-2019-5188Highe2fsprogs 1.44.1-1ubuntu1.14.6AV:L/AC:L/Au:N/C:P/I:P/A:P
\n
\n
\n\n

\nscan completed: |\nsource updated: \n

\n" diff --git a/src/report/testdata/snapshots/TestReports/image_label.testza b/src/report/testdata/snapshots/TestReports/image_label.testza deleted file mode 100755 index cc43a1e..0000000 --- a/src/report/testdata/snapshots/TestReports/image_label.testza +++ /dev/null @@ -1 +0,0 @@ -(string) (len=288) "\n\n\n\n

Vulnerability summary for \"label of image\"

\n

test-repo:digest-value

\n\n\n

No vulnerabilities reported.

\n\n\n

\nscan completed: |\nsource updated: \n

\n" diff --git a/src/report/testdata/snapshots/TestReports/no_vulnerabilities.testza b/src/report/testdata/snapshots/TestReports/no_vulnerabilities.testza deleted file mode 100755 index c49c013..0000000 --- a/src/report/testdata/snapshots/TestReports/no_vulnerabilities.testza +++ /dev/null @@ -1 +0,0 @@ -(string) (len=240) "\n\n\n\n

Vulnerability summary for \"test-repo:digest-value\"

\n\n\n

No vulnerabilities reported.

\n\n\n

\nscan completed: |\nsource updated: \n

\n" diff --git a/src/runtimeerrors/error.go b/src/runtimeerrors/error.go deleted file mode 100644 index ef1afad..0000000 --- a/src/runtimeerrors/error.go +++ /dev/null @@ -1,32 +0,0 @@ -package runtimeerrors - -import "errors" - -// NonFatalError is an error that will not cause the plugin to fail. -type NonFatalError struct { - Message string - Wrapped error -} - -func (e NonFatalError) Error() string { - m := e.Message - if e.Wrapped != nil { - m += ": " + e.Wrapped.Error() - } - return m -} - -func (e NonFatalError) Unwrap() error { - return e.Wrapped -} - -func NonFatal(message string, err error) NonFatalError { - return NonFatalError{ - Message: message, - Wrapped: err, - } -} - -func IsFatal(err error) bool { - return !errors.As(err, &NonFatalError{}) -} diff --git a/src/runtimeerrors/error_test.go b/src/runtimeerrors/error_test.go deleted file mode 100644 index 8a181e9..0000000 --- a/src/runtimeerrors/error_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package runtimeerrors_test - -import ( - "fmt" - "testing" - - "github.com/cultureamp/ecrscanresults/runtimeerrors" - "github.com/stretchr/testify/assert" -) - -func TestIIsFatal(t *testing.T) { - cases := []struct { - test string - result bool - err error - }{ - { - test: "true when nil", - result: true, - err: nil, - }, - { - test: "true on normal error", - result: true, - err: assert.AnError, - }, - { - test: "false on nonfatal error", - result: false, - err: runtimeerrors.NonFatalError{}, - }, - { - test: "false on wrapped nonfatal error", - result: false, - err: fmt.Errorf("wrapped %w", runtimeerrors.NonFatal("end of the line", nil)), - }, - { - test: "false on nonfatal error in chain", - result: false, - err: fmt.Errorf("wrapped %w", runtimeerrors.NonFatal("wrapped further", assert.AnError)), - }, - } - - for _, c := range cases { - t.Run(c.test, func(t *testing.T) { - result := runtimeerrors.IsFatal(c.err) - assert.Equal(t, c.result, result) - }) - } -} diff --git a/tests/download.bats b/tests/download.bats index 38e140a..1127982 100755 --- a/tests/download.bats +++ b/tests/download.bats @@ -18,7 +18,7 @@ setup() { teardown() { unset BUILDKITE_PLUGIN_ECR_SCAN_RESULTS_BUILDKITE_PLUGIN_TEST_MODE - rm ./ecr-scan-results-buildkite-plugin || true + rm ./example-go-buildkite-plugin || true } create_script() { @@ -43,6 +43,6 @@ EOM unset downloader assert_success - assert_line --regexp "https://github.com/cultureamp/ecr-scan-results-buildkite-plugin/releases/latest/download/ecr-scan-results-buildkite-plugin_linux_amd64 ecr-scan-results-buildkite-plugin" - assert_line --regexp "executing ecr-scan-results-buildkite-plugin" + assert_line --regexp "https://github.com/cultureamp/example-go-buildkite-plugin/releases/latest/download/example-go-buildkite-plugin_linux_amd64 example-go-buildkite-plugin" + assert_line --regexp "executing example-go-buildkite-plugin" }