diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..37767b5b --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,62 @@ + +categories: + - title: '⚠️ Breaking changes' + labels: + - 'kind/major' + - 'kind/breaking-change' + - title: '🚀 Features' + labels: + - 'kind/enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'kind/bug' + - title: '🧰 Maintenance' + labels: + - 'kind/chore' + - 'area/dependencies' + +exclude-labels: + - duplicate + - invalid + - later + - wontfix + - kind/question + - skip-changelog + +change-template: '- $TITLE (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +name-template: 'v$RESOLVED_VERSION' +template: | + $CHANGES + +autolabeler: + # Tag any PR with "!" in the subject as major update. In other words, breaking change + - label: 'kind/breaking-change' + title: '/.*!:.*/' + - label: 'area/dependencies' + title: 'chore(deps)' + - label: 'kind/enhancement' + title: 'feat' + - label: 'kind/bug' + title: 'fix' + - label: 'kind/chore' + title: 'chore' + +version-resolver: + major: + labels: + - 'kind/major' + - 'kind/breaking-change' + minor: + labels: + - 'kind/minor' + - 'kind/feature' + - 'kind/enhancement' + patch: + labels: + - 'area/dependencies' + - 'kind/patch' + - 'kind/fix' + - 'kind/bug' + - 'kind/chore' + default: patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3f5837a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + workflow_call: + push: + pull_request: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + unit_tests: + name: Unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '1.19' + - run: make unit-tests + + golangci: + name: Golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '1.19' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.49.0 diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 00000000..7590e496 --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,52 @@ +name: Build container image +# to depend on other workflows, or provide container image for all branches + +on: + workflow_call: + outputs: + digest: + description: "Container image digest" + value: ${{jobs.build.outputs.digest}} + push: + branches: + - "*" + +jobs: + build: + name: Build + uses: kubewarden/audit-scanner/.github/workflows/reusable-container-image.yml@main + permissions: + packages: write + with: + push-image: true + + sign: + runs-on: ubuntu-latest + permissions: + packages: write + id-token: write + needs: build + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: sigstore/cosign-installer@main + - name: Sign the images + run: | + cosign sign \ + ${{needs.build.outputs.repository}}@${{needs.build.outputs.digest}} + env: + COSIGN_EXPERIMENTAL: 1 + + - uses: sigstore/cosign-installer@main + - name: Sign the SBOM + run: | + tag=$(echo '${{needs.build.outputs.digest}}' | sed 's/:/-/g') + cosign sign \ + "${{needs.build.outputs.repository}}:$tag.sbom" + env: + COSIGN_EXPERIMENTAL: 1 diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml new file mode 100644 index 00000000..071f4e86 --- /dev/null +++ b/.github/workflows/fossa.yml @@ -0,0 +1,20 @@ +--- +name: fossa scanning +on: + push: + tags: + - 'v*' + branches: + - 'main' + +# Declare default permissions as read only. +permissions: read-all + +jobs: + fossa-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: fossas/fossa-action@v1.3.1 + with: + api-key: ${{secrets.FOSSA_API_TOKEN}} diff --git a/.github/workflows/openssf.yml b/.github/workflows/openssf.yml new file mode 100644 index 00000000..b81c6b75 --- /dev/null +++ b/.github/workflows/openssf.yml @@ -0,0 +1,32 @@ +name: Scorecards supply-chain security +on: + push: + branches: [ main ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Used to receive a badge. (Upcoming feature) + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@v2.0.3 + with: + results_file: results.sarif + results_format: sarif + # Publish the results for public repositories to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..a86cd9ec --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,37 @@ +name: Release Drafter + +on: + workflow_dispatch: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..113291de --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: audit-scanner release +on: + push: + tags: + - 'v*' + +# Declare default permissions as read only. +permissions: read-all + +jobs: + ci: + uses: kubewarden/audit-scanner/.github/workflows/ci.yml@main + permissions: read-all + + container-build: + uses: kubewarden/audit-scanner/.github/workflows/container-build.yml@main + permissions: + id-token: write + packages: write + release: + permissions: + id-token: write + contents: write + name: Create release + runs-on: ubuntu-latest + needs: + - ci + - container-build + steps: + - name: Install Golang + uses: actions/setup-go@v3 + with: + go-version: '1.19' + + - name: Install the bom command + shell: bash + run: go install sigs.k8s.io/bom/cmd/bom@v0.2.2 + + - name: Install cosign + uses: sigstore/cosign-installer@main + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Retrieve tag name + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + echo TAG_NAME=$(echo ${{ github.ref_name }}) >> $GITHUB_ENV + + - name: Create SBOM file + shell: bash + run: | + bom generate -n https://kubewarden.io/kubewarden.spdx \ + --image "ghcr.io/${{github.repository_owner}}/audit-scanner@${{ needs.container-build.outputs.digest }}" \ + . > audit-scanner-sbom.spdx + + - name: Sign BOM file + run: | + cosign sign-blob --output-certificate audit-scanner-sbom.spdx.cert \ + --output-signature audit-scanner-sbom.spdx.sig \ + audit-scanner-sbom.spdx + env: + COSIGN_EXPERIMENTAL: 1 + + - name: Get latest release tag + id: get_last_release_tag + uses: actions/github-script@v6 + with: + script: | + let release = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + if (release.status === 200 ) { + core.setOutput('old_release_tag', release.data.tag_name) + return + } + core.setFailed("Cannot find latest release") + + - name: Get release ID from the release created by release drafter + uses: actions/github-script@v6 + with: + script: | + let releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + for (const release of releases.data) { + if (release.draft) { + core.info(release) + core.exportVariable('RELEASE_ID', release.id) + return + } + } + core.setFailed(`Draft release not found`) + + - name: Upload release assets + id: upload_release_assets + uses: actions/github-script@v6 + with: + script: | + let fs = require('fs'); + let files = ['audit-scanner-sbom.spdx', 'audit-scanner-sbom.spdx.cert', 'audit-scanner-sbom.spdx.sig', "CRDS.tar.gz"] + const {RELEASE_ID} = process.env + + for (const file of files) { + let file_data = fs.readFileSync(file); + + let response = await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: `${RELEASE_ID}`, + name: file, + data: file_data, + }); + // store the crds asset id used it in the helm chart update + if (file === "CRDS.tar.gz") { + core.setOutput('crds_asset_id', response.data.id) + } + } + + - name: Publish release + uses: actions/github-script@v6 + with: + script: | + const {RELEASE_ID} = process.env + const {TAG_NAME} = process.env + github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: `${RELEASE_ID}`, + draft: false, + tag_name: `${TAG_NAME}`, + name: `${TAG_NAME}` + }); + + # TODO + # - name: Trigger chart update + # uses: peter-evans/repository-dispatch@26b39ed245ab8f31526069329e112ab2fb224588 + # with: + # token: ${{ secrets.HELM_CHART_REPO_ACCESS_TOKEN }} + # repository: "${{github.repository_owner}}/helm-charts" + # event-type: update-chart + # client-payload: '{"version": "${{ github.ref_name }}", "oldVersion": "${{ steps.get_last_release_tag.outputs.old_release_tag }}", "repository": "${{ github.repository }}"}' diff --git a/.github/workflows/reusable-container-image.yml b/.github/workflows/reusable-container-image.yml new file mode 100644 index 00000000..e2be1988 --- /dev/null +++ b/.github/workflows/reusable-container-image.yml @@ -0,0 +1,131 @@ +name: Reusable container image build +# useful to call from the real workflows + +on: + workflow_call: + inputs: + push-image: + type: boolean + required: true + generate-sbom: + type: boolean + required: false + default: true + outputs: + repository: + description: "Repository used to build the container image" + value: ${{ jobs.build.outputs.repository }} + tag: + description: "Tag used to build the container image" + value: ${{ jobs.build.outputs.tag }} + artifact: + description: "Uploaded artifact with the container tarball" + value: ${{ jobs.build.outputs.artifact }} + digest: + description: "Image digest" + value: ${{ jobs.build.outputs.digest }} + +jobs: + build: + name: Build container image + permissions: + packages: write + runs-on: ubuntu-latest + outputs: + repository: ${{ steps.setoutput.outputs.repository }} + tag: ${{ steps.setoutput.outputs.tag }} + artifact: ${{ steps.setoutput.outputs.artifact }} + digest: ${{ steps.setoutput.outputs.digest }} + steps: + - + name: Checkout code + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Install Golang + uses: actions/setup-go@v3 + with: + go-version: '1.19' + - + name: Install the bom command + uses: kubewarden/github-actions/kubernetes-bom-installer@v1 + - + name: Install Cosign + if: ${{ inputs.generate-sbom == true }} + uses: sigstore/cosign-installer@main + - + name: Retrieve tag name + if: ${{ startsWith(github.ref, 'refs/heads/') }} + run: | + echo TAG_NAME=latest >> $GITHUB_ENV + - + name: Retrieve tag name + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + echo TAG_NAME=$(echo $GITHUB_REF | sed -e "s|refs/tags/||") >> $GITHUB_ENV + - + name: Build and push container image + if: ${{ inputs.push-image }} + id: build-image + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64, linux/arm64 + push: true + tags: | + ghcr.io/${{github.repository_owner}}/audit-scanner:${{ env.TAG_NAME }} + - + # Only build amd64 because buildx does not allow multiple platforms when + # exporting the image to a tarball. As we use this only for end-to-end tests + # and they run on amd64 arch, let's skip the arm64 build for now. + name: Build linux/amd64 container image + if: ${{ inputs.push-image == false }} + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + outputs: type=docker,dest=/tmp/audit-scanner-image-${{ env.TAG_NAME }}.tar + tags: | + ghcr.io/${{github.repository_owner}}/audit-scanner:${{ env.TAG_NAME }} + - + name: Create SBOM file + if: ${{ inputs.generate-sbom == true }} + shell: bash + run: | + bom generate -n https://kubewarden.io/kubewarden.spdx -o audit-scanner.spdx . + - + name: Attach SBOM file in the container image + if: ${{ inputs.generate-sbom == true }} + shell: bash + run: | + set -e + cosign attach sbom --sbom audit-scanner.spdx "ghcr.io/${{github.repository_owner}}/audit-scanner@${{ steps.build-image.outputs.digest }}" + - + name: Upload container image to use in other jobs + if: ${{ inputs.push-image == false }} + uses: actions/upload-artifact@v3 + with: + name: audit-scanner-image-${{ env.TAG_NAME }} + path: /tmp/audit-scanner-image-${{ env.TAG_NAME }}.tar + - + id: setoutput + name: Set output parameters + run: | + echo "repository=ghcr.io/${{github.repository_owner}}/audit-scanner" >> $GITHUB_OUTPUT + echo "tag=${{ env.TAG_NAME }}" >> $GITHUB_OUTPUT + echo "artifact=audit-scanner-image-${{env.TAG_NAME}}" >> $GITHUB_OUTPUT + echo "digest=${{ steps.build-image.outputs.digest }}" >> $GITHUB_OUTPUT diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..8b98fc77 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,61 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + tests: true + timeout: 10m + +issues: + exclude-rules: + - linters: + - funlen + # Disable 'funlen' linter for test functions. + # It's common for table-driven tests to be more than 60 characters long + source: "^func Test" + +linters: + enable-all: true + disable: + - exhaustivestruct + - exhaustruct + - gci + - gochecknoglobals + - gochecknoinits + - gocognit + - godot + - goerr113 + - golint + - gofumpt + - gomnd + - maligned + - nlreturn + - paralleltest + - scopelint + - testpackage + - wsl + - lll # long lines + # https://github.com/golangci/golangci-lint/issues/541 + - interfacer + - interfacebloat + # deprecated: + - deadcode + - ifshort + - structcheck + - varcheck + - nosnakecase + # disabled because generics: + - rowserrcheck + - sqlclosecheck + - wastedassign + # TODO REMOVE THESE BEFORE RELEASE + - wrapcheck + - godox + - forbidigo + - ireturn + +linters-settings: + cyclop: + max-complexity: 13 + nestif: + min-complexity: 8 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..cfa1f46a --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @kubewarden/kubewarden-developers diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b73a8e49 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Build the audit-scanner binary +FROM golang:1.19 as builder + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY *.go ./ +COPY cmd/ cmd/ +COPY internal/ internal/ + +# Build +RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -o audit-scanner . + +# Use distroless as minimal base image to package the audit-scanner binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +WORKDIR / +COPY --from=builder /workspace/audit-scanner . +USER 65532:65532 + +ENTRYPOINT ["/audit-scanner"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile index 5415eda1..399c9c14 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,9 @@ vet: ## Run go vet against code. lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run -.PHONY: test -test: fmt vet ## Run unit tests. +.PHONY: unit-tests +unit-tests: fmt vet ## Run unit tests. go test ./internal/... -test.v -coverprofile cover.out -build: fmt vet lint ## Build manager binary. +build: fmt vet lint ## Build audit-scanner binary. go build -o bin/audit-scanner . diff --git a/cmd/root.go b/cmd/root.go index 9392847f..760ac70a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,12 +2,13 @@ package cmd import ( "fmt" + "os" + logconfig "github.com/kubewarden/audit-scanner/internal/log" "github.com/kubewarden/audit-scanner/internal/policies" "github.com/kubewarden/audit-scanner/internal/resources" "github.com/kubewarden/audit-scanner/internal/scanner" "github.com/spf13/cobra" - "os" ) // A Scanner verifies that existing resources don't violate any of the policies diff --git a/internal/log/level.go b/internal/log/level.go index d775bcfe..fc8e9e58 100644 --- a/internal/log/level.go +++ b/internal/log/level.go @@ -2,6 +2,7 @@ package log import ( "fmt" + "github.com/rs/zerolog" ) diff --git a/internal/policies/fetcher.go b/internal/policies/fetcher.go index 06165ad6..a986f2af 100644 --- a/internal/policies/fetcher.go +++ b/internal/policies/fetcher.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -46,13 +47,14 @@ func (f *Fetcher) GetPoliciesForAllNamespaces() ([]policiesv1.Policy, error) { func (f *Fetcher) GetPoliciesForANamespace(namespace string) ([]policiesv1.Policy, error) { namespacePolicies, err := f.findNamespacesForAllClusterAdmissionPolicies() if err != nil { - return nil, fmt.Errorf("can't fetch ClusterAdmissionPolicies: %v", err) + return nil, fmt.Errorf("can't fetch ClusterAdmissionPolicies: %w", err) } admissionPolicies, err := f.getAdmissionPolicies(namespace) if err != nil { - return nil, fmt.Errorf("can't fetch AdmissionPolicies: %v", err) + return nil, fmt.Errorf("can't fetch AdmissionPolicies: %w", err) } for _, policy := range admissionPolicies { + policy := policy namespacePolicies[namespace] = append(namespacePolicies[namespace], &policy) } @@ -65,7 +67,7 @@ func (f *Fetcher) initNamespacePoliciesMap() (map[string][]policiesv1.Policy, er namespaceList := &v1.NamespaceList{} err := f.client.List(context.Background(), namespaceList, &client.ListOptions{}) if err != nil { - return nil, fmt.Errorf("can't list namespaces: %v", err) + return nil, fmt.Errorf("can't list namespaces: %w", err) } for _, namespace := range namespaceList.Items { namespacePolicies[namespace.Name] = []policiesv1.Policy{} @@ -84,14 +86,14 @@ func (f *Fetcher) findNamespacesForAllClusterAdmissionPolicies() (map[string][]p policies := &policiesv1.ClusterAdmissionPolicyList{} err = f.client.List(context.Background(), policies, &client.ListOptions{}) if err != nil { - return nil, fmt.Errorf("can't list AdmissionPolicies: %v", err) + return nil, fmt.Errorf("can't list AdmissionPolicies: %w", err) } for _, policy := range policies.Items { policy := policy namespaces, err := f.findNamespacesForClusterAdmissionPolicy(policy) if err != nil { - return nil, fmt.Errorf("can't find namespaces for ClusterAdmissionPolicy %s: %v", policy.Name, err) + return nil, fmt.Errorf("can't find namespaces for ClusterAdmissionPolicy %s: %w", policy.Name, err) } for _, namespace := range namespaces { namespacePolicies[namespace.Name] = append(namespacePolicies[namespace.Name], &policy) diff --git a/internal/policies/fetcher_test.go b/internal/policies/fetcher_test.go index ac2c7549..4164d8e5 100644 --- a/internal/policies/fetcher_test.go +++ b/internal/policies/fetcher_test.go @@ -1,6 +1,8 @@ package policies import ( + "testing" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" @@ -10,7 +12,6 @@ import ( "k8s.io/client-go/kubernetes/scheme" k8sClient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "testing" ) func TestFindNamespacesForAllClusterAdmissionPolicies(t *testing.T) { diff --git a/internal/policies/filter_test.go b/internal/policies/filter_test.go index 8196061e..c9f09475 100644 --- a/internal/policies/filter_test.go +++ b/internal/policies/filter_test.go @@ -1,10 +1,11 @@ package policies import ( + "testing" + "github.com/google/go-cmp/cmp" policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - "testing" ) func TestFilterAuditablePolicies(t *testing.T) { @@ -85,5 +86,4 @@ func TestFilterAuditablePolicies(t *testing.T) { } }) } - } diff --git a/internal/resources/fetcher.go b/internal/resources/fetcher.go index a47a7ab1..45050352 100644 --- a/internal/resources/fetcher.go +++ b/internal/resources/fetcher.go @@ -2,6 +2,7 @@ package resources import ( "context" + policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -67,13 +68,12 @@ func (f *Fetcher) GetResourcesForPolicies(ctx context.Context, policies []polici func (f *Fetcher) getResourcesDynamically(ctx context.Context, group string, version string, resource string, namespace string) ( *unstructured.UnstructuredList, error) { - - resourceId := schema.GroupVersionResource{ + resourceID := schema.GroupVersionResource{ Group: group, Version: version, Resource: resource, } - list, err := f.dynamicClient.Resource(resourceId).Namespace(namespace). + list, err := f.dynamicClient.Resource(resourceID).Namespace(namespace). List(ctx, metav1.ListOptions{}) if err != nil { diff --git a/internal/resources/fetcher_test.go b/internal/resources/fetcher_test.go index 5ac05834..dd28a0c4 100644 --- a/internal/resources/fetcher_test.go +++ b/internal/resources/fetcher_test.go @@ -2,6 +2,8 @@ package resources import ( "context" + "testing" + "github.com/google/go-cmp/cmp" policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" @@ -12,7 +14,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/scheme" - "testing" ) const ( @@ -21,7 +22,7 @@ const ( ) // policies for testing -var p1 = policiesv1.AdmissionPolicy{ +var policy1 = policiesv1.AdmissionPolicy{ Spec: policiesv1.AdmissionPolicySpec{PolicySpec: policiesv1.PolicySpec{ Rules: []admissionregistrationv1.RuleWithOperations{{ Operations: nil, @@ -35,7 +36,7 @@ var p1 = policiesv1.AdmissionPolicy{ }}, } -var p2 = policiesv1.ClusterAdmissionPolicy{ +var policy2 = policiesv1.ClusterAdmissionPolicy{ Spec: policiesv1.ClusterAdmissionPolicySpec{PolicySpec: policiesv1.PolicySpec{ Rules: []admissionregistrationv1.RuleWithOperations{{ Operations: nil, @@ -49,7 +50,7 @@ var p2 = policiesv1.ClusterAdmissionPolicy{ }}, } -var p3 = policiesv1.AdmissionPolicy{ +var policy3 = policiesv1.AdmissionPolicy{ Spec: policiesv1.AdmissionPolicySpec{PolicySpec: policiesv1.PolicySpec{ Rules: []admissionregistrationv1.RuleWithOperations{{ Operations: nil, @@ -89,7 +90,7 @@ func TestGetResourcesForPolicies(t *testing.T) { customScheme.AddKnownTypes(schema.GroupVersion{Group: kubewardenPoliciesGroup, Version: kubewardenPoliciesVersion}, &policiesv1.ClusterAdmissionPolicy{}, &policiesv1.AdmissionPolicy{}, &policiesv1.ClusterAdmissionPolicyList{}, &policiesv1.AdmissionPolicyList{}) metav1.AddToGroupVersion(customScheme, schema.GroupVersion{Group: kubewardenPoliciesGroup, Version: kubewardenPoliciesVersion}) - dynamicClient := fake.NewSimpleDynamicClient(customScheme, &p1, &pod1, &pod2, &deployment1) + dynamicClient := fake.NewSimpleDynamicClient(customScheme, &policy1, &pod1, &pod2, &deployment1) unstructuredPod1 := map[string]interface{}{ "apiVersion": "v1", @@ -106,7 +107,7 @@ func TestGetResourcesForPolicies(t *testing.T) { } expectedP1 := []AuditableResources{{ - Policies: []policiesv1.Policy{&p1}, + Policies: []policiesv1.Policy{&policy1}, Resources: []unstructured.Unstructured{{Object: unstructuredPod1}}, }} @@ -117,7 +118,7 @@ func TestGetResourcesForPolicies(t *testing.T) { policies []policiesv1.Policy expect []AuditableResources }{ - {"policy1 (just pods)", []policiesv1.Policy{&p1}, expectedP1}, + {"policy1 (just pods)", []policiesv1.Policy{&policy1}, expectedP1}, {"no policies", []policiesv1.Policy{}, []AuditableResources{}}, } @@ -126,7 +127,7 @@ func TestGetResourcesForPolicies(t *testing.T) { t.Run(ttest.name, func(t *testing.T) { resources, err := fetcher.GetResourcesForPolicies(context.Background(), ttest.policies, "default") if err != nil { - t.Errorf("error shouldn't have happend " + err.Error()) + t.Errorf("error shouldn't have happened " + err.Error()) } if !cmp.Equal(resources, ttest.expect) { t.Errorf("expected %v, but got %v", ttest.expect, resources) @@ -136,7 +137,6 @@ func TestGetResourcesForPolicies(t *testing.T) { } func TestCreateGVRPolicyMap(t *testing.T) { - // all posible combination of GVR (Group, Version, Resource) for p1, p2 and p3 gvr1 := schema.GroupVersionResource{ Group: "", @@ -183,46 +183,46 @@ func TestCreateGVRPolicyMap(t *testing.T) { expectedP1andP2 := make(map[schema.GroupVersionResource][]policiesv1.Policy) - expectedP1andP2[gvr1] = []policiesv1.Policy{&p1, &p2} - expectedP1andP2[gvr2] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr3] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr4] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr5] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr6] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr7] = []policiesv1.Policy{&p2} - expectedP1andP2[gvr8] = []policiesv1.Policy{&p2} + expectedP1andP2[gvr1] = []policiesv1.Policy{&policy1, &policy2} + expectedP1andP2[gvr2] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr3] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr4] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr5] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr6] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr7] = []policiesv1.Policy{&policy2} + expectedP1andP2[gvr8] = []policiesv1.Policy{&policy2} expectedP1P2andP3 := make(map[schema.GroupVersionResource][]policiesv1.Policy) - expectedP1P2andP3[gvr1] = []policiesv1.Policy{&p1, &p2, &p3} - expectedP1P2andP3[gvr2] = []policiesv1.Policy{&p2, &p3} - expectedP1P2andP3[gvr3] = []policiesv1.Policy{&p2} - expectedP1P2andP3[gvr4] = []policiesv1.Policy{&p2} - expectedP1P2andP3[gvr5] = []policiesv1.Policy{&p2, &p3} - expectedP1P2andP3[gvr6] = []policiesv1.Policy{&p2, &p3} - expectedP1P2andP3[gvr7] = []policiesv1.Policy{&p2} - expectedP1P2andP3[gvr8] = []policiesv1.Policy{&p2} + expectedP1P2andP3[gvr1] = []policiesv1.Policy{&policy1, &policy2, &policy3} + expectedP1P2andP3[gvr2] = []policiesv1.Policy{&policy2, &policy3} + expectedP1P2andP3[gvr3] = []policiesv1.Policy{&policy2} + expectedP1P2andP3[gvr4] = []policiesv1.Policy{&policy2} + expectedP1P2andP3[gvr5] = []policiesv1.Policy{&policy2, &policy3} + expectedP1P2andP3[gvr6] = []policiesv1.Policy{&policy2, &policy3} + expectedP1P2andP3[gvr7] = []policiesv1.Policy{&policy2} + expectedP1P2andP3[gvr8] = []policiesv1.Policy{&policy2} expectedP1andP3 := make(map[schema.GroupVersionResource][]policiesv1.Policy) - expectedP1andP3[gvr1] = []policiesv1.Policy{&p1, &p3} - expectedP1andP3[gvr2] = []policiesv1.Policy{&p3} - expectedP1andP3[gvr5] = []policiesv1.Policy{&p3} - expectedP1andP3[gvr6] = []policiesv1.Policy{&p3} + expectedP1andP3[gvr1] = []policiesv1.Policy{&policy1, &policy3} + expectedP1andP3[gvr2] = []policiesv1.Policy{&policy3} + expectedP1andP3[gvr5] = []policiesv1.Policy{&policy3} + expectedP1andP3[gvr6] = []policiesv1.Policy{&policy3} expectedP1 := make(map[schema.GroupVersionResource][]policiesv1.Policy) - expectedP1[gvr1] = []policiesv1.Policy{&p1} + expectedP1[gvr1] = []policiesv1.Policy{&policy1} tests := []struct { name string policies []policiesv1.Policy expect map[schema.GroupVersionResource][]policiesv1.Policy }{ - {"policy1 (just pods) and policy2 (pods, deployments, v1 and alphav1)", []policiesv1.Policy{&p1, &p2}, expectedP1andP2}, - {"policy1 (just pods), policy2 (pods, deployments, v1 and alphav1) and policy3 (pods, deployments, v1)", []policiesv1.Policy{&p1, &p2, &p3}, expectedP1P2andP3}, - {"policy1 (just pods) and policy3 (pods, deployments, v1)", []policiesv1.Policy{&p1, &p3}, expectedP1andP3}, - {"policy1 (just pods)", []policiesv1.Policy{&p1}, expectedP1}, + {"policy1 (just pods) and policy2 (pods, deployments, v1 and alphav1)", []policiesv1.Policy{&policy1, &policy2}, expectedP1andP2}, + {"policy1 (just pods), policy2 (pods, deployments, v1 and alphav1) and policy3 (pods, deployments, v1)", []policiesv1.Policy{&policy1, &policy2, &policy3}, expectedP1P2andP3}, + {"policy1 (just pods) and policy3 (pods, deployments, v1)", []policiesv1.Policy{&policy1, &policy3}, expectedP1andP3}, + {"policy1 (just pods)", []policiesv1.Policy{&policy1}, expectedP1}, {"empty array", []policiesv1.Policy{}, make(map[schema.GroupVersionResource][]policiesv1.Policy)}, } @@ -235,5 +235,4 @@ func TestCreateGVRPolicyMap(t *testing.T) { } }) } - } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index a70e0b69..9ee12d26 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/kubewarden/audit-scanner/internal/resources" policiesv1 "github.com/kubewarden/kubewarden-controller/pkg/apis/policies/v1" "github.com/rs/zerolog/log" @@ -78,5 +79,5 @@ func (s *Scanner) ScanNamespace(namespace string) error { // ScanAllNamespaces scans resources for all namespaces func (s *Scanner) ScanAllNamespaces() error { - return errors.New("Scanning all namespaces is not implemented yet. Please pass the --namespace flag to scan a namespace") + return errors.New("scanning all namespaces is not implemented yet. Please pass the --namespace flag to scan a namespace") }