├── .github └── workflows │ ├── build.yml │ ├── publish-helm-chart.yaml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── .yamllint ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── Vagrantfile ├── cmd └── scanner-adapter │ └── main.go ├── docs └── images │ ├── adding_harbor_registry_in_aqua.png │ ├── aqua_cicd_scans_page.png │ ├── aqua_settings_save_cicd_scans.png │ ├── aqua_user_for_harbor.png │ ├── harbor_deployment_security.png │ ├── harbor_ui_add_scanner.png │ ├── harbor_ui_default_scanner.png │ ├── harbor_ui_scanners_config.png │ ├── harbor_user_for_aqua.png │ ├── hld.excalidraw │ └── hld.png ├── go.mod ├── go.sum ├── helm └── harbor-scanner-aqua │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── secret-registry.yaml │ ├── secret-tls.yaml │ ├── secret.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── pkg ├── app.go ├── app_test.go ├── aqua │ ├── command.go │ ├── command_mock.go │ ├── command_test.go │ ├── model.go │ ├── model_test.go │ └── test_fixtures │ │ └── aqua_report_photon_3.0.json ├── etc │ ├── config.go │ └── config_test.go ├── ext │ ├── ambassador.go │ ├── ambassador_mock.go │ └── clock.go ├── harbor │ ├── model.go │ └── model_test.go ├── http │ └── api │ │ ├── base_handler.go │ │ ├── base_handler_test.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── v1 │ │ ├── handler.go │ │ └── handler_test.go ├── job │ └── job.go ├── persistence │ ├── mock │ │ └── store.go │ ├── redis │ │ └── store.go │ └── store.go ├── redisx │ ├── pool.go │ └── pool_test.go ├── scanner │ ├── adapter.go │ ├── adapter_test.go │ ├── enqueuer.go │ ├── enqueuer_mock.go │ ├── enqueuer_test.go │ ├── transformer.go │ ├── transformer_mock.go │ ├── transformer_test.go │ ├── worker.go │ └── worker_test.go └── work │ ├── work.go │ └── work_test.go ├── test └── integration │ └── persistence │ └── redis │ └── store_test.go └── vagrant ├── harbor.yml ├── install-aqua.sh ├── install-docker.sh ├── install-go.sh └── install-harbor.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Setup Go 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: 1.18 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: yaml-lint 22 | uses: ibiqlik/action-yamllint@v3 23 | - name: Run unit tests 24 | run: make test 25 | - name: Run integration tests 26 | run: make test-integration 27 | - name: Release snapshot 28 | uses: goreleaser/goreleaser-action@v2 29 | with: 30 | version: v1.1.0 31 | args: release --snapshot --skip-publish --rm-dist 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-helm-chart.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is a manually triggered workflow to package and upload the Helm chart from the 3 | # main branch to Aqua Security repository at https://github.com/aquasecurity/helm-charts. 4 | name: Publish Helm chart 5 | 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | ref: 10 | description: Git revision to be published 11 | required: true 12 | 13 | env: 14 | HELM_REP: helm-charts 15 | GH_OWNER: aquasecurity 16 | CHART_DIR: helm/harbor-scanner-aqua 17 | jobs: 18 | release: 19 | runs-on: ubuntu-20.04 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | with: 24 | ref: "${{ github.event.inputs.ref }}" 25 | fetch-depth: 1 26 | - name: Install Helm 27 | uses: azure/setup-helm@v1 28 | with: 29 | version: v3.5.0 30 | - name: Install chart-releaser 31 | run: | 32 | wget https://github.com/helm/chart-releaser/releases/download/v1.3.0/chart-releaser_1.3.0_linux_amd64.tar.gz 33 | tar xzvf chart-releaser_1.3.0_linux_amd64.tar.gz cr 34 | - name: Package helm chart 35 | run: | 36 | ./cr package ${{ env.CHART_DIR }} 37 | - name: Upload helm chart 38 | # Failed with upload the same version: https://github.com/helm/chart-releaser/issues/101 39 | continue-on-error: true 40 | run: | 41 | ./cr upload --owner ${{ env.GH_OWNER }} \ 42 | --git-repo ${{ env.HELM_REP }} \ 43 | --token ${{ secrets.ORG_REPO_TOKEN }} \ 44 | --package-path .cr-release-packages 45 | - name: Index helm chart 46 | run: | 47 | ./cr index --owner ${{ env.GH_OWNER }} \ 48 | --git-repo ${{ env.HELM_REP }} \ 49 | --charts-repo https://${{ env.GH_OWNER }}.github.io/${{ env.HELM_REP }}/ \ 50 | --index-path index.yaml 51 | - name: Push index file 52 | uses: dmnemec/copy_file_to_another_repo_action@v1.1.1 53 | env: 54 | API_TOKEN_GITHUB: ${{ secrets.ORG_REPO_TOKEN }} 55 | with: 56 | source_file: 'index.yaml' 57 | destination_repo: '${{ env.GH_OWNER }}/${{ env.HELM_REP }}' 58 | destination_folder: '.' 59 | destination_branch: 'gh-pages' 60 | user_email: aqua-bot@users.noreply.github.com 61 | user_name: 'aqua-bot' 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Setup Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.18 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Run unit tests 21 | run: make test 22 | - name: Run integration tests 23 | run: make test-integration 24 | - name: Login to docker.io registry 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USER }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | - name: Login to ECR 30 | uses: docker/login-action@v1 31 | with: 32 | registry: public.ecr.aws 33 | username: ${{ secrets.ECR_ACCESS_KEY_ID }} 34 | password: ${{ secrets.ECR_SECRET_ACCESS_KEY }} 35 | - name: Release 36 | uses: goreleaser/goreleaser-action@v2 37 | with: 38 | version: v1.1.0 39 | args: release --rm-dist 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | 4 | coverage.txt 5 | scanner-adapter 6 | 7 | .vagrant/ 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | - GO111MODULE=on 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - id: scanner-adapter 9 | main: ./cmd/scanner-adapter/main.go 10 | binary: scanner-adapter 11 | env: 12 | - CGO_ENABLED=0 13 | archives: 14 | - replacements: 15 | darwin: Darwin 16 | linux: Linux 17 | 386: i386 18 | amd64: x86_64 19 | checksum: 20 | name_template: "checksums.txt" 21 | snapshot: 22 | name_template: "{{ .FullCommit }}" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs' 28 | - '^test' 29 | - '^release' 30 | dockers: 31 | - image_templates: 32 | - "docker.io/aquasec/harbor-scanner-aqua:{{ .Version }}" 33 | - "public.ecr.aws/aquasecurity/harbor-scanner-aqua:{{ .Version }}" 34 | ids: 35 | - scanner-adapter 36 | build_flag_templates: 37 | - "--label=org.label-schema.schema-version=1.0" 38 | - "--label=org.label-schema.name={{ .ProjectName }}" 39 | - "--label=org.label-schema.description=Harbor Scanner Adapter for Aqua Enterprise Scanner" 40 | - "--label=org.label-schema.vendor=Aqua Security" 41 | - "--label=org.label-schema.version={{ .Version }}" 42 | - "--label=org.label-schema.build-date={{ .Date }}" 43 | - "--label=org.label-schema.vcs=https://github.com/aquasecurity/harbor-scanner-aqua" 44 | - "--label=org.label-schema.vcs-ref={{ .FullCommit }}" 45 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: disable 6 | truthy: disable 7 | 8 | ignore: | 9 | /helm/ 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Table of Contents 4 | 5 | * [Set up Local Development Environment](#set-up-local-development-environment) 6 | * [Setup Development Environment with Vagrant](#setup-development-environment-with-vagrant) 7 | * [Build Binaries](#build-binaries) 8 | * [Run Tests](#run-tests) 9 | * [Run Unit Tests](#run-unit-tests) 10 | * [Run Integration Tests](#run-integration-tests) 11 | 12 | ## Set up Local Development Environment 13 | 14 | 1. The project requires [Go 1.15][go-download] or later. We also assume that you're familiar with Go's 15 | [GOPATH workspace][go-code] convention, and have the appropriate environment variables set. 16 | 3. Install Docker, Docker Compose, and Make. 17 | 4. Get the source code. 18 | ``` 19 | git clone https://github.com/aquasecurity/harbor-scanner-aqua.git 20 | cd harbor-scanner-aqua 21 | ``` 22 | 23 | ## Setup Development Environment with Vagrant 24 | 25 | 1. Get the source code. 26 | ``` 27 | git clone https://github.com/aquasecurity/harbor-scanner-aqua.git 28 | cd harbor-scanner-aqua 29 | ``` 30 | 2. Create and configure a guest development machine, which is based on Ubuntu 20.4.3 LTS and has Go, Docker, Docker Compose, 31 | Make, and Harbor v2.4.0 preinstalled. Harbor is installed in the `/opt/harbor` directory. 32 | ``` 33 | export AQUA_REGISTRY_USERNAME= 34 | export AQUA_REGISTRY_PASSWORD= 35 | export AQUA_VERSION="6.5" 36 | export HARBOR_VERSION="v2.4.0" 37 | 38 | vagrant up 39 | ``` 40 | 41 | The Harbor UI is accessible at http://localhost:8181 (admin/Harbor12345). The Aqua Management Console is accessible at 42 | http://localhost:9181 (administrator/@Aqua12345). Note that you'll be prompted for a valid licence key upon successful 43 | login to the Aqua Management Console. 44 | 45 | To SSH into a running Vagrant machine. 46 | ``` 47 | vagrant ssh 48 | ``` 49 | 50 | The `/vagrant` directory in the development machine is shared between project (host) and guest. 51 | 52 | ``` 53 | vagrant@ubuntu-focal:~$ cd /vagrant 54 | ``` 55 | 56 | ## Build Binaries 57 | 58 | Run `make` to build the binary in `./scanner-adapter`. 59 | 60 | ``` 61 | make 62 | ``` 63 | 64 | To build into a Docker container `aquasec/harbor-scanner-aqua:dev`. 65 | 66 | ``` 67 | make docker-build 68 | ``` 69 | 70 | ## Run Tests 71 | 72 | ### Run Unit Tests 73 | 74 | ``` 75 | make test 76 | ``` 77 | 78 | ### Run Integration Tests 79 | 80 | ``` 81 | make test-integration 82 | ``` 83 | 84 | [go-download]: https://golang.org/dl/ 85 | [go-code]: https://golang.org/doc/code.html 86 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14 2 | 3 | RUN echo "@edge http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories 4 | 5 | RUN apk update \ 6 | && apk upgrade musl \ 7 | && apk add ca-certificates dpkg@edge rpm@edge expat@edge libbz2@edge libarchive@edge db@edge 8 | 9 | RUN adduser -u 1000 -D -g '' scanner scanner 10 | 11 | COPY scanner-adapter /usr/local/bin/scanner-adapter 12 | 13 | USER scanner 14 | 15 | ENTRYPOINT ["scanner-adapter"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(shell find . -name '*.go') 2 | BINARY := scanner-adapter 3 | IMAGE_TAG := dev 4 | IMAGE := aquasec/harbor-scanner-aqua:$(IMAGE_TAG) 5 | 6 | build: $(BINARY) 7 | 8 | test: build 9 | GO111MODULE=on go test -v -short -race -coverprofile=coverage.txt -covermode=atomic ./... 10 | 11 | test-integration: build 12 | GO111MODULE=on go test -count=1 -v -tags=integration ./test/integration/... 13 | 14 | $(BINARY): $(SOURCES) 15 | GOOS=linux GO111MODULE=on CGO_ENABLED=0 go build -o $(BINARY) cmd/scanner-adapter/main.go 16 | 17 | docker-build: build 18 | docker build --no-cache -t $(IMAGE) . 19 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | harbor-scanner-aqua 2 | Copyright 2019-2021 Aqua Security Software Ltd. 3 | 4 | This product includes software developed by Aqua Security (https://aquasec.com). 5 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "ubuntu/focal64" 6 | 7 | config.vm.provider "virtualbox" do |vb| 8 | vb.gui = false 9 | vb.memory = "2096" 10 | end 11 | 12 | config.vm.provision "shell", env: { 13 | "AQUA_REGISTRY_USERNAME" => ENV['AQUA_REGISTRY_USERNAME'], 14 | "AQUA_REGISTRY_PASSWORD" => ENV['AQUA_REGISTRY_PASSWORD'] 15 | }, inline: <<-SHELL 16 | if [ -z "$AQUA_REGISTRY_USERNAME" ]; then echo "AQUA_REGISTRY_USERNAME env is unset" && exit 1; fi 17 | if [ -z "$AQUA_REGISTRY_PASSWORD" ]; then echo "AQUA_REGISTRY_PASSWORD env is unset" && exit 1; fi 18 | SHELL 19 | 20 | config.vm.provision "install-go", type: "shell", path: "vagrant/install-go.sh" 21 | config.vm.provision "install-docker-ce", type: "shell", path: "vagrant/install-docker.sh" 22 | config.vm.provision "install-harbor", type: "shell", path: "vagrant/install-harbor.sh", env: { 23 | "HARBOR_VERSION" => ENV["HARBOR_VERSION"] || "v2.4.0" 24 | } 25 | config.vm.provision "install-aqua", type: "shell", path: "vagrant/install-aqua.sh", env: { 26 | "AQUA_REGISTRY_USERNAME" => ENV['AQUA_REGISTRY_USERNAME'], 27 | "AQUA_REGISTRY_PASSWORD" => ENV['AQUA_REGISTRY_PASSWORD'], 28 | "AQUA_VERSION" => ENV['AQUA_VERSION'] || "6.5", 29 | "HARBOR_VERSION" => ENV['HARBOR_VERSION'] || "v2.4.0" 30 | } 31 | 32 | # Access Harbor Portal at http://localhost:8181 (admin/@Harbor12345) 33 | config.vm.network :forwarded_port, guest: 8080, host: 8181 34 | 35 | # Access Aqua Management Console at http://localhost:9181 (administrator/@Aqua12345) 36 | config.vm.network :forwarded_port, guest: 9080, host: 9181 37 | end 38 | -------------------------------------------------------------------------------- /cmd/scanner-adapter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg" 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var ( 12 | // Default wise GoReleaser sets three ldflags: 13 | version = "dev" 14 | commit = "none" 15 | date = "unknown" 16 | ) 17 | 18 | func main() { 19 | log.SetOutput(os.Stdout) 20 | log.SetLevel(etc.GetLogLevel()) 21 | log.SetReportCaller(false) 22 | log.SetFormatter(&log.JSONFormatter{}) 23 | 24 | info := etc.BuildInfo{ 25 | Version: version, 26 | Commit: commit, 27 | Date: date, 28 | } 29 | 30 | if err := pkg.Run(info); err != nil { 31 | log.Fatalf("Error: %v", err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/images/adding_harbor_registry_in_aqua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/adding_harbor_registry_in_aqua.png -------------------------------------------------------------------------------- /docs/images/aqua_cicd_scans_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/aqua_cicd_scans_page.png -------------------------------------------------------------------------------- /docs/images/aqua_settings_save_cicd_scans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/aqua_settings_save_cicd_scans.png -------------------------------------------------------------------------------- /docs/images/aqua_user_for_harbor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/aqua_user_for_harbor.png -------------------------------------------------------------------------------- /docs/images/harbor_deployment_security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/harbor_deployment_security.png -------------------------------------------------------------------------------- /docs/images/harbor_ui_add_scanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/harbor_ui_add_scanner.png -------------------------------------------------------------------------------- /docs/images/harbor_ui_default_scanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/harbor_ui_default_scanner.png -------------------------------------------------------------------------------- /docs/images/harbor_ui_scanners_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/harbor_ui_scanners_config.png -------------------------------------------------------------------------------- /docs/images/harbor_user_for_aqua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/harbor_user_for_aqua.png -------------------------------------------------------------------------------- /docs/images/hld.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 336, 9 | "versionNonce": 1540727700, 10 | "isDeleted": false, 11 | "id": "gnpGDI40PBVCKn81wg50B", 12 | "fillStyle": "solid", 13 | "strokeWidth": 1, 14 | "strokeStyle": "solid", 15 | "roughness": 1, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": 623.86865234375, 19 | "y": 331.95794677734375, 20 | "strokeColor": "#000000", 21 | "backgroundColor": "#ced4da", 22 | "width": 192.1923828125, 23 | "height": 209.56594848632818, 24 | "seed": 13792239, 25 | "groupIds": [], 26 | "strokeSharpness": "sharp", 27 | "boundElementIds": [ 28 | "BHn3qoNTStrJIZP007K6o", 29 | "P63ctnMJONk4bE_XxSen8", 30 | "GjHPXFn-AhaXVbtkq_Ntu", 31 | "tlKalHaZFu40GxW9wvNx4" 32 | ] 33 | }, 34 | { 35 | "type": "text", 36 | "version": 316, 37 | "versionNonce": 1034060308, 38 | "isDeleted": false, 39 | "id": "tLn1GO6ZbUpYzilcLKl9n", 40 | "fillStyle": "hachure", 41 | "strokeWidth": 1, 42 | "strokeStyle": "solid", 43 | "roughness": 1, 44 | "opacity": 100, 45 | "angle": 0, 46 | "x": 643.8318481445312, 47 | "y": 350.8264465332031, 48 | "strokeColor": "#000000", 49 | "backgroundColor": "transparent", 50 | "width": 152, 51 | "height": 75, 52 | "seed": 1084924047, 53 | "groupIds": [], 54 | "strokeSharpness": "sharp", 55 | "boundElementIds": [], 56 | "fontSize": 20, 57 | "fontFamily": 1, 58 | "text": "Harbor Cloud\nNative Registry\nv2.3.2", 59 | "baseline": 68, 60 | "textAlign": "center", 61 | "verticalAlign": "top" 62 | }, 63 | { 64 | "type": "rectangle", 65 | "version": 205, 66 | "versionNonce": 1338205327, 67 | "isDeleted": false, 68 | "id": "K3AuBrdqaJNkDQA7lGSBR", 69 | "fillStyle": "solid", 70 | "strokeWidth": 1, 71 | "strokeStyle": "solid", 72 | "roughness": 1, 73 | "opacity": 100, 74 | "angle": 0, 75 | "x": 1031.0205078125, 76 | "y": 330.83526611328125, 77 | "strokeColor": "#000000", 78 | "backgroundColor": "#fab005", 79 | "width": 195.658447265625, 80 | "height": 137.7322998046875, 81 | "seed": 1941182895, 82 | "groupIds": [], 83 | "strokeSharpness": "sharp", 84 | "boundElementIds": [ 85 | "BHn3qoNTStrJIZP007K6o", 86 | "P63ctnMJONk4bE_XxSen8" 87 | ] 88 | }, 89 | { 90 | "type": "text", 91 | "version": 146, 92 | "versionNonce": 201207777, 93 | "isDeleted": false, 94 | "id": "2z7Cqspc14pBkRPFLOETI", 95 | "fillStyle": "hachure", 96 | "strokeWidth": 1, 97 | "strokeStyle": "solid", 98 | "roughness": 1, 99 | "opacity": 100, 100 | "angle": 0, 101 | "x": 1055.7025146484375, 102 | "y": 342.4569396972656, 103 | "strokeColor": "#000000", 104 | "backgroundColor": "transparent", 105 | "width": 147, 106 | "height": 50, 107 | "seed": 1185373423, 108 | "groupIds": [], 109 | "strokeSharpness": "sharp", 110 | "boundElementIds": [], 111 | "fontSize": 20, 112 | "fontFamily": 1, 113 | "text": "Harbor Scanner\nAqua v0.11.2", 114 | "baseline": 43, 115 | "textAlign": "center", 116 | "verticalAlign": "top" 117 | }, 118 | { 119 | "type": "rectangle", 120 | "version": 382, 121 | "versionNonce": 1890421743, 122 | "isDeleted": false, 123 | "id": "tqML5M3EgbT-MjahBBxRj", 124 | "fillStyle": "solid", 125 | "strokeWidth": 1, 126 | "strokeStyle": "solid", 127 | "roughness": 1, 128 | "opacity": 100, 129 | "angle": 0, 130 | "x": 1050.97119140625, 131 | "y": 409.15240478515625, 132 | "strokeColor": "#000000", 133 | "backgroundColor": "#15aabf", 134 | "width": 144.76147460937497, 135 | "height": 34.88848876953124, 136 | "seed": 1599033903, 137 | "groupIds": [], 138 | "strokeSharpness": "sharp", 139 | "boundElementIds": [ 140 | "p2Y6Kqtw5NCNg0KcxW_HK", 141 | "yvm5MoQnTwmM23qoVmTRI", 142 | "glrlI13oTRddUQrUkMc9y" 143 | ] 144 | }, 145 | { 146 | "type": "text", 147 | "version": 561, 148 | "versionNonce": 246156833, 149 | "isDeleted": false, 150 | "id": "fke0SOAxZNS08rduBcRGb", 151 | "fillStyle": "solid", 152 | "strokeWidth": 1, 153 | "strokeStyle": "solid", 154 | "roughness": 1, 155 | "opacity": 100, 156 | "angle": 0, 157 | "x": 1058.0427856445312, 158 | "y": 413.5665283203125, 159 | "strokeColor": "#000000", 160 | "backgroundColor": "#15aabf", 161 | "width": 135, 162 | "height": 25, 163 | "seed": 501377313, 164 | "groupIds": [], 165 | "strokeSharpness": "sharp", 166 | "boundElementIds": [], 167 | "fontSize": 20, 168 | "fontFamily": 1, 169 | "text": "scannercli 6.2", 170 | "baseline": 18, 171 | "textAlign": "center", 172 | "verticalAlign": "top" 173 | }, 174 | { 175 | "type": "rectangle", 176 | "version": 144, 177 | "versionNonce": 1276169423, 178 | "isDeleted": false, 179 | "id": "COOiKDe_esnyxvePnkgEv", 180 | "fillStyle": "solid", 181 | "strokeWidth": 1, 182 | "strokeStyle": "solid", 183 | "roughness": 1, 184 | "opacity": 100, 185 | "angle": 0, 186 | "x": 1399.249267578125, 187 | "y": 330.5872802734375, 188 | "strokeColor": "#000000", 189 | "backgroundColor": "#15aabf", 190 | "width": 204.7283935546875, 191 | "height": 209.17807006835938, 192 | "seed": 1403607297, 193 | "groupIds": [], 194 | "strokeSharpness": "sharp", 195 | "boundElementIds": [ 196 | "p2Y6Kqtw5NCNg0KcxW_HK", 197 | "glrlI13oTRddUQrUkMc9y" 198 | ] 199 | }, 200 | { 201 | "type": "text", 202 | "version": 174, 203 | "versionNonce": 1405774753, 204 | "isDeleted": false, 205 | "id": "oMyKx0jUSXb9QdrktCW4g", 206 | "fillStyle": "hachure", 207 | "strokeWidth": 1, 208 | "strokeStyle": "solid", 209 | "roughness": 1, 210 | "opacity": 100, 211 | "angle": 0, 212 | "x": 1420.0655517578125, 213 | "y": 351.7283935546875, 214 | "strokeColor": "#000000", 215 | "backgroundColor": "transparent", 216 | "width": 157, 217 | "height": 50, 218 | "seed": 556932559, 219 | "groupIds": [], 220 | "strokeSharpness": "sharp", 221 | "boundElementIds": [], 222 | "fontSize": 20, 223 | "fontFamily": 1, 224 | "text": "Aqua Enterprise\n6.2", 225 | "baseline": 43, 226 | "textAlign": "center", 227 | "verticalAlign": "top" 228 | }, 229 | { 230 | "type": "rectangle", 231 | "version": 283, 232 | "versionNonce": 804500897, 233 | "isDeleted": false, 234 | "id": "lIx9uQj8xYb9WRZUGM_DJ", 235 | "fillStyle": "hachure", 236 | "strokeWidth": 1, 237 | "strokeStyle": "solid", 238 | "roughness": 1, 239 | "opacity": 100, 240 | "angle": 0, 241 | "x": 1420.961181640625, 242 | "y": 468.298828125, 243 | "strokeColor": "#000000", 244 | "backgroundColor": "transparent", 245 | "width": 146.006591796875, 246 | "height": 53.03286743164063, 247 | "seed": 1378174145, 248 | "groupIds": [], 249 | "strokeSharpness": "sharp", 250 | "boundElementIds": [ 251 | "GjHPXFn-AhaXVbtkq_Ntu" 252 | ] 253 | }, 254 | { 255 | "type": "text", 256 | "version": 248, 257 | "versionNonce": 1424589153, 258 | "isDeleted": false, 259 | "id": "ShQ99PqL4ubnX4jXfGR0K", 260 | "fillStyle": "hachure", 261 | "strokeWidth": 1, 262 | "strokeStyle": "solid", 263 | "roughness": 1, 264 | "opacity": 100, 265 | "angle": 0, 266 | "x": 1432.0433349609375, 267 | "y": 479.5457458496094, 268 | "strokeColor": "#000000", 269 | "backgroundColor": "transparent", 270 | "width": 121, 271 | "height": 25, 272 | "seed": 1617458415, 273 | "groupIds": [], 274 | "strokeSharpness": "sharp", 275 | "boundElementIds": [], 276 | "fontSize": 20, 277 | "fontFamily": 1, 278 | "text": "Integrations", 279 | "baseline": 18, 280 | "textAlign": "center", 281 | "verticalAlign": "top" 282 | }, 283 | { 284 | "type": "ellipse", 285 | "version": 497, 286 | "versionNonce": 1162697004, 287 | "isDeleted": false, 288 | "id": "zHv-T39tTcy7KpDFf1_0N", 289 | "fillStyle": "hachure", 290 | "strokeWidth": 4, 291 | "strokeStyle": "solid", 292 | "roughness": 1, 293 | "opacity": 100, 294 | "angle": 0, 295 | "x": 692.9142716261149, 296 | "y": 628.8840942382812, 297 | "strokeColor": "#000000", 298 | "backgroundColor": "transparent", 299 | "width": 27.224450934060215, 300 | "height": 27.224450934060215, 301 | "seed": 1982035023, 302 | "groupIds": [ 303 | "3sbzyNoEs64Jk93mqPdFn" 304 | ], 305 | "strokeSharpness": "sharp", 306 | "boundElementIds": [ 307 | "tlKalHaZFu40GxW9wvNx4" 308 | ] 309 | }, 310 | { 311 | "type": "line", 312 | "version": 474, 313 | "versionNonce": 589386668, 314 | "isDeleted": false, 315 | "id": "6ZdOxuBTcF9t-iuRXDsjh", 316 | "fillStyle": "hachure", 317 | "strokeWidth": 4, 318 | "strokeStyle": "solid", 319 | "roughness": 1, 320 | "opacity": 100, 321 | "angle": 0, 322 | "x": 708.0872388865116, 323 | "y": 656.8522859097916, 324 | "strokeColor": "#000000", 325 | "backgroundColor": "transparent", 326 | "width": 0, 327 | "height": 36.70399778166478, 328 | "seed": 1965631329, 329 | "groupIds": [ 330 | "3sbzyNoEs64Jk93mqPdFn" 331 | ], 332 | "strokeSharpness": "round", 333 | "boundElementIds": [], 334 | "startBinding": null, 335 | "endBinding": null, 336 | "lastCommittedPoint": null, 337 | "startArrowhead": null, 338 | "endArrowhead": null, 339 | "points": [ 340 | [ 341 | 0, 342 | 0 343 | ], 344 | [ 345 | 0, 346 | 36.70399778166478 347 | ] 348 | ] 349 | }, 350 | { 351 | "type": "line", 352 | "version": 445, 353 | "versionNonce": 420482324, 354 | "isDeleted": false, 355 | "id": "gcYH-Rl9rz8hrGzAZUFoy", 356 | "fillStyle": "hachure", 357 | "strokeWidth": 4, 358 | "strokeStyle": "solid", 359 | "roughness": 1, 360 | "opacity": 100, 361 | "angle": 0, 362 | "x": 686.1465454101562, 363 | "y": 668.2272360036895, 364 | "strokeColor": "#000000", 365 | "backgroundColor": "transparent", 366 | "width": 43.117115182708496, 367 | "height": 0, 368 | "seed": 54586401, 369 | "groupIds": [ 370 | "3sbzyNoEs64Jk93mqPdFn" 371 | ], 372 | "strokeSharpness": "round", 373 | "boundElementIds": [], 374 | "startBinding": null, 375 | "endBinding": null, 376 | "lastCommittedPoint": null, 377 | "startArrowhead": null, 378 | "endArrowhead": null, 379 | "points": [ 380 | [ 381 | 0, 382 | 0 383 | ], 384 | [ 385 | 43.117115182708496, 386 | 0 387 | ] 388 | ] 389 | }, 390 | { 391 | "type": "line", 392 | "version": 529, 393 | "versionNonce": 1234304556, 394 | "isDeleted": false, 395 | "id": "lBCTbOoTVxQAmzAsRKroB", 396 | "fillStyle": "hachure", 397 | "strokeWidth": 4, 398 | "strokeStyle": "solid", 399 | "roughness": 1, 400 | "opacity": 100, 401 | "angle": 0, 402 | "x": 708.0882908980822, 403 | "y": 692.9950227472855, 404 | "strokeColor": "#000000", 405 | "backgroundColor": "transparent", 406 | "width": 20.066637069610522, 407 | "height": 19.95764595803247, 408 | "seed": 747414145, 409 | "groupIds": [ 410 | "3sbzyNoEs64Jk93mqPdFn" 411 | ], 412 | "strokeSharpness": "round", 413 | "boundElementIds": [], 414 | "startBinding": null, 415 | "endBinding": null, 416 | "lastCommittedPoint": null, 417 | "startArrowhead": null, 418 | "endArrowhead": null, 419 | "points": [ 420 | [ 421 | 0, 422 | 0 423 | ], 424 | [ 425 | -20.066637069610522, 426 | 19.95764595803247 427 | ] 428 | ] 429 | }, 430 | { 431 | "type": "line", 432 | "version": 460, 433 | "versionNonce": 1328892564, 434 | "isDeleted": false, 435 | "id": "sCAXoClc2thMHHbrxA4FD", 436 | "fillStyle": "hachure", 437 | "strokeWidth": 4, 438 | "strokeStyle": "solid", 439 | "roughness": 1, 440 | "opacity": 100, 441 | "angle": 0, 442 | "x": 708.0850693082702, 443 | "y": 694.7555483099422, 444 | "strokeColor": "#000000", 445 | "backgroundColor": "transparent", 446 | "width": 17.214139603724686, 447 | "height": 17.462940213278404, 448 | "seed": 948282031, 449 | "groupIds": [ 450 | "3sbzyNoEs64Jk93mqPdFn" 451 | ], 452 | "strokeSharpness": "round", 453 | "boundElementIds": [], 454 | "startBinding": null, 455 | "endBinding": null, 456 | "lastCommittedPoint": null, 457 | "startArrowhead": null, 458 | "endArrowhead": null, 459 | "points": [ 460 | [ 461 | 0, 462 | 0 463 | ], 464 | [ 465 | 17.214139603724686, 466 | 17.462940213278404 467 | ] 468 | ] 469 | }, 470 | { 471 | "type": "rectangle", 472 | "version": 429, 473 | "versionNonce": 1636391084, 474 | "isDeleted": false, 475 | "id": "uAeAM9RFVA0FBSJnm6LIg", 476 | "fillStyle": "solid", 477 | "strokeWidth": 1, 478 | "strokeStyle": "solid", 479 | "roughness": 1, 480 | "opacity": 100, 481 | "angle": 0, 482 | "x": 634.7490234375, 483 | "y": 467.4551544189453, 484 | "strokeColor": "#000000", 485 | "backgroundColor": "#fab005", 486 | "width": 146.006591796875, 487 | "height": 63.52328491210938, 488 | "seed": 650690113, 489 | "groupIds": [], 490 | "strokeSharpness": "sharp", 491 | "boundElementIds": [] 492 | }, 493 | { 494 | "type": "text", 495 | "version": 522, 496 | "versionNonce": 1490475028, 497 | "isDeleted": false, 498 | "id": "tgwJD4TQRHSREjmlBkvPP", 499 | "fillStyle": "hachure", 500 | "strokeWidth": 1, 501 | "strokeStyle": "solid", 502 | "roughness": 1, 503 | "opacity": 100, 504 | "angle": 0, 505 | "x": 638.5521850585938, 506 | "y": 475.9716033935547, 507 | "strokeColor": "#000000", 508 | "backgroundColor": "transparent", 509 | "width": 132, 510 | "height": 50, 511 | "seed": 366608975, 512 | "groupIds": [], 513 | "strokeSharpness": "sharp", 514 | "boundElementIds": [], 515 | "fontSize": 20, 516 | "fontFamily": 1, 517 | "text": "Pluggable\nScanners API", 518 | "baseline": 43, 519 | "textAlign": "center", 520 | "verticalAlign": "top" 521 | }, 522 | { 523 | "type": "arrow", 524 | "version": 260, 525 | "versionNonce": 326928788, 526 | "isDeleted": false, 527 | "id": "BHn3qoNTStrJIZP007K6o", 528 | "fillStyle": "hachure", 529 | "strokeWidth": 1, 530 | "strokeStyle": "solid", 531 | "roughness": 1, 532 | "opacity": 100, 533 | "angle": 0, 534 | "x": 820.67822265625, 535 | "y": 371.8158747148455, 536 | "strokeColor": "#000000", 537 | "backgroundColor": "transparent", 538 | "width": 208.4151611328125, 539 | "height": 0.6606409190495128, 540 | "seed": 959378895, 541 | "groupIds": [], 542 | "strokeSharpness": "round", 543 | "boundElementIds": [], 544 | "startBinding": { 545 | "elementId": "gnpGDI40PBVCKn81wg50B", 546 | "focus": -0.615035662407096, 547 | "gap": 4.6171875 548 | }, 549 | "endBinding": { 550 | "elementId": "K3AuBrdqaJNkDQA7lGSBR", 551 | "focus": 0.4172297886626035, 552 | "gap": 1.9271240234375 553 | }, 554 | "lastCommittedPoint": null, 555 | "startArrowhead": null, 556 | "endArrowhead": "arrow", 557 | "points": [ 558 | [ 559 | 0, 560 | 0 561 | ], 562 | [ 563 | 208.4151611328125, 564 | -0.6606409190495128 565 | ] 566 | ] 567 | }, 568 | { 569 | "type": "arrow", 570 | "version": 288, 571 | "versionNonce": 1504351532, 572 | "isDeleted": false, 573 | "id": "P63ctnMJONk4bE_XxSen8", 574 | "fillStyle": "hachure", 575 | "strokeWidth": 1, 576 | "strokeStyle": "solid", 577 | "roughness": 1, 578 | "opacity": 100, 579 | "angle": 0, 580 | "x": 1029.367431640625, 581 | "y": 404.5181519631251, 582 | "strokeColor": "#000000", 583 | "backgroundColor": "transparent", 584 | "width": 208.7694091796874, 585 | "height": 1.7273846049238273, 586 | "seed": 1427982849, 587 | "groupIds": [], 588 | "strokeSharpness": "round", 589 | "boundElementIds": [], 590 | "startBinding": { 591 | "elementId": "K3AuBrdqaJNkDQA7lGSBR", 592 | "focus": -0.0798842972430826, 593 | "gap": 1.653076171875 594 | }, 595 | "endBinding": { 596 | "elementId": "gnpGDI40PBVCKn81wg50B", 597 | "focus": -0.32945099282127455, 598 | "gap": 4.536987304687614 599 | }, 600 | "lastCommittedPoint": null, 601 | "startArrowhead": null, 602 | "endArrowhead": "arrow", 603 | "points": [ 604 | [ 605 | 0, 606 | 0 607 | ], 608 | [ 609 | -208.7694091796874, 610 | -1.7273846049238273 611 | ] 612 | ] 613 | }, 614 | { 615 | "type": "arrow", 616 | "version": 487, 617 | "versionNonce": 1176611521, 618 | "isDeleted": false, 619 | "id": "p2Y6Kqtw5NCNg0KcxW_HK", 620 | "fillStyle": "hachure", 621 | "strokeWidth": 1, 622 | "strokeStyle": "solid", 623 | "roughness": 1, 624 | "opacity": 100, 625 | "angle": 0, 626 | "x": 1199.5041563714644, 627 | "y": 417.4309857077538, 628 | "strokeColor": "#000000", 629 | "backgroundColor": "transparent", 630 | "width": 191.87503608415636, 631 | "height": 0.5942160710637268, 632 | "seed": 1814633775, 633 | "groupIds": [], 634 | "strokeSharpness": "round", 635 | "boundElementIds": [], 636 | "startBinding": { 637 | "elementId": "tqML5M3EgbT-MjahBBxRj", 638 | "gap": 3.771490355839451, 639 | "focus": -0.5054510885051404 640 | }, 641 | "endBinding": { 642 | "elementId": "COOiKDe_esnyxvePnkgEv", 643 | "gap": 7.870075122504333, 644 | "focus": 0.17807295140584803 645 | }, 646 | "lastCommittedPoint": null, 647 | "startArrowhead": null, 648 | "endArrowhead": "arrow", 649 | "points": [ 650 | [ 651 | 0, 652 | 0 653 | ], 654 | [ 655 | 191.87503608415636, 656 | -0.5942160710637268 657 | ] 658 | ] 659 | }, 660 | { 661 | "type": "arrow", 662 | "version": 457, 663 | "versionNonce": 1560324769, 664 | "isDeleted": false, 665 | "id": "glrlI13oTRddUQrUkMc9y", 666 | "fillStyle": "hachure", 667 | "strokeWidth": 1, 668 | "strokeStyle": "solid", 669 | "roughness": 1, 670 | "opacity": 100, 671 | "angle": 0, 672 | "x": 1397.312912149786, 673 | "y": 435.7034468251062, 674 | "strokeColor": "#000000", 675 | "backgroundColor": "transparent", 676 | "width": 200.53379489802728, 677 | "height": 0.32665702982006906, 678 | "seed": 1230219599, 679 | "groupIds": [], 680 | "strokeSharpness": "round", 681 | "boundElementIds": [], 682 | "startBinding": { 683 | "elementId": "COOiKDe_esnyxvePnkgEv", 684 | "gap": 1.93635542833917, 685 | "focus": -0.0034097153892514087 686 | }, 687 | "endBinding": { 688 | "elementId": "tqML5M3EgbT-MjahBBxRj", 689 | "gap": 1.0464512361336002, 690 | "focus": 0.5439520889757847 691 | }, 692 | "lastCommittedPoint": null, 693 | "startArrowhead": null, 694 | "endArrowhead": "arrow", 695 | "points": [ 696 | [ 697 | 0, 698 | 0 699 | ], 700 | [ 701 | -200.53379489802728, 702 | 0.32665702982006906 703 | ] 704 | ] 705 | }, 706 | { 707 | "type": "arrow", 708 | "version": 432, 709 | "versionNonce": 2125345940, 710 | "isDeleted": false, 711 | "id": "GjHPXFn-AhaXVbtkq_Ntu", 712 | "fillStyle": "hachure", 713 | "strokeWidth": 1, 714 | "strokeStyle": "solid", 715 | "roughness": 1, 716 | "opacity": 100, 717 | "angle": 0, 718 | "x": 1419.9562907093184, 719 | "y": 502.672889800922, 720 | "strokeColor": "#000000", 721 | "backgroundColor": "transparent", 722 | "width": 599.7955689453934, 723 | "height": 0.6764323839470308, 724 | "seed": 1789667777, 725 | "groupIds": [], 726 | "strokeSharpness": "round", 727 | "boundElementIds": [], 728 | "startBinding": { 729 | "elementId": "lIx9uQj8xYb9WRZUGM_DJ", 730 | "gap": 1.0048909313066443, 731 | "focus": -0.29787249364698276 732 | }, 733 | "endBinding": { 734 | "elementId": "gnpGDI40PBVCKn81wg50B", 735 | "gap": 4.099686607674912, 736 | "focus": 0.6210477707018276 737 | }, 738 | "lastCommittedPoint": null, 739 | "startArrowhead": null, 740 | "endArrowhead": "arrow", 741 | "points": [ 742 | [ 743 | 0, 744 | 0 745 | ], 746 | [ 747 | -599.7955689453934, 748 | -0.6764323839470308 749 | ] 750 | ] 751 | }, 752 | { 753 | "type": "text", 754 | "version": 83, 755 | "versionNonce": 1470496385, 756 | "isDeleted": false, 757 | "id": "Pconbxe0K3wTWcx0n_kH9", 758 | "fillStyle": "hachure", 759 | "strokeWidth": 1, 760 | "strokeStyle": "solid", 761 | "roughness": 1, 762 | "opacity": 100, 763 | "angle": 0, 764 | "x": 1047.464599609375, 765 | "y": 509.5965576171875, 766 | "strokeColor": "#000000", 767 | "backgroundColor": "transparent", 768 | "width": 96, 769 | "height": 20, 770 | "seed": 867697423, 771 | "groupIds": [], 772 | "strokeSharpness": "sharp", 773 | "boundElementIds": [], 774 | "fontSize": 16, 775 | "fontFamily": 1, 776 | "text": "pull nginx:1.16", 777 | "baseline": 14, 778 | "textAlign": "center", 779 | "verticalAlign": "top" 780 | }, 781 | { 782 | "type": "text", 783 | "version": 357, 784 | "versionNonce": 1305204372, 785 | "isDeleted": false, 786 | "id": "dTI4aXUV63_TPXnk5u1Cq", 787 | "fillStyle": "hachure", 788 | "strokeWidth": 1, 789 | "strokeStyle": "solid", 790 | "roughness": 1, 791 | "opacity": 100, 792 | "angle": 0, 793 | "x": 861.6762084960938, 794 | "y": 372.93170166015625, 795 | "strokeColor": "#000000", 796 | "backgroundColor": "transparent", 797 | "width": 106, 798 | "height": 20, 799 | "seed": 1440624591, 800 | "groupIds": [], 801 | "strokeSharpness": "sharp", 802 | "boundElementIds": [], 803 | "fontSize": 16, 804 | "fontFamily": 1, 805 | "text": "scan nginx:1.16", 806 | "baseline": 14, 807 | "textAlign": "center", 808 | "verticalAlign": "top" 809 | }, 810 | { 811 | "type": "text", 812 | "version": 250, 813 | "versionNonce": 145808428, 814 | "isDeleted": false, 815 | "id": "aVhnhzASOMNtlNDCeF0_3", 816 | "fillStyle": "hachure", 817 | "strokeWidth": 1, 818 | "strokeStyle": "solid", 819 | "roughness": 1, 820 | "opacity": 100, 821 | "angle": 0, 822 | "x": 626.2330932617188, 823 | "y": 275.70941162109375, 824 | "strokeColor": "#000000", 825 | "backgroundColor": "transparent", 826 | "width": 167, 827 | "height": 36, 828 | "seed": 2047752449, 829 | "groupIds": [], 830 | "strokeSharpness": "sharp", 831 | "boundElementIds": [], 832 | "fontSize": 16, 833 | "fontFamily": 2, 834 | "text": "http://nginx:8080\nhttps://harbor.domain.io", 835 | "baseline": 32, 836 | "textAlign": "center", 837 | "verticalAlign": "top" 838 | }, 839 | { 840 | "type": "text", 841 | "version": 200, 842 | "versionNonce": 1375452321, 843 | "isDeleted": false, 844 | "id": "_e7t6wOGab7EdXEjqdzGZ", 845 | "fillStyle": "hachure", 846 | "strokeWidth": 1, 847 | "strokeStyle": "solid", 848 | "roughness": 1, 849 | "opacity": 100, 850 | "angle": 0, 851 | "x": 1426.8751220703125, 852 | "y": 293.70941162109375, 853 | "strokeColor": "#000000", 854 | "backgroundColor": "transparent", 855 | "width": 157, 856 | "height": 18, 857 | "seed": 1481742607, 858 | "groupIds": [], 859 | "strokeSharpness": "sharp", 860 | "boundElementIds": [], 861 | "fontSize": 16, 862 | "fontFamily": 2, 863 | "text": "https://aqua.domain.io", 864 | "baseline": 14, 865 | "textAlign": "center", 866 | "verticalAlign": "top" 867 | }, 868 | { 869 | "type": "text", 870 | "version": 190, 871 | "versionNonce": 1839879343, 872 | "isDeleted": false, 873 | "id": "ygmD97rGfxdWeyhdF_JDm", 874 | "fillStyle": "hachure", 875 | "strokeWidth": 1, 876 | "strokeStyle": "solid", 877 | "roughness": 1, 878 | "opacity": 100, 879 | "angle": 0, 880 | "x": 1037.6820068359375, 881 | "y": 293.70941162109375, 882 | "strokeColor": "#000000", 883 | "backgroundColor": "transparent", 884 | "width": 175, 885 | "height": 18, 886 | "seed": 512520271, 887 | "groupIds": [], 888 | "strokeSharpness": "sharp", 889 | "boundElementIds": [], 890 | "fontSize": 16, 891 | "fontFamily": 2, 892 | "text": "http://aqua-adapter:8080", 893 | "baseline": 14, 894 | "textAlign": "center", 895 | "verticalAlign": "top" 896 | }, 897 | { 898 | "type": "arrow", 899 | "version": 678, 900 | "versionNonce": 1350656148, 901 | "isDeleted": false, 902 | "id": "tlKalHaZFu40GxW9wvNx4", 903 | "fillStyle": "hachure", 904 | "strokeWidth": 1, 905 | "strokeStyle": "solid", 906 | "roughness": 1, 907 | "opacity": 100, 908 | "angle": 0, 909 | "x": 708.2351684570312, 910 | "y": 618.1965942382812, 911 | "strokeColor": "#000000", 912 | "backgroundColor": "transparent", 913 | "width": 1.34490966796875, 914 | "height": 70.0626220703125, 915 | "seed": 841251919, 916 | "groupIds": [], 917 | "strokeSharpness": "round", 918 | "boundElementIds": [], 919 | "startBinding": { 920 | "elementId": "rLNmnD7IltlLPUNlAoJFh", 921 | "focus": -1.2754314757551055, 922 | "gap": 14.496826171875 923 | }, 924 | "endBinding": { 925 | "elementId": "gnpGDI40PBVCKn81wg50B", 926 | "focus": 0.15506309667138696, 927 | "gap": 6.610076904296875 928 | }, 929 | "lastCommittedPoint": null, 930 | "startArrowhead": null, 931 | "endArrowhead": "arrow", 932 | "points": [ 933 | [ 934 | 0, 935 | 0 936 | ], 937 | [ 938 | -1.34490966796875, 939 | -70.0626220703125 940 | ] 941 | ] 942 | }, 943 | { 944 | "type": "text", 945 | "version": 373, 946 | "versionNonce": 1010484780, 947 | "isDeleted": false, 948 | "id": "e42YtrbUC8rXHiC-xkgDp", 949 | "fillStyle": "hachure", 950 | "strokeWidth": 1, 951 | "strokeStyle": "solid", 952 | "roughness": 1, 953 | "opacity": 100, 954 | "angle": 0, 955 | "x": 830.5232543945312, 956 | "y": 411.9049072265625, 957 | "strokeColor": "#000000", 958 | "backgroundColor": "transparent", 959 | "width": 194, 960 | "height": 20, 961 | "seed": 1977681121, 962 | "groupIds": [], 963 | "strokeSharpness": "sharp", 964 | "boundElementIds": [], 965 | "fontSize": 16, 966 | "fontFamily": 1, 967 | "text": "vulnerabilities in nginx:1.16", 968 | "baseline": 14, 969 | "textAlign": "center", 970 | "verticalAlign": "top" 971 | }, 972 | { 973 | "type": "text", 974 | "version": 259, 975 | "versionNonce": 888899628, 976 | "isDeleted": false, 977 | "id": "rLNmnD7IltlLPUNlAoJFh", 978 | "fillStyle": "hachure", 979 | "strokeWidth": 1, 980 | "strokeStyle": "solid", 981 | "roughness": 1, 982 | "opacity": 100, 983 | "angle": 0, 984 | "x": 722.7319946289062, 985 | "y": 590.1785278320312, 986 | "strokeColor": "#000000", 987 | "backgroundColor": "transparent", 988 | "width": 106, 989 | "height": 20, 990 | "seed": 1915918785, 991 | "groupIds": [], 992 | "strokeSharpness": "sharp", 993 | "boundElementIds": [ 994 | "tlKalHaZFu40GxW9wvNx4" 995 | ], 996 | "fontSize": 16, 997 | "fontFamily": 1, 998 | "text": "scan nginx:1.16", 999 | "baseline": 14, 1000 | "textAlign": "center", 1001 | "verticalAlign": "top" 1002 | }, 1003 | { 1004 | "type": "text", 1005 | "version": 161, 1006 | "versionNonce": 1703294465, 1007 | "isDeleted": false, 1008 | "id": "pjMbQ4oCIEUzswgJ38gKg", 1009 | "fillStyle": "hachure", 1010 | "strokeWidth": 1, 1011 | "strokeStyle": "solid", 1012 | "roughness": 1, 1013 | "opacity": 100, 1014 | "angle": 0, 1015 | "x": 1261.963134765625, 1016 | "y": 390.162353515625, 1017 | "strokeColor": "#000000", 1018 | "backgroundColor": "transparent", 1019 | "width": 83, 1020 | "height": 20, 1021 | "seed": 1277450575, 1022 | "groupIds": [], 1023 | "strokeSharpness": "sharp", 1024 | "boundElementIds": [], 1025 | "fontSize": 16, 1026 | "fontFamily": 1, 1027 | "text": "HTTP API", 1028 | "baseline": 14, 1029 | "textAlign": "center", 1030 | "verticalAlign": "top" 1031 | }, 1032 | { 1033 | "type": "text", 1034 | "version": 205, 1035 | "versionNonce": 1653114516, 1036 | "isDeleted": false, 1037 | "id": "1yfqUtMr23gn5ofA7vsrC", 1038 | "fillStyle": "hachure", 1039 | "strokeWidth": 1, 1040 | "strokeStyle": "solid", 1041 | "roughness": 1, 1042 | "opacity": 100, 1043 | "angle": 0, 1044 | "x": 874.8617553710938, 1045 | "y": 339.71330334679965, 1046 | "strokeColor": "#000000", 1047 | "backgroundColor": "transparent", 1048 | "width": 83, 1049 | "height": 20, 1050 | "seed": 1271409452, 1051 | "groupIds": [], 1052 | "strokeSharpness": "sharp", 1053 | "boundElementIds": [], 1054 | "fontSize": 16, 1055 | "fontFamily": 1, 1056 | "text": "HTTP API", 1057 | "baseline": 14, 1058 | "textAlign": "center", 1059 | "verticalAlign": "top" 1060 | } 1061 | ], 1062 | "appState": { 1063 | "gridSize": null, 1064 | "viewBackgroundColor": "#ffffff" 1065 | } 1066 | } -------------------------------------------------------------------------------- /docs/images/hld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/harbor-scanner-aqua/085b02593caf650e701576c74da5d417d33f5695/docs/images/hld.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/harbor-scanner-aqua 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/FZambia/sentinel v1.1.0 7 | github.com/caarlos0/env/v6 v6.8.0 8 | github.com/gomodule/redigo v1.8.8 9 | github.com/google/uuid v1.3.0 10 | github.com/gorilla/mux v1.8.0 11 | github.com/prometheus/client_golang v1.11.0 12 | github.com/sirupsen/logrus v1.8.1 13 | github.com/stretchr/testify v1.7.0 14 | github.com/testcontainers/testcontainers-go v0.13.0 15 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 16 | ) 17 | 18 | require ( 19 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 20 | github.com/Microsoft/go-winio v0.4.17 // indirect 21 | github.com/Microsoft/hcsshim v0.8.23 // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cenkalti/backoff/v4 v4.1.2 // indirect 24 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 25 | github.com/containerd/cgroups v1.0.1 // indirect 26 | github.com/containerd/containerd v1.5.9 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/docker/distribution v2.7.1+incompatible // indirect 29 | github.com/docker/docker v20.10.11+incompatible // indirect 30 | github.com/docker/go-connections v0.4.0 // indirect 31 | github.com/docker/go-units v0.4.0 // indirect 32 | github.com/gogo/protobuf v1.3.2 // indirect 33 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 34 | github.com/golang/protobuf v1.5.2 // indirect 35 | github.com/magiconair/properties v1.8.5 // indirect 36 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 37 | github.com/moby/sys/mount v0.2.0 // indirect 38 | github.com/moby/sys/mountinfo v0.5.0 // indirect 39 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 40 | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect 41 | github.com/opencontainers/go-digest v1.0.0 // indirect 42 | github.com/opencontainers/image-spec v1.0.2 // indirect 43 | github.com/opencontainers/runc v1.0.2 // indirect 44 | github.com/pkg/errors v0.9.1 // indirect 45 | github.com/pmezard/go-difflib v1.0.0 // indirect 46 | github.com/prometheus/client_model v0.2.0 // indirect 47 | github.com/prometheus/common v0.26.0 // indirect 48 | github.com/prometheus/procfs v0.6.0 // indirect 49 | github.com/stretchr/objx v0.2.0 // indirect 50 | go.opencensus.io v0.22.3 // indirect 51 | golang.org/x/net v0.0.0-20211108170745-6635138e15ea // indirect 52 | golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect 53 | google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect 54 | google.golang.org/grpc v1.33.2 // indirect 55 | google.golang.org/protobuf v1.27.1 // indirect 56 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: harbor-scanner-aqua 3 | version: 0.14.0 4 | appVersion: 0.14.0 5 | description: Harbor scanner adapter for Aqua Enterprise scanner 6 | keywords: 7 | - scanner 8 | - harbor 9 | - vulnerability 10 | sources: 11 | - https://github.com/aquasecurity/harbor-scanner-aqua 12 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/README.md: -------------------------------------------------------------------------------- 1 | # Harbor Scanner Aqua 2 | 3 | Aqua Enterprise Scanner as a plug-in vulnerability scanner in the Harbor registry. 4 | 5 | ## TL;DR; 6 | 7 | ``` 8 | $ helm repo add aqua https://helm.aquasec.com 9 | ``` 10 | 11 | ### Without TLS 12 | 13 | ``` 14 | $ helm install harbor-scanner-aqua aqua/harbor-scanner-aqua \ 15 | --namespace harbor \ 16 | --set aqua.version=$AQUA_VERSION \ 17 | --set aqua.registry.server=registry.aquasec.com \ 18 | --set aqua.registry.username=$AQUA_REGISTRY_USERNAME \ 19 | --set aqua.registry.password=$AQUA_REGISTRY_PASSWORD \ 20 | --set scanner.aqua.username=$AQUA_CONSOLE_USERNAME \ 21 | --set scanner.aqua.password=$AQUA_CONSOLE_PASSWORD \ 22 | --set scanner.aqua.host=http://csp-console-svc.aqua:8080 23 | ``` 24 | 25 | ### With TLS 26 | 27 | 1. Generate certificate and private key files: 28 | ``` 29 | $ openssl genrsa -out tls.key 2048 30 | $ openssl req -new -x509 \ 31 | -key tls.key \ 32 | -out tls.crt \ 33 | -days 365 \ 34 | -subj /CN=harbor-scanner-aqua.harbor 35 | ``` 36 | 2. Install the `harbor-scanner-aqua` chart: 37 | ``` 38 | $ helm install harbor-scanner-aqua aqua/harbor-scanner-aqua \ 39 | --namespace harbor \ 40 | --set service.port=8443 \ 41 | --set scanner.api.tlsEnabled=true \ 42 | --set scanner.api.tlsCertificate="`cat tls.crt`" \ 43 | --set scanner.api.tlsKey="`cat tls.key`" \ 44 | --set aqua.version=$AQUA_VERSION \ 45 | --set aqua.registry.server=registry.aquasec.com \ 46 | --set aqua.registry.username=$AQUA_REGISTRY_USERNAME \ 47 | --set aqua.registry.password=$AQUA_REGISTRY_PASSWORD \ 48 | --set scanner.aqua.username=$AQUA_CONSOLE_USERNAME \ 49 | --set scanner.aqua.password=$AQUA_CONSOLE_PASSWORD \ 50 | --set scanner.aqua.host=http://csp-console-svc.aqua:8080 51 | ``` 52 | 53 | ## Introduction 54 | 55 | This chart bootstraps a scanner adapter deployment on a [Kubernetes](http://kubernetes.io) cluster using the 56 | [Helm](https://helm.sh) package manager. 57 | 58 | ## Prerequisites 59 | 60 | - Kubernetes 1.12+ 61 | - Helm 2.11+ or Helm 3+ 62 | - Add Aqua chart repository: 63 | ``` 64 | $ helm repo add aqua https://helm.aquasec.com 65 | ``` 66 | 67 | ## Installing the Chart 68 | 69 | To install the chart with the release name `my-release`: 70 | 71 | ``` 72 | $ helm install my-release aqua/harbor-scanner-aqua 73 | ``` 74 | 75 | The command deploys scanner adapter on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) 76 | section lists the parameters that can be configured during installation. 77 | 78 | > **Tip**: List all releases using `helm list`. 79 | 80 | ## Uninstalling the Chart 81 | 82 | To uninstall/delete the `my-release` deployment: 83 | 84 | ``` 85 | $ helm delete my-release 86 | ``` 87 | 88 | The command removes all the Kubernetes components associated with the chart and deletes the release. 89 | 90 | ## Parameters 91 | 92 | The following table lists the configurable parameters of the scanner adapter chart and their default values. 93 | 94 | | Parameter | Description | Default | 95 | |------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------| 96 | | `aqua.version` | The version of Aqua Enterprise that the adapter operates against | `5.0` | 97 | | `aqua.registry.server` | Aqua Docker registry server | `registry.aquasec.com` | 98 | | `aqua.registry.username` | Aqua Docker registry username | N/A | 99 | | `aqua.registry.password` | Aqua Docker registry password | N/A | 100 | | `scanner.image.registry` | Image registry | `docker.io` | 101 | | `scanner.image.repository` | Image name | `aquasec/harbor-scanner-aqua` | 102 | | `scanner.image.tag` | Image tag | `{TAG_NAME}` | 103 | | `scanner.image.pullPolicy` | Image pull policy | `IfNotPresent` | 104 | | `scanner.logLevel` | The log level of `trace`, `debug`, `info`, `warn`, `warning`, `error`, `fatal` or `panic`. The standard logger logs entries with that level or anything above it | `info` | 105 | | `scanner.aqua.username` | Aqua management console username (required) | N/A | 106 | | `scanner.aqua.password` | Aqua management console password (required) | N/A | 107 | | `scanner.aqua.host` | Aqua management console address | `http://csp-console-svc.aqua:8080` | 108 | | `scanner.aqua.registry` | The name of the Harbor registry configured in Aqua management console | `Harbor` | 109 | | `scanner.aqua.scannerCLINoVerify` | The flag passed to `scannercli` to skip verifying TLS certificates | `false` | 110 | | `scanner.aqua.scannerCLIShowNegligible` | The flag passed to `scannercli` to show negligible/unknown severity vulnerabilities | `true` | 111 | | `scanner.aqua.scannerCLIOverrideRegistryCredentials` | The flag to enable passing `--robot-username` and `--robot-password` flags to the `scannercli` executable binary | `false` | 112 | | `scanner.aqua.scannerCLIDirectCC` | The flag passed to `scannercli` to contact CyberCenter directly (rather than through the Aqua server) | `false` | 113 | | `scanner.aqua.scannerCLIRegisterImages` | The flag to determine whether images are registered in Aqua management console: `Never` - skips registration; `Compliant` - registers only compliant images; `Always` - registers compliant and non-compliant images. | `Never` | 114 | | `scanner.aqua.reportsDir` | Directory to save temporary scan reports | `/var/lib/scanner/reports` | 115 | | `scanner.aqua.useImageTag` | The flag to determine whether the image tag or digest is used in the image reference passed to `scannercli` | `false` | 116 | | `scanner.api.tlsEnabled` | The flag to enable or disable TLS for HTTP | `true` | 117 | | `scanner.api.tlsCertificate` | The absolute path to the x509 certificate file | | 118 | | `scanner.api.tlsKey` | The absolute path to the x509 private key file | | 119 | | `scanner.api.readTimeout` | The maximum duration for reading the entire request, including the body | `15s` | 120 | | `scanner.api.writeTimeout` | The maximum duration before timing out writes of the response | `15s` | 121 | | `scanner.api.idleTimeout` | The maximum amount of time to wait for the next request when keep-alives are enabled | `60s` | 122 | | `scanner.store.redisNamespace` | The namespace for keys in the Redis store | `harbor.scanner.aqua:store` | 123 | | `scanner.store.redisScanJobTTL` | The time to live for persisting scan jobs and associated scan reports | `1h` | 124 | | `scanner.redis.poolURL` | The server URI for the Redis store | `redis://harbor-harbor-redis:6379` | 125 | | `scanner.redis.poolMaxActive` | The max number of connections allocated by the pool for the Redis store | `5` | 126 | | `scanner.redis.poolMaxIdle` | The max number of idle connections in the pool for the Redis store | `5` | 127 | | `scanner.redis.poolpIdleTimeout` | The duration after which idle connections to the Redis server are closed. If the value is zero, then idle connections are not closed. | `5m` | 128 | | `scanner.redis.poolConnectionTimeout` | The timeout for connecting to the Redis server | `1s` | 129 | | `scanner.redis.poolReadTimeout` | The timeout for reading a single Redis command reply | `1s` | 130 | | `scanner.redis.poolWriteTimeout` | The timeout for writing a single Redis command | `1s` | 131 | | `service.type` | Kubernetes service type | `ClusterIP` | 132 | | `service.port` | Kubernetes service port | `8080` | 133 | | `replicaCount` | The number of scanner adapter Pods to run | `1` | 134 | 135 | The above parameters map to the env variables defined in [harbor-scanner-aqua](https://github.com/aquasecurity/harbor-scanner-aqua#configuration). 136 | 137 | Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. 138 | 139 | ``` 140 | $ helm install my-release aqua/harbor-scanner-aqua \ 141 | --namespace my-namespace \ 142 | --set scanner.aqua.username=$AQUA_CONSOLE_USERNAME \ 143 | --set scanner.aqua.password=$AQUA_CONSOLE_PASSWORD 144 | ``` 145 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | You should be able to access your scanner adapter installation within 2 | the cluster at {{ if .Values.scanner.api.tlsEnabled }}https{{ else }}http{{ end }}://{{ .Release.Name }}.{{ .Release.Namespace }}:{{ .Values.service.port }} 3 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "harbor-scanner-aqua.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 6 | {{- end -}} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "harbor-scanner-aqua.fullname" -}} 14 | {{- if .Values.fullnameOverride -}} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 16 | {{- else -}} 17 | {{- $name := default .Chart.Name .Values.nameOverride -}} 18 | {{- if contains $name .Release.Name -}} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 20 | {{- else -}} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 22 | {{- end -}} 23 | {{- end -}} 24 | {{- end -}} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "harbor-scanner-aqua.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 31 | {{- end -}} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "harbor-scanner-aqua.labels" -}} 37 | app.kubernetes.io/name: {{ include "harbor-scanner-aqua.name" . }} 38 | helm.sh/chart: {{ include "harbor-scanner-aqua.chart" . }} 39 | app.kubernetes.io/instance: {{ .Release.Name }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Return the proper imageRef as used by the init conainer template spec. 48 | */}} 49 | {{- define "harbor-scanner-aqua.scannerImageRef" -}} 50 | {{- $registryName := .Values.aqua.registry.server -}} 51 | {{- $repositoryName := "scanner" -}} 52 | {{- $tag := .Values.aqua.version | toString -}} 53 | {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} 54 | {{- end -}} 55 | 56 | {{/* 57 | Return the proper imageRef as used by the container template spec. 58 | */}} 59 | {{- define "harbor-scanner-aqua.adapterImageRef" -}} 60 | {{- $registryName := .Values.scanner.image.registry -}} 61 | {{- $repositoryName := .Values.scanner.image.repository -}} 62 | {{- $tag := .Values.scanner.image.tag | toString -}} 63 | {{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} 64 | {{- end -}} 65 | 66 | {{- define "imagePullSecret" -}} 67 | {{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.aqua.registry.server (printf "%s:%s" .Values.aqua.registry.username .Values.aqua.registry.password | b64enc) | b64enc }} 68 | {{- end }} 69 | 70 | {{/* 71 | Return the proper scheme for liveness and readiness probe spec. 72 | */}} 73 | {{- define "probeScheme" -}} 74 | {{- if .Values.scanner.api.tlsEnabled -}} 75 | HTTPS 76 | {{- else -}} 77 | HTTP 78 | {{- end -}} 79 | {{- end -}} 80 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "harbor-scanner-aqua.fullname" . }} 5 | labels: 6 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "harbor-scanner-aqua.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: {{ include "harbor-scanner-aqua.name" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | spec: 19 | serviceAccountName: {{ include "harbor-scanner-aqua.fullname" . }} 20 | automountServiceAccountToken: false 21 | securityContext: 22 | fsGroup: 1000 23 | runAsUser: 1000 24 | runAsNonRoot: true 25 | initContainers: 26 | - name: scannercli 27 | image: {{ template "harbor-scanner-aqua.scannerImageRef" . }} 28 | imagePullPolicy: {{ .Values.aqua.image.pullPolicy | quote }} 29 | securityContext: 30 | privileged: false 31 | allowPrivilegeEscalation: false 32 | readOnlyRootFilesystem: true 33 | capabilities: 34 | drop: 35 | - NET_RAW 36 | command: 37 | - cp 38 | args: 39 | - "/opt/aquasec/scannercli" 40 | - "/downloads/scannercli" 41 | {{- if .Values.initResources }} 42 | resources: 43 | {{ toYaml .Values.initResources | indent 12 }} 44 | {{- end }} 45 | volumeMounts: 46 | - name: scannercli 47 | mountPath: /downloads 48 | containers: 49 | - name: main 50 | image: {{ template "harbor-scanner-aqua.adapterImageRef" . }} 51 | imagePullPolicy: {{ .Values.scanner.image.pullPolicy | quote }} 52 | securityContext: 53 | privileged: false 54 | allowPrivilegeEscalation: false 55 | readOnlyRootFilesystem: true 56 | capabilities: 57 | drop: 58 | - NET_RAW 59 | env: 60 | - name: "SCANNER_LOG_LEVEL" 61 | value: {{ .Values.scanner.logLevel }} 62 | - name: "SCANNER_API_ADDR" 63 | value: ":{{ .Values.service.port }}" 64 | - name: "SCANNER_API_READ_TIMEOUT" 65 | value: {{ .Values.scanner.api.readTimeout | default "15s" | quote }} 66 | - name: "SCANNER_API_WRITE_TIMEOUT" 67 | value: {{ .Values.scanner.api.writeTimeout | default "15s" | quote }} 68 | - name: "SCANNER_API_IDLE_TIMEOUT" 69 | value: {{ .Values.scanner.api.idleTimeout | default "60s" | quote }} 70 | - name: "SCANNER_AQUA_USERNAME" 71 | valueFrom: 72 | secretKeyRef: 73 | name: {{ include "harbor-scanner-aqua.fullname" . }} 74 | key: aqua_username 75 | - name: "SCANNER_AQUA_PASSWORD" 76 | valueFrom: 77 | secretKeyRef: 78 | name: {{ include "harbor-scanner-aqua.fullname" . }} 79 | key: aqua_password 80 | - name: "SCANNER_AQUA_TOKEN" 81 | valueFrom: 82 | secretKeyRef: 83 | name: {{ include "harbor-scanner-aqua.fullname" . }} 84 | key: aqua_token 85 | - name: "SCANNER_AQUA_HOST" 86 | value: {{ .Values.scanner.aqua.host | quote }} 87 | - name: "SCANNER_AQUA_REGISTRY" 88 | value: {{ .Values.scanner.aqua.registry | default "Harbor" | quote }} 89 | - name: "SCANNER_CLI_NO_VERIFY" 90 | value: {{ .Values.scanner.aqua.scannerCLINoVerify | default false | quote }} 91 | - name: "SCANNER_CLI_SHOW_NEGLIGIBLE" 92 | value: {{ .Values.scanner.aqua.scannerCLIShowNegligible | default true | quote }} 93 | - name: "SCANNER_AQUA_REPORTS_DIR" 94 | value: {{ .Values.scanner.aqua.reportsDir | quote }} 95 | - name: "SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS" 96 | value: {{ .Values.scanner.aqua.scannerCLIOverrideRegistryCredentials | default false | quote }} 97 | - name: "SCANNER_CLI_DIRECT_CC" 98 | value: {{ .Values.scanner.aqua.scannerCLIDirectCC | default false | quote }} 99 | - name: "SCANNER_CLI_REGISTER_IMAGES" 100 | value: {{ .Values.scanner.aqua.scannerCLIRegisterImages | default "Never" | quote }} 101 | - name: "SCANNER_AQUA_USE_IMAGE_TAG" 102 | value: {{ .Values.scanner.aqua.useImageTag | default false | quote }} 103 | - name: "SCANNER_STORE_REDIS_NAMESPACE" 104 | value: {{ .Values.scanner.store.redisNamespace | quote }} 105 | - name: "SCANNER_STORE_REDIS_SCAN_JOB_TTL" 106 | value: {{ .Values.scanner.store.redisScanJobTTL | quote }} 107 | - name: "SCANNER_REDIS_URL" 108 | value: {{ .Values.scanner.redis.poolURL | quote }} 109 | - name: "SCANNER_REDIS_POOL_MAX_ACTIVE" 110 | value: {{ .Values.scanner.redis.poolMaxActive | quote }} 111 | - name: "SCANNER_REDIS_POOL_MAX_IDLE" 112 | value: {{ .Values.scanner.redis.poolMaxIdle | quote }} 113 | - name: "SCANNER_REDIS_POOL_IDLE_TIMEOUT" 114 | value: {{ .Values.scanner.redis.poolIdleTimeout | quote }} 115 | - name: "SCANNER_REDIS_POOL_CONNECTION_TIMEOUT" 116 | value: {{ .Values.scanner.redis.poolConnectionTimeout | quote }} 117 | - name: "SCANNER_REDIS_POOL_READ_TIMEOUT" 118 | value: {{ .Values.scanner.redis.poolReadTimeout | quote }} 119 | - name: "SCANNER_REDIS_POOL_WRITE_TIMEOUT" 120 | value: {{ .Values.scanner.redis.poolWriteTimeout | quote }} 121 | - name: "TMPDIR" 122 | value: {{ .Values.scanner.tmpdir }} 123 | {{- if .Values.scanner.api.tlsEnabled }} 124 | - name: "SCANNER_API_TLS_CERTIFICATE" 125 | value: "/certs/tls.crt" 126 | - name: "SCANNER_API_TLS_KEY" 127 | value: "/certs/tls.key" 128 | {{- end }} 129 | ports: 130 | - name: api-server 131 | containerPort: {{ .Values.service.port | default 8080 }} 132 | livenessProbe: 133 | httpGet: 134 | scheme: {{ include "probeScheme" . }} 135 | path: /probe/healthy 136 | port: api-server 137 | initialDelaySeconds: 5 138 | periodSeconds: 10 139 | successThreshold: 1 140 | failureThreshold: 10 141 | readinessProbe: 142 | httpGet: 143 | scheme: {{ include "probeScheme" . }} 144 | path: /probe/ready 145 | port: api-server 146 | initialDelaySeconds: 5 147 | periodSeconds: 10 148 | successThreshold: 1 149 | failureThreshold: 3 150 | {{- if .Values.mainResources }} 151 | resources: 152 | {{ toYaml .Values.mainResources | indent 12 }} 153 | {{- end}} 154 | volumeMounts: 155 | - name: scannercli 156 | mountPath: /usr/local/bin/scannercli 157 | subPath: scannercli 158 | - name: data 159 | mountPath: /var/lib/scanner/reports 160 | readOnly: false 161 | - name: aqua 162 | mountPath: /opt/aquascans 163 | readOnly: false 164 | {{- if .Values.scanner.api.tlsEnabled }} 165 | - name: certs 166 | mountPath: /certs 167 | readOnly: true 168 | {{- end }} 169 | volumes: 170 | - name: scannercli 171 | emptyDir: {} 172 | - name: data 173 | emptyDir: {} 174 | - name: aqua 175 | emptyDir: {} 176 | {{- if .Values.scanner.api.tlsEnabled }} 177 | - name: certs 178 | secret: 179 | secretName: {{ include "harbor-scanner-aqua.fullname" . }}-tls 180 | {{- end }} 181 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/secret-registry.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "harbor-scanner-aqua.fullname" . }}-registry 5 | labels: 6 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 7 | type: kubernetes.io/dockerconfigjson 8 | data: 9 | .dockerconfigjson: {{ template "imagePullSecret" . }} -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/secret-tls.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.scanner.api.tlsEnabled }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "harbor-scanner-aqua.fullname" . }}-tls 6 | labels: 7 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 8 | type: kubernetes.io/tls 9 | data: 10 | tls.crt: {{ required "TLS certificate required!" .Values.scanner.api.tlsCertificate | b64enc | quote }} 11 | tls.key: {{ required "TLS key required!" .Values.scanner.api.tlsKey | b64enc | quote }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "harbor-scanner-aqua.fullname" . }} 5 | labels: 6 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 7 | type: Opaque 8 | data: 9 | aqua_username: {{ .Values.scanner.aqua.username | b64enc | quote }} 10 | aqua_password: {{ .Values.scanner.aqua.password | b64enc | quote }} 11 | aqua_token: {{ .Values.scanner.aqua.token | b64enc | quote }} 12 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "harbor-scanner-aqua.fullname" . }} 5 | labels: 6 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | selector: 10 | app.kubernetes.io/name: {{ include "harbor-scanner-aqua.name" . }} 11 | app.kubernetes.io/instance: {{ .Release.Name }} 12 | ports: 13 | - name: api-server 14 | protocol: TCP 15 | port: {{ .Values.service.port }} 16 | targetPort: {{ .Values.service.port }} 17 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "harbor-scanner-aqua.fullname" . }} 5 | labels: 6 | {{ include "harbor-scanner-aqua.labels" . | indent 4 }} 7 | imagePullSecrets: 8 | - name: {{ include "harbor-scanner-aqua.fullname" . }}-registry 9 | -------------------------------------------------------------------------------- /helm/harbor-scanner-aqua/values.yaml: -------------------------------------------------------------------------------- 1 | nameOverride: "" 2 | fullnameOverride: "" 3 | 4 | service: 5 | ## type Kubernetes service type 6 | type: "ClusterIP" 7 | ## port Kubernetes service port 8 | port: 8080 9 | 10 | ## replicaCount the number of scanner adapter Pods to run 11 | replicaCount: 1 12 | 13 | initResources: 14 | requests: 15 | cpu: 200m 16 | memory: 128Mi 17 | limits: 18 | cpu: 500m 19 | memory: 256Mi 20 | 21 | mainResources: 22 | requests: 23 | cpu: 200m 24 | memory: 256Mi 25 | limits: 26 | cpu: 1 27 | memory: 512Mi 28 | 29 | aqua: 30 | ## version the version of Aqua Enterprise that the adapter operates against 31 | version: 5.0 32 | registry: 33 | ## server the Aqua Docker registry server 34 | server: "registry.aquasec.com" 35 | ## username the Aqua Docker registry username 36 | username: "" 37 | ## password the Aqua Docker registry password 38 | password: "" 39 | image: 40 | pullPolicy: "IfNotPresent" 41 | 42 | scanner: 43 | image: 44 | registry: "docker.io" 45 | repository: "aquasec/harbor-scanner-aqua" 46 | tag: "0.14.0" 47 | pullPolicy: "IfNotPresent" 48 | logLevel: info 49 | tmpdir: "" 50 | api: 51 | ## tlsEnabled the flag to enable TLS for HTTP 52 | tlsEnabled: false 53 | ## tlsCertificate the absolute path to the x509 certificate file 54 | tlsCertificate: "" 55 | ## tlsKey the absolute path to the x509 private key file 56 | tlsKey: "" 57 | ## readTimeout the maximum duration for reading the entire request, including the body 58 | readTimeout: 15s 59 | ## writeTimeout the maximum duration before timing out writes of the response 60 | writeTimeout: 15s 61 | ## idleTimeout the maximum amount of time to wait for the next request when keep-alives are enabled 62 | idleTimeout: 60s 63 | aqua: 64 | ## username Aqua management console username 65 | username: "" 66 | ## password Aqua management console password 67 | password: "" 68 | ## token Aqua scanner token for token based authentication 69 | token: "" 70 | ## host Aqua management console address 71 | host: "http://csp-console-svc.aqua:8080" 72 | ## registry the name of the Harbor registry configured in Aqua management console 73 | registry: "Harbor" 74 | ## reportsDir directory to save temporary scan reports 75 | reportsDir: "/var/lib/scanner/reports" 76 | ## useImageTag the flag to determine whether the image tag or digest is used in the image reference passed to `scannercli` 77 | useImageTag: false 78 | ## scannerCLINoVerify the flag passed to `scannercli` to skip verifying TLS certificates 79 | scannerCLINoVerify: false 80 | ## scannerCLIShowNegligible the flag passed to `scannercli` to show negligible/unknown severity vulnerabilities 81 | scannerCLIShowNegligible: true 82 | ## scannerCLIOverrideRegistryCredentials the flag to enable passing `--robot-username` and `--robot-password` 83 | ## flags to the `scannercli` executable binary 84 | scannerCLIOverrideRegistryCredentials: false 85 | ## scannerCLIDirectCC the flag passed to `scannercli` to contact CyberCenter directly (rather than through the Aqua server) 86 | scannerCLIDirectCC: false 87 | ## scannerCLIRegisterImages the flag to determine whether images are registered in Aqua management console: 88 | ## `Never` - skips registration 89 | ## `Compliant` - registers only compliant images 90 | ## `Always` - registers compliant and non-compliant images 91 | scannerCLIRegisterImages: Never 92 | store: 93 | ## redisNamespace the namespace for keys in the Redis store 94 | redisNamespace: "harbor.scanner.aqua:store" 95 | ## redisScanJobTTL the time to live for persisting scan jobs and associated scan reports 96 | redisScanJobTTL: "1h" 97 | redis: 98 | ## poolURL the Redis server URI. The URI supports schemas to connect to a standalone Redis server, 99 | ## i.e. `redis://:password@standalone_host:port/db-number` and Redis Sentinel deployment, 100 | ## i.e. `redis+sentinel://:password@sentinel_host1:port1,sentinel_host2:port2/monitor-name/db-number`. 101 | poolURL: "redis://harbor-harbor-redis:6379" 102 | ## poolMaxActive the max number of connections allocated by the Redis connection pool 103 | poolMaxActive: 5 104 | ## poolMaxIdle the max number of idle connections in the Redis connection pool 105 | poolMaxIdle: 5 106 | ## poolIdleTimeout the duration after which idle connections to the Redis server are closed. 107 | ## If the value is zero, then idle connections are not closed. 108 | poolIdleTimeout: 5m 109 | ## poolConnectionTimeout the timeout for connecting to the Redis server 110 | poolConnectionTimeout: 1s 111 | ## poolReadTimeout the timeout for reading a single Redis command reply 112 | poolReadTimeout: 1s 113 | ## poolWriteTimeout The timeout for writing a single Redis command 114 | poolWriteTimeout: 1s 115 | -------------------------------------------------------------------------------- /pkg/app.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/ext" 12 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/http/api" 13 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/http/api/v1" 14 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence/redis" 15 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/redisx" 16 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/scanner" 17 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/work" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | func Run(info etc.BuildInfo) error { 22 | log.WithFields(log.Fields{ 23 | "version": info.Version, 24 | "commit": info.Commit, 25 | "built_at": info.Date, 26 | }).Info("Starting harbor-scanner-aqua") 27 | 28 | config, err := etc.GetConfig() 29 | if err != nil { 30 | return fmt.Errorf("getting config: %w", err) 31 | } 32 | 33 | if _, err := os.Stat(config.AquaCSP.ReportsDir); os.IsNotExist(err) { 34 | log.WithField("path", config.AquaCSP.ReportsDir).Debug("Creating reports dir") 35 | err = os.MkdirAll(config.AquaCSP.ReportsDir, os.ModeDir) 36 | if err != nil { 37 | return fmt.Errorf("creating reports dir: %w", err) 38 | } 39 | } 40 | 41 | pool, err := redisx.NewPool(config.RedisPool) 42 | if err != nil { 43 | return fmt.Errorf("constructing connection pool: %w", err) 44 | } 45 | 46 | workPool := work.New() 47 | command := aqua.NewCommand(config.AquaCSP, ext.DefaultAmbassador) 48 | transformer := scanner.NewTransformer(ext.NewSystemClock()) 49 | adapter := scanner.NewAdapter(command, transformer) 50 | store := redis.NewStore(config.RedisStore, pool) 51 | enqueuer := scanner.NewEnqueuer(workPool, adapter, store) 52 | apiServer := api.NewServer(config.API, v1.NewAPIHandler(info, config, enqueuer, store)) 53 | 54 | shutdownComplete := make(chan struct{}) 55 | go func() { 56 | sigint := make(chan os.Signal, 1) 57 | signal.Notify(sigint, syscall.SIGINT, syscall.SIGTERM) 58 | captured := <-sigint 59 | log.WithField("signal", captured.String()).Debug("Trapped os signal") 60 | 61 | apiServer.Shutdown() 62 | workPool.Shutdown() 63 | 64 | close(shutdownComplete) 65 | }() 66 | 67 | workPool.Start() 68 | apiServer.ListenAndServe() 69 | 70 | <-shutdownComplete 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/app_test.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | -------------------------------------------------------------------------------- /pkg/aqua/command.go: -------------------------------------------------------------------------------- 1 | package aqua 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/ext" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type ImageRef struct { 15 | Repository string 16 | Tag string 17 | Digest string 18 | Auth RegistryAuth 19 | } 20 | 21 | type RegistryAuth struct { 22 | Username string 23 | Password string 24 | } 25 | 26 | func (ir *ImageRef) WithTag() string { 27 | return fmt.Sprintf("%s:%s", ir.Repository, ir.Tag) 28 | } 29 | 30 | func (ir *ImageRef) WithDigest() string { 31 | return fmt.Sprintf("%s@%s", ir.Repository, ir.Digest) 32 | } 33 | 34 | // Command represents the CLI interface for the Aqua Enterprise scanner, 35 | // i.e. scannercli executable. 36 | type Command interface { 37 | Scan(imageRef ImageRef) (ScanReport, error) 38 | } 39 | 40 | // NewCommand constructs Aqua Enterprise scanner command with the given configuration. 41 | func NewCommand(cfg etc.AquaCSP, ambassador ext.Ambassador) Command { 42 | return &command{ 43 | cfg: cfg, 44 | ambassador: ambassador, 45 | } 46 | } 47 | 48 | type command struct { 49 | cfg etc.AquaCSP 50 | ambassador ext.Ambassador 51 | } 52 | 53 | func (c *command) Scan(imageRef ImageRef) (report ScanReport, err error) { 54 | executable, err := c.ambassador.LookPath("scannercli") 55 | if err != nil { 56 | return report, fmt.Errorf("searching for scannercli executable: %w", err) 57 | } 58 | reportFile, err := c.ambassador.TempFile(c.cfg.ReportsDir, "aqua_scan_report_*.json") 59 | if err != nil { 60 | return report, fmt.Errorf("creating tmp scan report file: %w", err) 61 | } 62 | log.WithField("path", reportFile.Name()).Debug("Saving tmp scan report file") 63 | if c.cfg.ReportDelete { 64 | defer func() { 65 | log.WithField("path", reportFile.Name()).Debug("Removing tmp scan report file") 66 | err := c.ambassador.Remove(reportFile.Name()) 67 | if err != nil { 68 | log.WithError(err).Warn("Error while removing tmp scan report file") 69 | } 70 | }() 71 | } else { 72 | log.WithField("path", reportFile.Name()).Warn("tmp scan report file was stored") 73 | } 74 | 75 | image := imageRef.WithDigest() 76 | if c.cfg.UseImageTag && imageRef.WithTag() != "" { 77 | repoAndTag := strings.Split(imageRef.WithTag(), ":") 78 | if len(repoAndTag) == 2 && len(strings.TrimSpace(repoAndTag[1])) != 0 { 79 | log.WithField("input image name", c.cfg.UseImageTag).Infof("got proper image name:tag") 80 | image = imageRef.WithTag() 81 | } else { 82 | log.WithField("input image name", c.cfg.UseImageTag).WithField("input digest", imageRef.WithDigest()). 83 | Infof("failed with tag..proceeding with digest") 84 | } 85 | } 86 | args := []string{ 87 | "scan", 88 | "--checkonly", 89 | "--dockerless", 90 | //fmt.Sprintf("--user=%s", c.cfg.Username), 91 | fmt.Sprintf("--host=%s", c.cfg.Host), 92 | fmt.Sprintf("--registry=%s", c.cfg.Registry), 93 | fmt.Sprintf("--no-verify=%t", c.cfg.ScannerCLINoVerify), 94 | fmt.Sprintf("--direct-cc=%t", c.cfg.ScannerCLIDirectCC), 95 | fmt.Sprintf("--show-negligible=%t", c.cfg.ScannerCLIShowNegligible), 96 | fmt.Sprintf("--jsonfile=%s", reportFile.Name()), 97 | } 98 | 99 | switch c.cfg.ScannerCLIRegisterImages { 100 | case etc.Never: 101 | // Do nothing 102 | case etc.Always: 103 | args = append(args, "--register") 104 | case etc.Compliant: 105 | args = append(args, "--register-compliant") 106 | } 107 | 108 | log.WithFields(log.Fields{"exec": executable, "args": args}).Debug("Running scannercli") 109 | 110 | if c.cfg.ScannerCLIOverrideRegistryCredentials { 111 | args = append(args, fmt.Sprintf("--robot-username=%s", imageRef.Auth.Username), 112 | fmt.Sprintf("--robot-password=%s", imageRef.Auth.Password)) 113 | } 114 | if c.cfg.Token != "" { 115 | args = append(args, fmt.Sprintf("--token=%s", c.cfg.Token), image) 116 | } else { 117 | if c.cfg.Username == "" || c.cfg.Password == "" { 118 | log.WithFields(log.Fields{"exec": executable, "args": args}).Debug("Running scannercli") 119 | args = append(args, fmt.Sprintf("--user=%s", c.cfg.Username), fmt.Sprintf("--password=%s", c.cfg.Password), image) 120 | return report, fmt.Errorf("running command: %v: %v", args, "Username or password should not be empty") 121 | 122 | } 123 | args = append(args, fmt.Sprintf("--user=%s", c.cfg.Username), fmt.Sprintf("--password=%s", c.cfg.Password), image) 124 | } 125 | 126 | cmd := exec.Command(executable, args...) 127 | 128 | stdout, exitCode, err := c.ambassador.RunCmd(cmd) 129 | if err != nil { 130 | log.WithFields(log.Fields{ 131 | "image_ref_repository": imageRef.Repository, 132 | "image_ref_tag": imageRef.Tag, 133 | "image_ref_digest": imageRef.Digest, 134 | "exit_code": exitCode, 135 | "std_out": string(stdout), 136 | }).Error("Error while running scannercli command") 137 | return report, fmt.Errorf("running command: %v: %v", err, string(stdout)) 138 | } 139 | 140 | log.WithFields(log.Fields{ 141 | "image_ref_repository": imageRef.Repository, 142 | "image_ref_tag": imageRef.Tag, 143 | "image_ref_digest": imageRef.Digest, 144 | "exit_code": exitCode, 145 | "std_out": string(stdout), 146 | }).Trace("Running scannercli command finished") 147 | 148 | err = json.NewDecoder(reportFile).Decode(&report) 149 | if err != nil { 150 | return report, fmt.Errorf("decoding scan report from file: %w", err) 151 | } 152 | return 153 | } 154 | -------------------------------------------------------------------------------- /pkg/aqua/command_mock.go: -------------------------------------------------------------------------------- 1 | package aqua 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | type MockCommand struct { 6 | mock.Mock 7 | } 8 | 9 | func (c *MockCommand) Scan(imageRef ImageRef) (ScanReport, error) { 10 | args := c.Called(imageRef) 11 | return args.Get(0).(ScanReport), args.Error(1) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/aqua/command_test.go: -------------------------------------------------------------------------------- 1 | package aqua 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/ext" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | var ( 17 | NoError error = nil 18 | ) 19 | 20 | func TestCommand_Scan(t *testing.T) { 21 | 22 | config := etc.AquaCSP{ 23 | ReportsDir: "/var/lib/reports", 24 | Username: "scanner", 25 | Password: "ch@ng3me!", 26 | Host: "https://aqua.domain:8080", 27 | Registry: "Harbor", 28 | UseImageTag: true, 29 | ScannerCLINoVerify: true, 30 | ScannerCLIShowNegligible: true, 31 | ScannerCLIOverrideRegistryCredentials: true, 32 | ScannerCLIDirectCC: true, 33 | ScannerCLIRegisterImages: etc.Compliant, 34 | ReportDelete: true, 35 | } 36 | 37 | imageRef := ImageRef{ 38 | Repository: "library/alpine", 39 | Tag: "3.10.2", 40 | Auth: RegistryAuth{ 41 | Username: "robotName", 42 | Password: "robotPassword", 43 | }, 44 | } 45 | 46 | t.Run("Should return error when scannercli lookup returns error", func(t *testing.T) { 47 | ambassador := ext.NewMockAmbassador() 48 | ambassador.On("LookPath", "scannercli"). 49 | Return("/usr/local/bin/scannercli", errors.New("not found")) 50 | 51 | _, err := NewCommand(config, ambassador).Scan(ImageRef{}) 52 | assert.EqualError(t, err, "searching for scannercli executable: not found") 53 | ambassador.AssertExpectations(t) 54 | }) 55 | 56 | t.Run("Should return error when creating tmp file returns error", func(t *testing.T) { 57 | ambassador := ext.NewMockAmbassador() 58 | ambassador.On("LookPath", "scannercli"). 59 | Return("/usr/local/bin/scannercli", NoError) 60 | ambassador.On("TempFile", config.ReportsDir, "aqua_scan_report_*.json"). 61 | Return(nil, errors.New("no more space")) 62 | 63 | _, err := NewCommand(config, ambassador).Scan(ImageRef{}) 64 | assert.EqualError(t, err, "creating tmp scan report file: no more space") 65 | ambassador.AssertExpectations(t) 66 | }) 67 | 68 | t.Run("Should return error when running scannercli command returns error", func(t *testing.T) { 69 | ambassador := ext.NewMockAmbassador() 70 | ambassador.On("LookPath", "scannercli"). 71 | Return("/usr/local/bin/scannercli", NoError) 72 | ambassador.On("TempFile", config.ReportsDir, "aqua_scan_report_*.json"). 73 | Return(ext.NewFakeFile("/var/lib/scanner/reports/aqua_scan_report_1234567890.json", strings.NewReader("{}")), NoError) 74 | ambassador.On("Remove", "/var/lib/scanner/reports/aqua_scan_report_1234567890.json"). 75 | Return(NoError) 76 | ambassador.On("RunCmd", &exec.Cmd{ 77 | Path: "/usr/local/bin/scannercli", 78 | Args: []string{ 79 | "/usr/local/bin/scannercli", "scan", 80 | "--checkonly", 81 | "--dockerless", 82 | "--host=https://aqua.domain:8080", 83 | "--registry=Harbor", 84 | "--no-verify=true", 85 | "--direct-cc=true", 86 | "--show-negligible=true", 87 | "--jsonfile=/var/lib/scanner/reports/aqua_scan_report_1234567890.json", 88 | "--register-compliant", 89 | "--robot-username=robotName", 90 | "--robot-password=robotPassword", 91 | "--user=scanner", 92 | "--password=ch@ng3me!", 93 | "library/alpine:3.10.2", 94 | }, 95 | }).Return([]byte("killed"), 137, errors.New("boom")) 96 | 97 | _, err := NewCommand(config, ambassador).Scan(imageRef) 98 | assert.EqualError(t, err, "running command: boom: killed") 99 | ambassador.AssertExpectations(t) 100 | }) 101 | 102 | /* 103 | This test checks the tmp report isn't removed. 104 | There is no mock for `Remove` method, so there will be panic for removing the tmp report. 105 | */ 106 | t.Run("Should store the tmp report file", func(t *testing.T) { 107 | aquaReportJSON, err := os.Open("test_fixtures/aqua_report_photon_3.0.json") 108 | require.NoError(t, err) 109 | defer func() { 110 | _ = aquaReportJSON.Close() 111 | }() 112 | config.ReportDelete = false 113 | defer func() { 114 | config.ReportDelete = true 115 | }() 116 | ambassador := ext.NewMockAmbassador() 117 | ambassador.On("LookPath", "scannercli"). 118 | Return("/usr/local/bin/scannercli", NoError) 119 | ambassador.On("TempFile", config.ReportsDir, "aqua_scan_report_*.json"). 120 | Return(ext.NewFakeFile("/var/lib/scanner/reports/aqua_scan_report_1234567890.json", aquaReportJSON), NoError) 121 | // ambassador.On("Remove", "/var/lib/scanner/reports/aqua_scan_report_1234567890.json"). 122 | // Return(NoError) 123 | ambassador.On("RunCmd", &exec.Cmd{ 124 | Path: "/usr/local/bin/scannercli", 125 | Args: []string{ 126 | "/usr/local/bin/scannercli", "scan", 127 | "--checkonly", 128 | "--dockerless", 129 | "--host=https://aqua.domain:8080", 130 | "--registry=Harbor", 131 | "--no-verify=true", 132 | "--direct-cc=true", 133 | "--show-negligible=true", 134 | "--jsonfile=/var/lib/scanner/reports/aqua_scan_report_1234567890.json", 135 | "--register-compliant", 136 | "--robot-username=robotName", 137 | "--robot-password=robotPassword", 138 | "--user=scanner", 139 | "--password=ch@ng3me!", 140 | "library/alpine:3.10.2", 141 | }, 142 | }).Return([]byte{}, 0, NoError) 143 | aquaReport, err := NewCommand(config, ambassador).Scan(imageRef) 144 | require.NoError(t, err) 145 | assert.Equal(t, ScanReport{ 146 | Image: "library/photon@sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 147 | Registry: "Harbor", 148 | Digest: "", 149 | PullName: "core.harbor.domain/library/photon:sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 150 | OS: "photon", 151 | Version: "3.0", 152 | PartialResults: true, 153 | ChangedResults: false, 154 | InitiatingUser: "administrator", 155 | Resources: []ResourceScan{ 156 | { 157 | Resource: Resource{ 158 | Format: "", 159 | Type: Package, 160 | Path: "/usr/bin/bash", 161 | Name: "bash", 162 | Version: "4.4", 163 | CPE: "cpe:/a:gnu:bash:4.4", 164 | }, 165 | Scanned: true, 166 | Vulnerabilities: []Vulnerability{ 167 | { 168 | Name: "CVE-2017-5932", 169 | Description: "The path autocompletion feature in Bash 4.4 allows local users to gain privileges via a crafted filename starting with a \" (double quote) character and a command substitution metacharacter.", 170 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5932", 171 | VendorURL: "", 172 | FixVersion: "", 173 | AquaScore: 7.8, 174 | AquaSeverity: "high", 175 | AquaVectors: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 176 | AquaScoringSystem: "CVSS V3", 177 | }, 178 | { 179 | Name: "CVE-2019-18276", 180 | Description: "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", 181 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18276", 182 | VendorURL: "", 183 | FixVersion: "", 184 | AquaScore: 7.8, 185 | AquaSeverity: "high", 186 | AquaVectors: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 187 | AquaScoringSystem: "CVSS V3", 188 | }, 189 | }, 190 | }, 191 | { 192 | Resource: Resource{ 193 | Format: "", 194 | Type: Package, 195 | Path: "/usr/bin/gencat", 196 | Name: "glibc", 197 | Version: "2.28", 198 | CPE: "cpe:/a:gnu:glibc:2.28", 199 | }, 200 | Scanned: true, 201 | Vulnerabilities: []Vulnerability{ 202 | { 203 | Name: "CVE-2019-9169", 204 | Description: "In the GNU C Library (aka glibc or libc6) through 2.29, proceed_next_node in posix/regexec.c has a heap-based buffer over-read via an attempted case-insensitive regular-expression match.", 205 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-9169", 206 | VendorURL: "", 207 | FixVersion: "", 208 | AquaScore: 9.8, 209 | AquaSeverity: "critical", 210 | AquaVectors: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 211 | AquaScoringSystem: "CVSS V3", 212 | }, 213 | }, 214 | }, 215 | }, 216 | Summary: Summary{ 217 | Total: 12, 218 | Critical: 2, 219 | High: 6, 220 | Medium: 3, 221 | Low: 1, 222 | Negligible: 0, 223 | Sensitive: 0, 224 | Malware: 0, 225 | }, 226 | ScanOptions: ScanOptions{ 227 | ScanExecutables: true, 228 | ShowWillNotFix: true, 229 | StrictScan: true, 230 | ScanMalware: false, 231 | ScanFiles: true, 232 | ManualPullFallback: true, 233 | SaveAdHockScans: true, 234 | Dockerless: true, 235 | EnableFastScanning: true, 236 | SuggestOSUpgrade: true, 237 | IncludeSiblingAdvisories: true, 238 | UseCVSS3: true, 239 | }, 240 | }, aquaReport) 241 | 242 | ambassador.AssertExpectations(t) 243 | }) 244 | 245 | t.Run("Should return scan report", func(t *testing.T) { 246 | aquaReportJSON, err := os.Open("test_fixtures/aqua_report_photon_3.0.json") 247 | require.NoError(t, err) 248 | defer func() { 249 | _ = aquaReportJSON.Close() 250 | }() 251 | ambassador := ext.NewMockAmbassador() 252 | ambassador.On("LookPath", "scannercli"). 253 | Return("/usr/local/bin/scannercli", NoError) 254 | ambassador.On("TempFile", config.ReportsDir, "aqua_scan_report_*.json"). 255 | Return(ext.NewFakeFile("/var/lib/scanner/reports/aqua_scan_report_1234567890.json", aquaReportJSON), NoError) 256 | ambassador.On("Remove", "/var/lib/scanner/reports/aqua_scan_report_1234567890.json"). 257 | Return(NoError) 258 | ambassador.On("RunCmd", &exec.Cmd{ 259 | Path: "/usr/local/bin/scannercli", 260 | Args: []string{ 261 | "/usr/local/bin/scannercli", "scan", 262 | "--checkonly", 263 | "--dockerless", 264 | "--host=https://aqua.domain:8080", 265 | "--registry=Harbor", 266 | "--no-verify=true", 267 | "--direct-cc=true", 268 | "--show-negligible=true", 269 | "--jsonfile=/var/lib/scanner/reports/aqua_scan_report_1234567890.json", 270 | "--register-compliant", 271 | "--robot-username=robotName", 272 | "--robot-password=robotPassword", 273 | "--user=scanner", 274 | "--password=ch@ng3me!", 275 | "library/alpine:3.10.2", 276 | }, 277 | }).Return([]byte{}, 0, NoError) 278 | 279 | aquaReport, err := NewCommand(config, ambassador).Scan(imageRef) 280 | require.NoError(t, err) 281 | assert.Equal(t, ScanReport{ 282 | Image: "library/photon@sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 283 | Registry: "Harbor", 284 | Digest: "", 285 | PullName: "core.harbor.domain/library/photon:sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 286 | OS: "photon", 287 | Version: "3.0", 288 | PartialResults: true, 289 | ChangedResults: false, 290 | InitiatingUser: "administrator", 291 | Resources: []ResourceScan{ 292 | { 293 | Resource: Resource{ 294 | Format: "", 295 | Type: Package, 296 | Path: "/usr/bin/bash", 297 | Name: "bash", 298 | Version: "4.4", 299 | CPE: "cpe:/a:gnu:bash:4.4", 300 | }, 301 | Scanned: true, 302 | Vulnerabilities: []Vulnerability{ 303 | { 304 | Name: "CVE-2017-5932", 305 | Description: "The path autocompletion feature in Bash 4.4 allows local users to gain privileges via a crafted filename starting with a \" (double quote) character and a command substitution metacharacter.", 306 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5932", 307 | VendorURL: "", 308 | FixVersion: "", 309 | AquaScore: 7.8, 310 | AquaSeverity: "high", 311 | AquaVectors: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 312 | AquaScoringSystem: "CVSS V3", 313 | }, 314 | { 315 | Name: "CVE-2019-18276", 316 | Description: "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", 317 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18276", 318 | VendorURL: "", 319 | FixVersion: "", 320 | AquaScore: 7.8, 321 | AquaSeverity: "high", 322 | AquaVectors: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 323 | AquaScoringSystem: "CVSS V3", 324 | }, 325 | }, 326 | }, 327 | { 328 | Resource: Resource{ 329 | Format: "", 330 | Type: Package, 331 | Path: "/usr/bin/gencat", 332 | Name: "glibc", 333 | Version: "2.28", 334 | CPE: "cpe:/a:gnu:glibc:2.28", 335 | }, 336 | Scanned: true, 337 | Vulnerabilities: []Vulnerability{ 338 | { 339 | Name: "CVE-2019-9169", 340 | Description: "In the GNU C Library (aka glibc or libc6) through 2.29, proceed_next_node in posix/regexec.c has a heap-based buffer over-read via an attempted case-insensitive regular-expression match.", 341 | NVDURL: "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-9169", 342 | VendorURL: "", 343 | FixVersion: "", 344 | AquaScore: 9.8, 345 | AquaSeverity: "critical", 346 | AquaVectors: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 347 | AquaScoringSystem: "CVSS V3", 348 | }, 349 | }, 350 | }, 351 | }, 352 | Summary: Summary{ 353 | Total: 12, 354 | Critical: 2, 355 | High: 6, 356 | Medium: 3, 357 | Low: 1, 358 | Negligible: 0, 359 | Sensitive: 0, 360 | Malware: 0, 361 | }, 362 | ScanOptions: ScanOptions{ 363 | ScanExecutables: true, 364 | ShowWillNotFix: true, 365 | StrictScan: true, 366 | ScanMalware: false, 367 | ScanFiles: true, 368 | ManualPullFallback: true, 369 | SaveAdHockScans: true, 370 | Dockerless: true, 371 | EnableFastScanning: true, 372 | SuggestOSUpgrade: true, 373 | IncludeSiblingAdvisories: true, 374 | UseCVSS3: true, 375 | }, 376 | }, aquaReport) 377 | 378 | ambassador.AssertExpectations(t) 379 | }) 380 | 381 | } 382 | -------------------------------------------------------------------------------- /pkg/aqua/model.go: -------------------------------------------------------------------------------- 1 | package aqua 2 | 3 | type ResourceType int 4 | 5 | const ( 6 | _ ResourceType = iota 7 | Library 8 | Package 9 | ) 10 | 11 | type ScanReport struct { 12 | Image string `json:"image"` 13 | Registry string `json:"registry"` 14 | Digest string `json:"digest"` 15 | PullName string `json:"pull_name"` 16 | OS string `json:"os"` 17 | Version string `json:"version"` 18 | PartialResults bool `json:"partial_results"` 19 | ChangedResults bool `json:"changed_results"` 20 | InitiatingUser string `json:"initiating_user"` 21 | Resources []ResourceScan `json:"resources"` 22 | Summary Summary `json:"vulnerability_summary"` 23 | ScanOptions ScanOptions `json:"scan_options"` 24 | } 25 | 26 | type ResourceScan struct { 27 | Resource Resource `json:"resource"` 28 | Scanned bool `json:"scanned"` 29 | Vulnerabilities []Vulnerability `json:"vulnerabilities"` 30 | } 31 | 32 | type Resource struct { 33 | Format string `json:"format"` 34 | Type ResourceType `json:"type"` 35 | Path string `json:"path"` 36 | Name string `json:"name"` 37 | Version string `json:"version"` 38 | CPE string `json:"cpe"` // CPE Common Platform Enumerations 39 | } 40 | 41 | type Vulnerability struct { 42 | Name string `json:"name"` 43 | Description string `json:"description"` 44 | NVDURL string `json:"nvd_url"` 45 | VendorURL string `json:"vendor_url"` 46 | FixVersion string `json:"fix_version"` 47 | AquaScore float32 `json:"aqua_score"` 48 | AquaSeverity string `json:"aqua_severity"` 49 | AquaVectors string `json:"aqua_vectors"` 50 | AquaScoringSystem string `json:"aqua_scoring_system"` 51 | } 52 | 53 | type Summary struct { 54 | Total int `json:"total"` 55 | Critical int `json:"critical"` 56 | High int `json:"high"` 57 | Medium int `json:"medium"` 58 | Low int `json:"low"` 59 | Negligible int `json:"negligible"` 60 | Sensitive int `json:"sensitive"` 61 | Malware int `json:"malware"` 62 | } 63 | 64 | type ScanOptions struct { 65 | ScanExecutables bool `json:"scan_executables"` 66 | ShowWillNotFix bool `json:"show_will_not_fix"` 67 | StrictScan bool `json:"strict_scan"` 68 | ScanMalware bool `json:"scan_malware"` 69 | ScanFiles bool `json:"scan_files"` 70 | ManualPullFallback bool `json:"manual_pull_fallback"` 71 | SaveAdHockScans bool `json:"save_adhoc_scans"` 72 | Dockerless bool `json:"dockerless"` 73 | EnableFastScanning bool `json:"enable_fast_scanning"` 74 | SuggestOSUpgrade bool `json:"suggest_os_upgrade"` 75 | IncludeSiblingAdvisories bool `json:"include_sibling_advisories"` 76 | UseCVSS3 bool `json:"use_cvss3"` 77 | } 78 | -------------------------------------------------------------------------------- /pkg/aqua/model_test.go: -------------------------------------------------------------------------------- 1 | package aqua 2 | -------------------------------------------------------------------------------- /pkg/aqua/test_fixtures/aqua_report_photon_3.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "library/photon@sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 3 | "registry": "Harbor", 4 | "image_size": 35298136, 5 | "os": "photon", 6 | "version": "3.0", 7 | "resources": [ 8 | { 9 | "resource": { 10 | "type": 2, 11 | "path": "/usr/bin/bash", 12 | "name": "bash", 13 | "version": "4.4", 14 | "cpe": "cpe:/a:gnu:bash:4.4", 15 | "layer": "/bin/sh -c #(nop) ADD file:8977940c5bfd0be1e27cac9394290b34ce29c9a16b1e9be164e5e93ba4cb403c in / ", 16 | "layer_digest": "sha256:3478fd58133b768140e03314353a1d1bd854ae7cbfdebdd26a02742129edb8c3" 17 | }, 18 | "scanned": true, 19 | "vulnerabilities": [ 20 | { 21 | "name": "CVE-2017-5932", 22 | "description": "The path autocompletion feature in Bash 4.4 allows local users to gain privileges via a crafted filename starting with a \" (double quote) character and a command substitution metacharacter.", 23 | "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5932", 24 | "aqua_score": 7.8, 25 | "aqua_severity": "high", 26 | "aqua_vectors": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 27 | "aqua_scoring_system": "CVSS V3" 28 | }, 29 | { 30 | "name": "CVE-2019-18276", 31 | "description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", 32 | "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18276", 33 | "aqua_score": 7.8, 34 | "aqua_severity": "high", 35 | "aqua_vectors": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 36 | "aqua_scoring_system": "CVSS V3" 37 | } 38 | ] 39 | }, 40 | { 41 | "resource": { 42 | "type": 2, 43 | "path": "/usr/bin/gencat", 44 | "name": "glibc", 45 | "version": "2.28", 46 | "cpe": "cpe:/a:gnu:glibc:2.28", 47 | "layer": "/bin/sh -c #(nop) ADD file:8977940c5bfd0be1e27cac9394290b34ce29c9a16b1e9be164e5e93ba4cb403c in / ", 48 | "layer_digest": "sha256:3478fd58133b768140e03314353a1d1bd854ae7cbfdebdd26a02742129edb8c3" 49 | }, 50 | "scanned": true, 51 | "vulnerabilities": [ 52 | { 53 | "name": "CVE-2019-9169", 54 | "description": "In the GNU C Library (aka glibc or libc6) through 2.29, proceed_next_node in posix/regexec.c has a heap-based buffer over-read via an attempted case-insensitive regular-expression match.", 55 | "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-9169", 56 | "aqua_score": 9.8, 57 | "aqua_severity": "critical", 58 | "aqua_vectors": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 59 | "aqua_scoring_system": "CVSS V3" 60 | } 61 | ] 62 | } 63 | ], 64 | "vulnerability_summary": { 65 | "total": 12, 66 | "high": 6, 67 | "medium": 3, 68 | "low": 1, 69 | "negligible": 0, 70 | "sensitive": 0, 71 | "malware": 0, 72 | "score_average": 7.091667, 73 | "critical": 2 74 | }, 75 | "scan_options": { 76 | "scan_executables": true, 77 | "show_will_not_fix": true, 78 | "strict_scan": true, 79 | "scan_files": true, 80 | "scan_timeout": 3600000000000, 81 | "manual_pull_fallback": true, 82 | "save_adhoc_scans": true, 83 | "use_cvss3": true, 84 | "dockerless": true, 85 | "system_image_platform": "amd64:::", 86 | "include_sibling_advisories": true, 87 | "enable_fast_scanning": true, 88 | "suggest_os_upgrade": true 89 | }, 90 | "partial_results": true, 91 | "initiating_user": "administrator", 92 | "data_date": 1587336045, 93 | "pull_name": "core.harbor.domain/library/photon:sha256:ba6a5e0592483f28827545ce100f711aa602adf100e5884840c56c5b9b059acc", 94 | "changed_result": false 95 | } 96 | -------------------------------------------------------------------------------- /pkg/etc/config.go: -------------------------------------------------------------------------------- 1 | package etc 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "reflect" 8 | "sync" 9 | "time" 10 | 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 12 | "github.com/caarlos0/env/v6" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var version = "Unknown" 17 | var once sync.Once 18 | 19 | type BuildInfo struct { 20 | Version string 21 | Commit string 22 | Date string 23 | } 24 | 25 | type Config struct { 26 | API API 27 | AquaCSP AquaCSP 28 | RedisStore RedisStore 29 | RedisPool RedisPool 30 | } 31 | 32 | type API struct { 33 | Addr string `env:"SCANNER_API_ADDR" envDefault:":8080"` 34 | TLSCertificate string `env:"SCANNER_API_TLS_CERTIFICATE"` 35 | TLSKey string `env:"SCANNER_API_TLS_KEY"` 36 | ReadTimeout time.Duration `env:"SCANNER_API_READ_TIMEOUT" envDefault:"15s"` 37 | WriteTimeout time.Duration `env:"SCANNER_API_WRITE_TIMEOUT" envDefault:"15s"` 38 | IdleTimeout time.Duration `env:"SCANNER_API_IDLE_TIMEOUT" envDefault:"60s"` 39 | } 40 | 41 | func (c API) IsTLSEnabled() bool { 42 | return c.TLSCertificate != "" && c.TLSKey != "" 43 | } 44 | 45 | type ImageRegistration string 46 | 47 | const ( 48 | Never ImageRegistration = "Never" 49 | Always ImageRegistration = "Always" 50 | Compliant ImageRegistration = "Compliant" 51 | ) 52 | 53 | type AquaCSP struct { 54 | Username string `env:"SCANNER_AQUA_USERNAME"` 55 | Password string `env:"SCANNER_AQUA_PASSWORD"` 56 | Token string `env:"SCANNER_AQUA_TOKEN"` 57 | Host string `env:"SCANNER_AQUA_HOST" envDefault:"http://csp-console-svc.aqua:8080"` 58 | Registry string `env:"SCANNER_AQUA_REGISTRY" envDefault:"Harbor"` 59 | 60 | UseImageTag bool `env:"SCANNER_AQUA_USE_IMAGE_TAG" envDefault:"true"` 61 | ReportsDir string `env:"SCANNER_AQUA_REPORTS_DIR" envDefault:"/var/lib/scanner/reports"` 62 | ScannerCLINoVerify bool `env:"SCANNER_CLI_NO_VERIFY" envDefault:"false"` 63 | ScannerCLIShowNegligible bool `env:"SCANNER_CLI_SHOW_NEGLIGIBLE" envDefault:"true"` 64 | ScannerCLIDirectCC bool `env:"SCANNER_CLI_DIRECT_CC" envDefault:"false"` 65 | ScannerCLIRegisterImages ImageRegistration `env:"SCANNER_CLI_REGISTER_IMAGES" envDefault:"Never"` 66 | 67 | ScannerCLIOverrideRegistryCredentials bool `env:"SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS" envDefault:"false"` 68 | 69 | ReportDelete bool `env:"SCANNER_AQUA_REPORT_DELETE" envDefault:"true"` 70 | } 71 | 72 | type RedisStore struct { 73 | Namespace string `env:"SCANNER_STORE_REDIS_NAMESPACE" envDefault:"harbor.scanner.aqua:store"` 74 | ScanJobTTL time.Duration `env:"SCANNER_STORE_REDIS_SCAN_JOB_TTL" envDefault:"1h"` 75 | } 76 | 77 | type RedisPool struct { 78 | URL string `env:"SCANNER_REDIS_URL" envDefault:"redis://harbor-harbor-redis:6379"` 79 | MaxActive int `env:"SCANNER_REDIS_POOL_MAX_ACTIVE" envDefault:"5"` 80 | MaxIdle int `env:"SCANNER_REDIS_POOL_MAX_IDLE" envDefault:"5"` 81 | IdleTimeout time.Duration `env:"SCANNER_REDIS_POOL_IDLE_TIMEOUT" envDefault:"5m"` 82 | ConnectionTimeout time.Duration `env:"SCANNER_REDIS_POOL_CONNECTION_TIMEOUT" envDefault:"1s"` 83 | ReadTimeout time.Duration `env:"SCANNER_REDIS_POOL_READ_TIMEOUT" envDefault:"1s"` 84 | WriteTimeout time.Duration `env:"SCANNER_REDIS_POOL_WRITE_TIMEOUT" envDefault:"1s"` 85 | } 86 | 87 | var ( 88 | customParser = map[reflect.Type]env.ParserFunc{ 89 | reflect.TypeOf(ImageRegistration("")): func(v string) (interface{}, error) { 90 | switch v { 91 | case string(Never): 92 | return Never, nil 93 | case string(Always): 94 | return Always, nil 95 | case string(Compliant): 96 | return Compliant, nil 97 | } 98 | return nil, fmt.Errorf("expected values %s, %s or %s but got %s", Never, Always, Compliant, v) 99 | }, 100 | } 101 | ) 102 | 103 | func GetConfig() (Config, error) { 104 | var cfg Config 105 | err := env.ParseWithFuncs(&cfg, customParser) 106 | if err != nil { 107 | return cfg, err 108 | } 109 | return cfg, nil 110 | } 111 | 112 | func GetLogLevel() log.Level { 113 | if value, ok := os.LookupEnv("SCANNER_LOG_LEVEL"); ok { 114 | level, err := log.ParseLevel(value) 115 | if err != nil { 116 | return log.InfoLevel 117 | } 118 | return level 119 | } 120 | return log.InfoLevel 121 | } 122 | 123 | func GetScannerMetadata() harbor.Scanner { 124 | once.Do(func() { 125 | v, err := getVersion() 126 | if err != nil { 127 | log.WithError(err).Error("Error while retrieving version") 128 | return 129 | } 130 | version = v 131 | }) 132 | return harbor.Scanner{ 133 | Name: "Aqua Enterprise", 134 | Vendor: "Aqua Security", 135 | Version: version, 136 | } 137 | } 138 | 139 | func getVersion() (string, error) { 140 | executable, err := exec.LookPath("scannercli") 141 | if err != nil { 142 | return "", err 143 | } 144 | cmd := exec.Command(executable, "version") 145 | out, err := cmd.Output() 146 | if err != nil { 147 | return "", err 148 | } 149 | 150 | return string(out), nil 151 | } 152 | -------------------------------------------------------------------------------- /pkg/etc/config_test.go: -------------------------------------------------------------------------------- 1 | package etc 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type envs map[string]string 14 | 15 | func TestGetConfig(t *testing.T) { 16 | testCases := []struct { 17 | name string 18 | envs envs 19 | expectedError string 20 | expectedConfig Config 21 | }{ 22 | { 23 | name: "Should return default config", 24 | expectedConfig: Config{ 25 | API: API{ 26 | Addr: ":8080", 27 | ReadTimeout: parseDuration(t, "15s"), 28 | WriteTimeout: parseDuration(t, "15s"), 29 | IdleTimeout: parseDuration(t, "60s"), 30 | }, 31 | AquaCSP: AquaCSP{ 32 | Username: "", 33 | Password: "", 34 | Host: "http://csp-console-svc.aqua:8080", 35 | Registry: "Harbor", 36 | ReportsDir: "/var/lib/scanner/reports", 37 | UseImageTag: true, 38 | 39 | ScannerCLINoVerify: false, 40 | ScannerCLIShowNegligible: true, 41 | ScannerCLIOverrideRegistryCredentials: false, 42 | ScannerCLIRegisterImages: Never, 43 | 44 | ReportDelete: true, 45 | }, 46 | RedisStore: RedisStore{ 47 | Namespace: "harbor.scanner.aqua:store", 48 | ScanJobTTL: parseDuration(t, "1h"), 49 | }, 50 | RedisPool: RedisPool{ 51 | URL: "redis://harbor-harbor-redis:6379", 52 | MaxActive: 5, 53 | MaxIdle: 5, 54 | IdleTimeout: parseDuration(t, "5m"), 55 | ConnectionTimeout: parseDuration(t, "1s"), 56 | ReadTimeout: parseDuration(t, "1s"), 57 | WriteTimeout: parseDuration(t, "1s"), 58 | }, 59 | }, 60 | }, 61 | { 62 | name: "Should return error when ScannerCLIRegisterImages has invalid value", 63 | envs: envs{ 64 | "SCANNER_CLI_REGISTER_IMAGES": "XXX", 65 | }, 66 | expectedError: "env: parse error on field \"ScannerCLIRegisterImages\" of type \"etc.ImageRegistration\": expected values Never, Always or Compliant but got XXX", 67 | }, 68 | { 69 | name: "Should overwrite default config with environment variables", 70 | envs: envs{ 71 | "SCANNER_API_ADDR": ":4200", 72 | "SCANNER_API_TLS_CERTIFICATE": "/certs/tls.crt", 73 | "SCANNER_API_TLS_KEY": "/certs/tls.key", 74 | "SCANNER_API_READ_TIMEOUT": "1h", 75 | "SCANNER_API_WRITE_TIMEOUT": "2m", 76 | "SCANNER_API_IDLE_TIMEOUT": "1h2m3s", 77 | "SCANNER_AQUA_REPORTS_DIR": "/somewhere/else", 78 | "SCANNER_AQUA_USE_IMAGE_TAG": "false", 79 | "SCANNER_AQUA_HOST": "http://aqua-web.aqua-security:8080", 80 | "SCANNER_AQUA_USERNAME": "scanner", 81 | "SCANNER_AQUA_PASSWORD": "s3cret", 82 | "SCANNER_CLI_NO_VERIFY": "true", 83 | "SCANNER_CLI_SHOW_NEGLIGIBLE": "false", 84 | "SCANNER_CLI_REGISTER_IMAGES": "Compliant", 85 | "SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS": "true", 86 | "SCANNER_REDIS_URL": "redis://localhost:6379", 87 | "SCANNER_AQUA_REPORT_DELETE": "false", 88 | }, 89 | expectedConfig: Config{ 90 | API: API{ 91 | Addr: ":4200", 92 | TLSCertificate: "/certs/tls.crt", 93 | TLSKey: "/certs/tls.key", 94 | ReadTimeout: parseDuration(t, "1h"), 95 | WriteTimeout: parseDuration(t, "2m"), 96 | IdleTimeout: parseDuration(t, "1h2m3s"), 97 | }, 98 | AquaCSP: AquaCSP{ 99 | Username: "scanner", 100 | Password: "s3cret", 101 | Host: "http://aqua-web.aqua-security:8080", 102 | Registry: "Harbor", 103 | ReportsDir: "/somewhere/else", 104 | UseImageTag: false, 105 | ScannerCLINoVerify: true, 106 | ScannerCLIShowNegligible: false, 107 | ScannerCLIRegisterImages: Compliant, 108 | ScannerCLIOverrideRegistryCredentials: true, 109 | ReportDelete: false, 110 | }, 111 | RedisStore: RedisStore{ 112 | Namespace: "harbor.scanner.aqua:store", 113 | ScanJobTTL: parseDuration(t, "1h"), 114 | }, 115 | RedisPool: RedisPool{ 116 | URL: "redis://localhost:6379", 117 | MaxActive: 5, 118 | MaxIdle: 5, 119 | IdleTimeout: parseDuration(t, "5m"), 120 | ConnectionTimeout: parseDuration(t, "1s"), 121 | ReadTimeout: parseDuration(t, "1s"), 122 | WriteTimeout: parseDuration(t, "1s"), 123 | }, 124 | }, 125 | }, 126 | } 127 | 128 | for _, tc := range testCases { 129 | t.Run(tc.name, func(t *testing.T) { 130 | setenvs(t, tc.envs) 131 | config, err := GetConfig() 132 | if tc.expectedError == "" { 133 | require.NoError(t, err) 134 | assert.Equal(t, tc.expectedConfig, config) 135 | } else { 136 | assert.EqualError(t, err, tc.expectedError) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func TestGetLogLevel(t *testing.T) { 143 | testCases := []struct { 144 | name string 145 | envs envs 146 | expectedLogLevel logrus.Level 147 | }{ 148 | { 149 | name: "Should return default log level when env is not set", 150 | expectedLogLevel: logrus.InfoLevel, 151 | }, 152 | { 153 | name: "Should return default log level when env has invalid value", 154 | envs: envs{"SCANNER_LOG_LEVEL": "unknown_level"}, 155 | expectedLogLevel: logrus.InfoLevel, 156 | }, 157 | { 158 | name: "Should return log level set as env", 159 | envs: envs{"SCANNER_LOG_LEVEL": "trace"}, 160 | expectedLogLevel: logrus.TraceLevel, 161 | }, 162 | } 163 | 164 | for _, tc := range testCases { 165 | t.Run(tc.name, func(t *testing.T) { 166 | setenvs(t, tc.envs) 167 | assert.Equal(t, tc.expectedLogLevel, GetLogLevel()) 168 | }) 169 | } 170 | } 171 | 172 | func setenvs(t *testing.T, envs envs) { 173 | t.Helper() 174 | os.Clearenv() 175 | for k, v := range envs { 176 | err := os.Setenv(k, v) 177 | require.NoError(t, err) 178 | } 179 | } 180 | 181 | func parseDuration(t *testing.T, s string) time.Duration { 182 | t.Helper() 183 | duration, err := time.ParseDuration(s) 184 | require.NoError(t, err) 185 | return duration 186 | } 187 | -------------------------------------------------------------------------------- /pkg/ext/ambassador.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | var ( 10 | DefaultAmbassador = &ambassador{} 11 | ) 12 | 13 | // File abstracts the few methods we need, so we can test without real files. 14 | type File interface { 15 | Name() string 16 | Read([]byte) (int, error) 17 | } 18 | 19 | // Ambassador the ambassador to the outside "world". Wraps methods that modify global state and hence make the code that 20 | // use them very hard to test. 21 | type Ambassador interface { 22 | Environ() []string 23 | LookPath(string) (string, error) 24 | RunCmd(cmd *exec.Cmd) ([]byte, int, error) 25 | TempFile(dir, pattern string) (File, error) 26 | Remove(name string) error 27 | } 28 | 29 | type ambassador struct { 30 | } 31 | 32 | func (a *ambassador) Environ() []string { 33 | return os.Environ() 34 | } 35 | 36 | func (a *ambassador) RunCmd(cmd *exec.Cmd) (output []byte, code int, err error) { 37 | output, err = cmd.CombinedOutput() 38 | code = cmd.ProcessState.ExitCode() 39 | return 40 | } 41 | 42 | func (a *ambassador) TempFile(dir, pattern string) (File, error) { 43 | return ioutil.TempFile(dir, pattern) 44 | } 45 | 46 | func (a *ambassador) Remove(name string) error { 47 | return os.Remove(name) 48 | } 49 | 50 | func (a *ambassador) LookPath(file string) (string, error) { 51 | return exec.LookPath(file) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/ext/ambassador_mock.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | "github.com/stretchr/testify/mock" 5 | "io" 6 | "os/exec" 7 | ) 8 | 9 | type FakeFile struct { 10 | name string 11 | reader io.Reader 12 | } 13 | 14 | // NewFakeFile constructs a new FakeFile with the given name and content. 15 | func NewFakeFile(name string, content io.Reader) *FakeFile { 16 | return &FakeFile{ 17 | name: name, 18 | reader: content, 19 | } 20 | } 21 | 22 | func (ff *FakeFile) Name() string { 23 | return ff.name 24 | } 25 | 26 | func (ff *FakeFile) Read(p []byte) (int, error) { 27 | return ff.reader.Read(p) 28 | } 29 | 30 | type MockAmbassador struct { 31 | mock.Mock 32 | } 33 | 34 | func NewMockAmbassador() *MockAmbassador { 35 | return &MockAmbassador{} 36 | } 37 | 38 | func (m *MockAmbassador) Environ() []string { 39 | args := m.Called() 40 | return args.Get(0).([]string) 41 | } 42 | 43 | func (m *MockAmbassador) LookPath(file string) (string, error) { 44 | args := m.Called(file) 45 | return args.String(0), args.Error(1) 46 | } 47 | 48 | func (m *MockAmbassador) RunCmd(cmd *exec.Cmd) ([]byte, int, error) { 49 | args := m.Called(cmd) 50 | return args.Get(0).([]byte), args.Int(1), args.Error(2) 51 | } 52 | 53 | func (m *MockAmbassador) TempFile(dir, pattern string) (file File, err error) { 54 | args := m.Called(dir, pattern) 55 | if arg := args.Get(0); arg != nil { 56 | file = arg.(File) 57 | } 58 | err = args.Error(1) 59 | return 60 | } 61 | 62 | func (m *MockAmbassador) Remove(name string) error { 63 | args := m.Called(name) 64 | return args.Error(0) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/ext/clock.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import "time" 4 | 5 | // Clock wraps the Now method. Introduced to allow replacing the global state with fixed clocks to facilitate testing. 6 | // Now returns the current time. 7 | type Clock interface { 8 | Now() time.Time 9 | } 10 | 11 | type systemClock struct { 12 | } 13 | 14 | func (c *systemClock) Now() time.Time { 15 | return time.Now() 16 | } 17 | 18 | func NewSystemClock() Clock { 19 | return &systemClock{} 20 | } 21 | 22 | type fixedClock struct { 23 | fixedTime time.Time 24 | } 25 | 26 | func (c *fixedClock) Now() time.Time { 27 | return c.fixedTime 28 | } 29 | 30 | func NewFixedClock(fixedTime time.Time) Clock { 31 | return &fixedClock{ 32 | fixedTime: fixedTime, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/harbor/model.go: -------------------------------------------------------------------------------- 1 | package harbor 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Severity represents the severity of a image/component in terms of vulnerability. 14 | type Severity int64 15 | 16 | // Sevxxx is the list of severity of image after scanning. 17 | const ( 18 | _ Severity = iota 19 | SevUnknown 20 | SevNegligible 21 | SevLow 22 | SevMedium 23 | SevHigh 24 | SevCritical 25 | ) 26 | 27 | func (s Severity) String() string { 28 | return severityToString[s] 29 | } 30 | 31 | var severityToString = map[Severity]string{ 32 | SevUnknown: "Unknown", 33 | SevNegligible: "Negligible", 34 | SevLow: "Low", 35 | SevMedium: "Medium", 36 | SevHigh: "High", 37 | SevCritical: "Critical", 38 | } 39 | 40 | var stringToSeverity = map[string]Severity{ 41 | "Unknown": SevUnknown, 42 | "Negligible": SevNegligible, 43 | "Low": SevLow, 44 | "Medium": SevMedium, 45 | "High": SevHigh, 46 | "Critical": SevCritical, 47 | } 48 | 49 | // MarshalJSON marshals the Severity enum value as a quoted JSON string. 50 | func (s Severity) MarshalJSON() ([]byte, error) { 51 | buffer := bytes.NewBufferString(`"`) 52 | buffer.WriteString(severityToString[s]) 53 | buffer.WriteString(`"`) 54 | return buffer.Bytes(), nil 55 | } 56 | 57 | // UnmarshalJSON unmarshals quoted JSON string to the Severity enum value. 58 | func (s *Severity) UnmarshalJSON(b []byte) error { 59 | var value string 60 | err := json.Unmarshal(b, &value) 61 | if err != nil { 62 | return err 63 | } 64 | *s = stringToSeverity[value] 65 | return nil 66 | } 67 | 68 | type Registry struct { 69 | URL string `json:"url"` 70 | Authorization string `json:"authorization"` 71 | } 72 | 73 | func (r Registry) GetBasicCredentials() (username, password string, err error) { 74 | tokens := strings.Split(r.Authorization, " ") 75 | if len(tokens) != 2 { 76 | err = fmt.Errorf("parsing authorization: expected got [%s]", r.Authorization) 77 | return 78 | } 79 | switch tokens[0] { 80 | case "Basic": 81 | return r.decodeBasicAuthentication(tokens[1]) 82 | } 83 | err = fmt.Errorf("unsupported authorization type: %s", tokens[0]) 84 | return 85 | } 86 | 87 | func (r Registry) decodeBasicAuthentication(value string) (username, password string, err error) { 88 | creds, err := base64.StdEncoding.DecodeString(value) 89 | if err != nil { 90 | return 91 | } 92 | tokens := strings.Split(string(creds), ":") 93 | if len(tokens) != 2 { 94 | err = errors.New("username and password not split by single colon") 95 | return 96 | } 97 | 98 | username = tokens[0] 99 | password = tokens[1] 100 | 101 | return 102 | } 103 | 104 | type Artifact struct { 105 | Repository string `json:"repository"` 106 | Digest string `json:"digest"` 107 | Tag string `json:"tag"` 108 | MimeType string `json:"mime_type,omitempty"` 109 | } 110 | 111 | type ScanRequest struct { 112 | Registry Registry `json:"registry"` 113 | Artifact Artifact `json:"artifact"` 114 | } 115 | 116 | type ScanResponse struct { 117 | ID string `json:"id"` 118 | } 119 | 120 | type ScanReport struct { 121 | GeneratedAt time.Time `json:"generated_at"` 122 | Artifact Artifact `json:"artifact"` 123 | Scanner Scanner `json:"scanner"` 124 | Severity Severity `json:"severity"` 125 | Vulnerabilities []VulnerabilityItem `json:"vulnerabilities"` 126 | } 127 | 128 | // VulnerabilityItem is an item in the vulnerability result returned by vulnerability details API. 129 | type VulnerabilityItem struct { 130 | ID string `json:"id"` 131 | Pkg string `json:"package"` 132 | Version string `json:"version"` 133 | FixVersion string `json:"fix_version,omitempty"` 134 | Severity Severity `json:"severity"` 135 | Description string `json:"description"` 136 | Links []string `json:"links"` 137 | } 138 | 139 | type ScannerAdapterMetadata struct { 140 | Scanner Scanner `json:"scanner"` 141 | Capabilities []Capability `json:"capabilities"` 142 | Properties map[string]string `json:"properties"` 143 | } 144 | 145 | type Scanner struct { 146 | Name string `json:"name"` 147 | Vendor string `json:"vendor"` 148 | Version string `json:"version"` 149 | } 150 | 151 | type Capability struct { 152 | ConsumesMIMETypes []string `json:"consumes_mime_types"` 153 | ProducesMIMETypes []string `json:"produces_mime_types"` 154 | } 155 | 156 | type Error struct { 157 | HTTPCode int `json:"-"` 158 | Message string `json:"message"` 159 | } 160 | -------------------------------------------------------------------------------- /pkg/harbor/model_test.go: -------------------------------------------------------------------------------- 1 | package harbor 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRegistry_GetBasicCredentials(t *testing.T) { 11 | testCases := []struct { 12 | Authorization string 13 | 14 | ExpectedUsername string 15 | ExpectedPassword string 16 | 17 | ExpectedError string 18 | }{ 19 | { 20 | Authorization: "", 21 | ExpectedError: "parsing authorization: expected got []", 22 | }, 23 | { 24 | Authorization: "Basic aGFyYm9yOnMzY3JldA==", 25 | ExpectedUsername: "harbor", 26 | ExpectedPassword: "s3cret", 27 | }, 28 | { 29 | Authorization: "Basic aGFyYm9yTmFtZQ==", 30 | ExpectedError: "username and password not split by single colon", 31 | }, 32 | { 33 | Authorization: "Basic invalidbase64", 34 | ExpectedError: "illegal base64 data at input byte 12", 35 | }, 36 | { 37 | Authorization: "APIKey 0123456789", 38 | ExpectedError: "unsupported authorization type: APIKey", 39 | }, 40 | } 41 | for _, tc := range testCases { 42 | t.Run(tc.Authorization, func(t *testing.T) { 43 | username, password, err := Registry{Authorization: tc.Authorization}.GetBasicCredentials() 44 | switch { 45 | case tc.ExpectedError != "": 46 | assert.EqualError(t, err, tc.ExpectedError) 47 | default: 48 | require.NoError(t, err) 49 | assert.Equal(t, tc.ExpectedUsername, username) 50 | assert.Equal(t, tc.ExpectedPassword, password) 51 | } 52 | 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/http/api/base_handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 7 | log "github.com/sirupsen/logrus" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | MimeTypeVersion = MimeTypeParams{"version": "1.0"} 14 | MimeTypeOCIImageManifest = MimeType{Type: "application", Subtype: "vnd.oci.image.manifest.v1+json"} 15 | MimeTypeDockerImageManifest = MimeType{Type: "application", Subtype: "vnd.docker.distribution.manifest.v2+json"} 16 | MimeTypeMetadata = MimeType{Type: "application", Subtype: "vnd.scanner.adapter.metadata+json", Params: MimeTypeVersion} 17 | MimeTypeHarborVulnerabilityReport = MimeType{Type: "application", Subtype: "vnd.scanner.adapter.vuln.report.harbor+json", Params: MimeTypeVersion} 18 | MimeTypeError = MimeType{Type: "application", Subtype: "vnd.scanner.adapter.error", Params: MimeTypeVersion} 19 | MimeTypeScanResponse = MimeType{Type: "application", Subtype: "vnd.scanner.adapter.scan.response+json", Params: MimeTypeVersion} 20 | MimeTypeScanReport = MimeType{Type: "application", Subtype: "vnd.scanner.adapter.vuln.report.harbor+json", Params: MimeTypeVersion} 21 | ) 22 | 23 | type MimeTypeParams map[string]string 24 | 25 | type MimeType struct { 26 | Type string 27 | Subtype string 28 | Params MimeTypeParams 29 | } 30 | 31 | func (mt MimeType) String() string { 32 | s := fmt.Sprintf("%s/%s", mt.Type, mt.Subtype) 33 | if len(mt.Params) == 0 { 34 | return s 35 | } 36 | params := make([]string, 0, len(mt.Params)) 37 | for k, v := range mt.Params { 38 | params = append(params, fmt.Sprintf("%s=%s", k, v)) 39 | } 40 | return fmt.Sprintf("%s; %s", s, strings.Join(params, ";")) 41 | } 42 | 43 | type BaseHandler struct { 44 | } 45 | 46 | func (h *BaseHandler) WriteJSON(res http.ResponseWriter, data interface{}, mimeType MimeType, statusCode int) { 47 | res.Header().Set("Content-Type", mimeType.String()) 48 | res.WriteHeader(statusCode) 49 | 50 | err := json.NewEncoder(res).Encode(data) 51 | if err != nil { 52 | log.WithError(err).Error("Error while writing JSON") 53 | h.SendInternalServerError(res) 54 | return 55 | } 56 | } 57 | 58 | func (h *BaseHandler) WriteJSONError(res http.ResponseWriter, err harbor.Error) { 59 | data := struct { 60 | Err harbor.Error `json:"error"` 61 | }{err} 62 | 63 | h.WriteJSON(res, data, MimeTypeError, err.HTTPCode) 64 | } 65 | 66 | func (h *BaseHandler) SendInternalServerError(res http.ResponseWriter) { 67 | http.Error(res, "Internal Server Error", http.StatusInternalServerError) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/http/api/base_handler_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestMimeType_String(t *testing.T) { 12 | testCases := []struct { 13 | mimeType MimeType 14 | expectedString string 15 | }{ 16 | { 17 | mimeType: MimeType{Type: "application", Subtype: "vnd.scanner.adapter.scan.request+json"}, 18 | expectedString: "application/vnd.scanner.adapter.scan.request+json", 19 | }, 20 | { 21 | mimeType: MimeType{Type: "application", Subtype: "vnd.scanner.adapter.scan.request+json", Params: MimeTypeParams{"version": "1.0"}}, 22 | expectedString: "application/vnd.scanner.adapter.scan.request+json; version=1.0", 23 | }, 24 | } 25 | 26 | for _, tc := range testCases { 27 | t.Run(tc.expectedString, func(t *testing.T) { 28 | assert.Equal(t, tc.expectedString, tc.mimeType.String()) 29 | }) 30 | } 31 | } 32 | 33 | func TestBaseHandler_WriteJSONError(t *testing.T) { 34 | recorder := httptest.NewRecorder() 35 | handler := &BaseHandler{} 36 | 37 | handler.WriteJSONError(recorder, harbor.Error{ 38 | HTTPCode: http.StatusBadRequest, 39 | Message: "Invalid request", 40 | }) 41 | 42 | assert.Equal(t, http.StatusBadRequest, recorder.Code) 43 | assert.JSONEq(t, `{"error":{"message":"Invalid request"}}`, recorder.Body.String()) 44 | } 45 | 46 | func TestBaseHandler_SendInternalServerError(t *testing.T) { 47 | recorder := httptest.NewRecorder() 48 | handler := &BaseHandler{} 49 | 50 | handler.SendInternalServerError(recorder) 51 | 52 | assert.Equal(t, http.StatusInternalServerError, recorder.Code) 53 | assert.Equal(t, "Internal Server Error\n", recorder.Body.String()) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/http/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | 8 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Server struct { 13 | config etc.API 14 | server *http.Server 15 | } 16 | 17 | func NewServer(config etc.API, handler http.Handler) (server *Server) { 18 | server = &Server{ 19 | config: config, 20 | server: &http.Server{ 21 | Handler: handler, 22 | Addr: config.Addr, 23 | ReadTimeout: config.ReadTimeout, 24 | WriteTimeout: config.WriteTimeout, 25 | IdleTimeout: config.IdleTimeout, 26 | }, 27 | } 28 | if config.IsTLSEnabled() { 29 | server.server.TLSConfig = &tls.Config{ 30 | MinVersion: tls.VersionTLS12, 31 | PreferServerCipherSuites: true, 32 | // The API server prefers elliptic curves which have assembly implementations 33 | // to ensure performance under heavy loads. 34 | CurvePreferences: []tls.CurveID{ 35 | tls.X25519, 36 | tls.CurveP256, 37 | }, 38 | // The API server only supports cipher suites which use ECDHE (forward secrecy) 39 | // and does not support weak cipher suites that use RC4, 3DES or CBC. 40 | CipherSuites: []uint16{ 41 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 42 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 43 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 44 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 45 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 46 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 47 | }, 48 | } 49 | } 50 | return 51 | } 52 | 53 | func (s *Server) ListenAndServe() { 54 | go func() { 55 | if err := s.listenAndServe(); err != http.ErrServerClosed { 56 | log.Fatalf("Error: %v", err) 57 | } 58 | log.Trace("API server stopped listening for incoming connections") 59 | }() 60 | } 61 | 62 | func (s *Server) listenAndServe() error { 63 | if s.config.IsTLSEnabled() { 64 | log.WithFields(log.Fields{ 65 | "certificate": s.config.TLSCertificate, 66 | "key": s.config.TLSKey, 67 | "addr": s.config.Addr, 68 | }).Debug("Starting API server with TLS") 69 | return s.server.ListenAndServeTLS(s.config.TLSCertificate, s.config.TLSKey) 70 | } 71 | log.WithField("addr", s.config.Addr).Warn("Starting API server without TLS") 72 | return s.server.ListenAndServe() 73 | } 74 | 75 | func (s *Server) Shutdown() { 76 | log.Trace("API server shutdown started") 77 | if err := s.server.Shutdown(context.Background()); err != nil { 78 | log.WithError(err).Error("Error while shutting down API server") 79 | } 80 | log.Trace("API server shutdown completed") 81 | } 82 | -------------------------------------------------------------------------------- /pkg/http/api/server_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | -------------------------------------------------------------------------------- /pkg/http/api/v1/handler.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 12 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/http/api" 13 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 14 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence" 15 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/scanner" 16 | "github.com/gorilla/mux" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | const ( 22 | pathVarScanRequestID = "scan_request_id" 23 | ) 24 | 25 | type handler struct { 26 | info etc.BuildInfo 27 | config etc.Config 28 | api.BaseHandler 29 | 30 | enqueuer scanner.Enqueuer 31 | store persistence.Store 32 | } 33 | 34 | func NewAPIHandler(info etc.BuildInfo, config etc.Config, enqueuer scanner.Enqueuer, store persistence.Store) http.Handler { 35 | handler := &handler{ 36 | info: info, 37 | config: config, 38 | enqueuer: enqueuer, 39 | store: store, 40 | } 41 | 42 | router := mux.NewRouter() 43 | router.Use(handler.logRequest) 44 | 45 | apiV1Router := router.PathPrefix("/api/v1").Subrouter() 46 | 47 | apiV1Router.Methods(http.MethodGet).Path("/metadata").HandlerFunc(handler.getMetadata) 48 | apiV1Router.Methods(http.MethodPost).Path("/scan").HandlerFunc(handler.acceptScanRequest) 49 | apiV1Router.Methods(http.MethodGet).Path("/scan/{scan_request_id}/report").HandlerFunc(handler.getScanReport) 50 | 51 | probeRouter := router.PathPrefix("/probe").Subrouter() 52 | probeRouter.Methods(http.MethodGet).Path("/healthy").HandlerFunc(handler.getHealthy) 53 | probeRouter.Methods(http.MethodGet).Path("/ready").HandlerFunc(handler.getReady) 54 | 55 | router.Methods(http.MethodGet).Path("/metrics").Handler(promhttp.Handler()) 56 | 57 | return router 58 | } 59 | 60 | func (h *handler) logRequest(next http.Handler) http.Handler { 61 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 | log.WithFields(log.Fields{ 63 | "remote_addr": r.RemoteAddr, 64 | "proto": r.Proto, 65 | "method": r.Method, 66 | "request_uri": r.URL.RequestURI(), 67 | }).Trace("Handling request") 68 | next.ServeHTTP(w, r) 69 | }) 70 | } 71 | 72 | func (h *handler) acceptScanRequest(res http.ResponseWriter, req *http.Request) { 73 | scanRequest := harbor.ScanRequest{} 74 | err := json.NewDecoder(req.Body).Decode(&scanRequest) 75 | if err != nil { 76 | log.WithError(err).Error("Error while unmarshalling scan request") 77 | h.WriteJSONError(res, harbor.Error{ 78 | HTTPCode: http.StatusBadRequest, 79 | Message: fmt.Sprintf("unmarshalling scan request: %s", err.Error()), 80 | }) 81 | return 82 | } 83 | 84 | if validationError := h.validate(scanRequest); validationError != nil { 85 | log.Errorf("Error while validating scan request: %s", validationError.Message) 86 | h.WriteJSONError(res, *validationError) 87 | return 88 | } 89 | 90 | jobID, err := h.enqueuer.Enqueue(scanRequest) 91 | if err != nil { 92 | log.WithError(err).Error("Error while enqueueing scan request") 93 | h.WriteJSONError(res, harbor.Error{ 94 | HTTPCode: http.StatusInternalServerError, 95 | Message: fmt.Sprintf("enqueueing scan request: %s", err.Error()), 96 | }) 97 | return 98 | } 99 | 100 | h.WriteJSON(res, harbor.ScanResponse{ID: jobID}, api.MimeTypeScanResponse, http.StatusAccepted) 101 | } 102 | 103 | func (h *handler) validate(req harbor.ScanRequest) *harbor.Error { 104 | if req.Registry.URL == "" { 105 | return &harbor.Error{ 106 | HTTPCode: http.StatusUnprocessableEntity, 107 | Message: "missing registry.url", 108 | } 109 | } 110 | 111 | _, err := url.ParseRequestURI(req.Registry.URL) 112 | if err != nil { 113 | return &harbor.Error{ 114 | HTTPCode: http.StatusUnprocessableEntity, 115 | Message: "invalid registry.url", 116 | } 117 | } 118 | 119 | if req.Artifact.Repository == "" { 120 | return &harbor.Error{ 121 | HTTPCode: http.StatusUnprocessableEntity, 122 | Message: "missing artifact.repository", 123 | } 124 | } 125 | 126 | if req.Artifact.Digest == "" { 127 | return &harbor.Error{ 128 | HTTPCode: http.StatusUnprocessableEntity, 129 | Message: "missing artifact.digest", 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func (h *handler) getScanReport(res http.ResponseWriter, req *http.Request) { 137 | vars := mux.Vars(req) 138 | jobID, ok := vars[pathVarScanRequestID] 139 | if !ok { 140 | log.Error("Error while parsing `scan_request_id` path variable") 141 | h.WriteJSONError(res, harbor.Error{ 142 | HTTPCode: http.StatusBadRequest, 143 | Message: "missing scan_request_id", 144 | }) 145 | return 146 | } 147 | 148 | reqLog := log.WithField("scan_job_id", jobID) 149 | 150 | scanJob, err := h.store.Get(jobID) 151 | if err != nil { 152 | h.WriteJSONError(res, harbor.Error{ 153 | HTTPCode: http.StatusInternalServerError, 154 | Message: fmt.Sprintf("getting scan job: %v", err), 155 | }) 156 | return 157 | } 158 | 159 | if scanJob == nil { 160 | reqLog.Error("Cannot find scan job") 161 | h.WriteJSONError(res, harbor.Error{ 162 | HTTPCode: http.StatusNotFound, 163 | Message: fmt.Sprintf("cannot find scan job: %v", jobID), 164 | }) 165 | return 166 | } 167 | 168 | if scanJob.Status == job.Pending || scanJob.Status == job.Running { 169 | reqLog.WithField("scan_job_status", scanJob.Status.String()).Debug("Scan job has not finished yet") 170 | res.Header().Add("Location", req.URL.String()) 171 | res.WriteHeader(http.StatusFound) 172 | return 173 | } 174 | 175 | if scanJob.Status == job.Failed { 176 | reqLog.WithField(log.ErrorKey, scanJob.Error).Error("Scan job failed") 177 | h.WriteJSONError(res, harbor.Error{ 178 | HTTPCode: http.StatusInternalServerError, 179 | Message: scanJob.Error, 180 | }) 181 | return 182 | } 183 | 184 | if scanJob.Status != job.Finished { 185 | reqLog.WithField("scan_job_status", scanJob.Status).Error("Unexpected scan job status") 186 | h.WriteJSONError(res, harbor.Error{ 187 | HTTPCode: http.StatusInternalServerError, 188 | Message: fmt.Sprintf("unexpected status %v of scan job %v", scanJob.Status, scanJob.ID), 189 | }) 190 | return 191 | } 192 | 193 | h.WriteJSON(res, scanJob.Report, api.MimeTypeScanReport, http.StatusOK) 194 | } 195 | 196 | func (h *handler) getMetadata(res http.ResponseWriter, _ *http.Request) { 197 | metadata := harbor.ScannerAdapterMetadata{ 198 | Scanner: etc.GetScannerMetadata(), 199 | Capabilities: []harbor.Capability{ 200 | { 201 | ConsumesMIMETypes: []string{ 202 | api.MimeTypeOCIImageManifest.String(), 203 | api.MimeTypeDockerImageManifest.String(), 204 | }, 205 | ProducesMIMETypes: []string{ 206 | api.MimeTypeHarborVulnerabilityReport.String(), 207 | }, 208 | }, 209 | }, 210 | Properties: map[string]string{ 211 | "harbor.scanner-adapter/scanner-type": "os-package-vulnerability", 212 | "org.label-schema.version": h.info.Version, 213 | "org.label-schema.build-date": h.info.Date, 214 | "org.label-schema.vcs-ref": h.info.Commit, 215 | "org.label-schema.vcs": "https://github.com/aquasecurity/harbor-scanner-aqua", 216 | "env.SCANNER_AQUA_HOST": h.config.AquaCSP.Host, 217 | "env.SCANNER_AQUA_REGISTRY": h.config.AquaCSP.Registry, 218 | "env.SCANNER_AQUA_REPORTS_DIR": h.config.AquaCSP.ReportsDir, 219 | "env.SCANNER_AQUA_USE_IMAGE_TAG": strconv.FormatBool(h.config.AquaCSP.UseImageTag), 220 | "env.SCANNER_CLI_NO_VERIFY": strconv.FormatBool(h.config.AquaCSP.ScannerCLINoVerify), 221 | "env.SCANNER_CLI_SHOW_NEGLIGIBLE": strconv.FormatBool(h.config.AquaCSP.ScannerCLIShowNegligible), 222 | "env.SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS": strconv.FormatBool(h.config.AquaCSP.ScannerCLIOverrideRegistryCredentials), 223 | "env.SCANNER_CLI_DIRECT_CC": strconv.FormatBool(h.config.AquaCSP.ScannerCLIDirectCC), 224 | "env.SCANNER_CLI_REGISTER_IMAGES": string(h.config.AquaCSP.ScannerCLIRegisterImages), 225 | }, 226 | } 227 | h.WriteJSON(res, metadata, api.MimeTypeMetadata, http.StatusOK) 228 | } 229 | 230 | func (h *handler) getHealthy(res http.ResponseWriter, _ *http.Request) { 231 | res.WriteHeader(http.StatusOK) 232 | } 233 | 234 | func (h *handler) getReady(res http.ResponseWriter, _ *http.Request) { 235 | res.WriteHeader(http.StatusOK) 236 | } 237 | -------------------------------------------------------------------------------- /pkg/http/api/v1/handler_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence/mock" 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/scanner" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestHandler(t *testing.T) { 17 | 18 | buildInfo := etc.BuildInfo{Version: "v0.0.5", Commit: "abc", Date: "20-04-1319T13:45:00"} 19 | config, err := etc.GetConfig() 20 | require.NoError(t, err) 21 | enqueuer := &scanner.MockEnqueuer{} 22 | store := &mock.Store{} 23 | handler := NewAPIHandler(buildInfo, config, enqueuer, store) 24 | 25 | ts := httptest.NewServer(handler) 26 | defer ts.Close() 27 | 28 | t.Run("GET /api/v1/metadata", func(t *testing.T) { 29 | rs, err := ts.Client().Get(ts.URL + "/api/v1/metadata") 30 | require.NoError(t, err) 31 | assert.Equal(t, http.StatusOK, rs.StatusCode) 32 | 33 | bodyBytes, err := ioutil.ReadAll(rs.Body) 34 | require.NoError(t, err) 35 | 36 | assert.JSONEq(t, `{ 37 | "scanner": { 38 | "name": "Aqua Enterprise", 39 | "vendor": "Aqua Security", 40 | "version": "Unknown" 41 | }, 42 | "capabilities": [ 43 | { 44 | "consumes_mime_types": [ 45 | "application/vnd.oci.image.manifest.v1+json", 46 | "application/vnd.docker.distribution.manifest.v2+json" 47 | ], 48 | "produces_mime_types": [ 49 | "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0" 50 | ] 51 | } 52 | ], 53 | "properties": { 54 | "harbor.scanner-adapter/scanner-type": "os-package-vulnerability", 55 | 56 | "org.label-schema.version": "v0.0.5", 57 | "org.label-schema.build-date": "20-04-1319T13:45:00", 58 | "org.label-schema.vcs-ref": "abc", 59 | "org.label-schema.vcs": "https://github.com/aquasecurity/harbor-scanner-aqua", 60 | 61 | "env.SCANNER_AQUA_HOST": "http://csp-console-svc.aqua:8080", 62 | "env.SCANNER_AQUA_REGISTRY": "Harbor", 63 | "env.SCANNER_AQUA_REPORTS_DIR": "/var/lib/scanner/reports", 64 | "env.SCANNER_AQUA_USE_IMAGE_TAG": "true", 65 | "env.SCANNER_CLI_NO_VERIFY": "false", 66 | "env.SCANNER_CLI_SHOW_NEGLIGIBLE": "true", 67 | "env.SCANNER_CLI_DIRECT_CC": "false", 68 | "env.SCANNER_CLI_REGISTER_IMAGES": "Never", 69 | 70 | "env.SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS": "false" 71 | } 72 | }`, string(bodyBytes)) 73 | }) 74 | 75 | t.Run("GET /probe/healthy", func(t *testing.T) { 76 | rs, err := ts.Client().Get(ts.URL + "/probe/healthy") 77 | require.NoError(t, err) 78 | assert.Equal(t, http.StatusOK, rs.StatusCode) 79 | }) 80 | 81 | t.Run("GET /probe/ready", func(t *testing.T) { 82 | rs, err := ts.Client().Get(ts.URL + "/probe/ready") 83 | require.NoError(t, err) 84 | assert.Equal(t, http.StatusOK, rs.StatusCode) 85 | }) 86 | 87 | t.Run("GET /metrics", func(t *testing.T) { 88 | rs, err := ts.Client().Get(ts.URL + "/metrics") 89 | require.NoError(t, err) 90 | assert.Equal(t, http.StatusOK, rs.StatusCode) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/job/job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 4 | 5 | type Status int 6 | 7 | const ( 8 | Pending Status = iota 9 | Running 10 | Finished 11 | Failed 12 | ) 13 | 14 | func (s Status) String() string { 15 | if s < 0 || s > 3 { 16 | return "Unknown" 17 | } 18 | return [...]string{"Pending", "Running", "Finished", "Failed"}[s] 19 | } 20 | 21 | type ScanJob struct { 22 | ID string `json:"id"` 23 | Status Status `json:"status"` 24 | Error string `json:"error"` 25 | Report harbor.ScanReport `json:"report"` 26 | } 27 | -------------------------------------------------------------------------------- /pkg/persistence/mock/store.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type Store struct { 10 | mock.Mock 11 | } 12 | 13 | func (s *Store) Create(scanJob job.ScanJob) error { 14 | args := s.Called(scanJob) 15 | return args.Error(0) 16 | } 17 | 18 | func (s *Store) Get(scanJobID string) (*job.ScanJob, error) { 19 | args := s.Called(scanJobID) 20 | return args.Get(0).(*job.ScanJob), args.Error(1) 21 | } 22 | 23 | func (s *Store) UpdateStatus(scanJobID string, newStatus job.Status, error ...string) error { 24 | args := s.Called(scanJobID, newStatus, error) 25 | return args.Error(0) 26 | } 27 | 28 | func (s *Store) UpdateReport(scanJobID string, report harbor.ScanReport) error { 29 | args := s.Called(scanJobID, report) 30 | return args.Error(0) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/persistence/redis/store.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 8 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 9 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 10 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence" 11 | "github.com/gomodule/redigo/redis" 12 | log "github.com/sirupsen/logrus" 13 | "golang.org/x/xerrors" 14 | ) 15 | 16 | type store struct { 17 | cfg etc.RedisStore 18 | pool *redis.Pool 19 | } 20 | 21 | func NewStore(cfg etc.RedisStore, pool *redis.Pool) persistence.Store { 22 | return &store{ 23 | cfg: cfg, 24 | pool: pool, 25 | } 26 | } 27 | 28 | func (s *store) Create(scanJob job.ScanJob) error { 29 | conn := s.pool.Get() 30 | defer s.close(conn) 31 | 32 | bytes, err := json.Marshal(scanJob) 33 | if err != nil { 34 | return xerrors.Errorf("marshalling scan job: %w", err) 35 | } 36 | 37 | key := s.getKeyForScanJob(scanJob.ID) 38 | 39 | log.WithFields(log.Fields{ 40 | "scan_job_id": scanJob.ID, 41 | "scan_job_status": scanJob.Status.String(), 42 | "redis_key": key, 43 | "expire": s.cfg.ScanJobTTL.Seconds(), 44 | }).Trace("Creating scan job") 45 | 46 | _, err = conn.Do("SET", key, string(bytes), "NX", "EX", int(s.cfg.ScanJobTTL.Seconds())) 47 | if err != nil { 48 | return xerrors.Errorf("creating scan job: %w", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (s *store) update(scanJob job.ScanJob) error { 55 | conn := s.pool.Get() 56 | defer s.close(conn) 57 | 58 | bytes, err := json.Marshal(scanJob) 59 | if err != nil { 60 | return xerrors.Errorf("marshalling scan job: %w", err) 61 | } 62 | 63 | key := s.getKeyForScanJob(scanJob.ID) 64 | 65 | log.WithFields(log.Fields{ 66 | "scan_job_id": scanJob.ID, 67 | "scan_job_status": scanJob.Status.String(), 68 | "redis_key": key, 69 | "expire": s.cfg.ScanJobTTL.Seconds(), 70 | }).Debug("Updating scan job") 71 | 72 | _, err = conn.Do("SET", key, string(bytes), "XX", "EX", int(s.cfg.ScanJobTTL.Seconds())) 73 | if err != nil { 74 | return xerrors.Errorf("updating scan job: %w", err) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (s *store) Get(scanJobID string) (*job.ScanJob, error) { 81 | conn := s.pool.Get() 82 | defer s.close(conn) 83 | 84 | key := s.getKeyForScanJob(scanJobID) 85 | value, err := redis.String(conn.Do("GET", key)) 86 | if err != nil { 87 | if err == redis.ErrNil { 88 | return nil, nil 89 | } 90 | return nil, err 91 | } 92 | 93 | var scanJob job.ScanJob 94 | err = json.Unmarshal([]byte(value), &scanJob) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return &scanJob, nil 100 | } 101 | 102 | func (s *store) UpdateStatus(scanJobID string, newStatus job.Status, error ...string) error { 103 | log.WithFields(log.Fields{ 104 | "scan_job_id": scanJobID, 105 | "new_status": newStatus.String(), 106 | }).Trace("Updating status for scan job") 107 | 108 | scanJob, err := s.Get(scanJobID) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | scanJob.Status = newStatus 114 | if len(error) > 0 { 115 | scanJob.Error = error[0] 116 | } 117 | 118 | return s.update(*scanJob) 119 | } 120 | 121 | func (s *store) UpdateReport(scanJobID string, report harbor.ScanReport) error { 122 | log.WithFields(log.Fields{ 123 | "scan_job_id": scanJobID, 124 | }).Trace("Updating reports for scan job") 125 | 126 | scanJob, err := s.Get(scanJobID) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | scanJob.Report = report 132 | return s.update(*scanJob) 133 | } 134 | 135 | func (s *store) getKeyForScanJob(scanJobID string) string { 136 | return fmt.Sprintf("%s:scan-job:%s", s.cfg.Namespace, scanJobID) 137 | } 138 | 139 | func (s *store) close(conn redis.Conn) { 140 | err := conn.Close() 141 | if err != nil { 142 | log.WithError(err).Error("Error while closing connection") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pkg/persistence/store.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 6 | ) 7 | 8 | // Store defines methods for persisting ScanJobs and associated ScanReports. 9 | type Store interface { 10 | Create(scanJob job.ScanJob) error 11 | Get(scanJobID string) (*job.ScanJob, error) 12 | UpdateStatus(scanJobID string, newStatus job.Status, error ...string) error 13 | UpdateReport(scanJobID string, reports harbor.ScanReport) error 14 | } 15 | -------------------------------------------------------------------------------- /pkg/redisx/pool.go: -------------------------------------------------------------------------------- 1 | package redisx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/FZambia/sentinel" 12 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 13 | "github.com/gomodule/redigo/redis" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // NewPool constructs a redis.Pool with the specified configuration. 18 | // 19 | // The URI scheme currently supports connections to a standalone Redis server, 20 | // i.e. `redis://user:password@host:port/db-number`. 21 | func NewPool(config etc.RedisPool) (pool *redis.Pool, err error) { 22 | configURL, err := url.Parse(config.URL) 23 | if err != nil { 24 | err = fmt.Errorf("invalid redis URL: %s", err) 25 | return 26 | } 27 | 28 | switch configURL.Scheme { 29 | case "redis": 30 | pool = newInstancePool(config) 31 | case "redis+sentinel": 32 | return newSentinelPool(configURL, config) 33 | default: 34 | err = fmt.Errorf("invalid redis URL scheme: %s", configURL.Scheme) 35 | } 36 | return 37 | } 38 | 39 | // redis://user:password@host:port/db-number 40 | func newInstancePool(config etc.RedisPool) *redis.Pool { 41 | return &redis.Pool{ 42 | Dial: func() (redis.Conn, error) { 43 | log.WithField("url", config.URL).Trace("Connecting to Redis") 44 | return redis.DialURL(config.URL) 45 | }, 46 | MaxIdle: config.MaxIdle, 47 | MaxActive: config.MaxActive, 48 | IdleTimeout: config.IdleTimeout, 49 | Wait: true, 50 | } 51 | } 52 | 53 | // redis+sentinel://user:password@sentinel_host1:port1,sentinel_host2:port2/monitor-name/db-number 54 | func newSentinelPool(configURL *url.URL, config etc.RedisPool) (pool *redis.Pool, err error) { 55 | log.Trace("Constructing connection pool for Redis Sentinel") 56 | sentinelURL, err := ParseSentinelURL(configURL) 57 | if err != nil { 58 | return 59 | } 60 | 61 | var commonOpts []redis.DialOption 62 | if config.ConnectionTimeout > 0 { 63 | commonOpts = append(commonOpts, redis.DialConnectTimeout(config.ConnectionTimeout)) 64 | } 65 | if config.ReadTimeout > 0 { 66 | commonOpts = append(commonOpts, redis.DialReadTimeout(config.ReadTimeout)) 67 | } 68 | if config.WriteTimeout > 0 { 69 | commonOpts = append(commonOpts, redis.DialWriteTimeout(config.WriteTimeout)) 70 | } 71 | 72 | sentinelOpts := commonOpts 73 | 74 | sntnl := &sentinel.Sentinel{ 75 | Addrs: sentinelURL.Addrs, 76 | MasterName: sentinelURL.MonitorName, 77 | Dial: func(addr string) (conn redis.Conn, err error) { 78 | log.WithField("addr", addr).Trace("Connecting to Redis sentinel") 79 | conn, err = redis.Dial("tcp", addr, sentinelOpts...) 80 | if err != nil { 81 | return 82 | } 83 | return 84 | }, 85 | } 86 | 87 | redisOpts := commonOpts 88 | 89 | redisOpts = append(redisOpts, redis.DialDatabase(sentinelURL.Database)) 90 | redisOpts = append(redisOpts, redis.DialPassword(sentinelURL.Password)) 91 | 92 | pool = &redis.Pool{ 93 | Dial: func() (conn redis.Conn, err error) { 94 | masterAddr, err := sntnl.MasterAddr() 95 | if err != nil { 96 | return 97 | } 98 | log.WithField("addr", masterAddr).Trace("Connecting to Redis master") 99 | return redis.Dial("tcp", masterAddr, redisOpts...) 100 | }, 101 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 102 | if time.Since(t) < time.Minute { 103 | return nil 104 | } 105 | log.Trace("Testing connection to Redis master on borrow") 106 | if !sentinel.TestRole(c, "master") { 107 | return errors.New("role check failed") 108 | } 109 | return nil 110 | }, 111 | MaxIdle: config.MaxIdle, 112 | MaxActive: config.MaxActive, 113 | IdleTimeout: config.IdleTimeout, 114 | Wait: true, 115 | } 116 | return 117 | } 118 | 119 | type SentinelURL struct { 120 | Password string 121 | Addrs []string 122 | MonitorName string 123 | Database int 124 | } 125 | 126 | func ParseSentinelURL(configURL *url.URL) (sentinelURL SentinelURL, err error) { 127 | ps := strings.Split(configURL.Path, "/") 128 | if len(ps) < 2 { 129 | err = fmt.Errorf("invalid redis sentinel URL: no master name") 130 | return 131 | } 132 | 133 | if user := configURL.User; user != nil { 134 | if password, set := user.Password(); set { 135 | sentinelURL.Password = password 136 | } 137 | } 138 | 139 | sentinelURL.Addrs = strings.Split(configURL.Host, ",") 140 | sentinelURL.MonitorName = ps[1] 141 | 142 | if len(ps) > 2 { 143 | sentinelURL.Database, err = strconv.Atoi(ps[2]) 144 | if err != nil { 145 | err = fmt.Errorf("invalid redis sentinel URL: invalid database number: %s", ps[2]) 146 | return 147 | } 148 | } 149 | 150 | return 151 | } 152 | -------------------------------------------------------------------------------- /pkg/redisx/pool_test.go: -------------------------------------------------------------------------------- 1 | package redisx 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGetPool(t *testing.T) { 13 | 14 | t.Run("Should return error when configured to connect to secure redis", func(t *testing.T) { 15 | _, err := NewPool(etc.RedisPool{ 16 | URL: "rediss://hostname:6379", 17 | }) 18 | assert.EqualError(t, err, "invalid redis URL scheme: rediss") 19 | }) 20 | 21 | t.Run("Should return error when configured with unsupported url scheme", func(t *testing.T) { 22 | _, err := NewPool(etc.RedisPool{ 23 | URL: "https://hostname:6379", 24 | }) 25 | assert.EqualError(t, err, "invalid redis URL scheme: https") 26 | }) 27 | 28 | } 29 | 30 | func TestParseSentinelURL(t *testing.T) { 31 | testCases := []struct { 32 | configURL string 33 | expectedSentinelURL SentinelURL 34 | expectedError string 35 | }{ 36 | { 37 | configURL: "redis+sentinel://harbor:s3cret@somehost:26379,otherhost:26479/mymaster/3", 38 | expectedSentinelURL: SentinelURL{ 39 | Password: "s3cret", 40 | Addrs: []string{ 41 | "somehost:26379", 42 | "otherhost:26479", 43 | }, 44 | MonitorName: "mymaster", 45 | Database: 3, 46 | }, 47 | }, 48 | { 49 | configURL: "redis+sentinel://:s3cret@somehost:26379,otherhost:26479/mymaster/5", 50 | expectedSentinelURL: SentinelURL{ 51 | Password: "s3cret", 52 | Addrs: []string{ 53 | "somehost:26379", 54 | "otherhost:26479", 55 | }, 56 | MonitorName: "mymaster", 57 | Database: 5, 58 | }, 59 | }, 60 | { 61 | configURL: "redis+sentinel://:s3cret@somehost:26379,otherhost:26479/mymaster", 62 | expectedSentinelURL: SentinelURL{ 63 | Password: "s3cret", 64 | Addrs: []string{ 65 | "somehost:26379", 66 | "otherhost:26479", 67 | }, 68 | MonitorName: "mymaster", 69 | Database: 0, 70 | }, 71 | }, 72 | { 73 | configURL: "redis+sentinel://:s3cret@somehost:26379,otherhost:26479/mymaster/X", 74 | expectedError: "invalid redis sentinel URL: invalid database number: X", 75 | }, 76 | { 77 | configURL: "redis+sentinel://:s3cret@somehost:26379,otherhost:26479", 78 | expectedError: "invalid redis sentinel URL: no master name", 79 | }, 80 | } 81 | for _, tc := range testCases { 82 | t.Run(tc.configURL, func(t *testing.T) { 83 | configURL, err := url.Parse(tc.configURL) 84 | require.NoError(t, err) 85 | 86 | sentinelURL, err := ParseSentinelURL(configURL) 87 | 88 | switch { 89 | case tc.expectedError == "": 90 | require.NoError(t, err) 91 | assert.Equal(t, tc.expectedSentinelURL, sentinelURL) 92 | default: 93 | assert.EqualError(t, err, tc.expectedError) 94 | } 95 | }) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /pkg/scanner/adapter.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 8 | ) 9 | 10 | type Adapter interface { 11 | Scan(req harbor.ScanRequest) (harbor.ScanReport, error) 12 | } 13 | 14 | type adapter struct { 15 | command aqua.Command 16 | transformer Transformer 17 | } 18 | 19 | func NewAdapter(command aqua.Command, transformer Transformer) Adapter { 20 | return &adapter{ 21 | command: command, 22 | transformer: transformer, 23 | } 24 | } 25 | 26 | func (s *adapter) Scan(req harbor.ScanRequest) (harborReport harbor.ScanReport, err error) { 27 | username, password, err := req.Registry.GetBasicCredentials() 28 | if err != nil { 29 | err = fmt.Errorf("getting basic credentials from scan request: %w", err) 30 | return 31 | } 32 | 33 | aquaReport, err := s.command.Scan(aqua.ImageRef{ 34 | Repository: req.Artifact.Repository, 35 | Tag: req.Artifact.Tag, 36 | Digest: req.Artifact.Digest, 37 | Auth: aqua.RegistryAuth{ 38 | Username: username, 39 | Password: password, 40 | }, 41 | }) 42 | if err != nil { 43 | return 44 | } 45 | harborReport = s.transformer.Transform(req.Artifact, aquaReport) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /pkg/scanner/adapter_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var NoError error 13 | 14 | func TestAdapter_Scan(t *testing.T) { 15 | 16 | t.Run("Should return error when getting registry credentials fails", func(t *testing.T) { 17 | command := &aqua.MockCommand{} 18 | transformer := &MockTransformer{} 19 | 20 | scanRequest := harbor.ScanRequest{ 21 | Registry: harbor.Registry{ 22 | Authorization: "Bearer 0123456789", 23 | }, 24 | } 25 | 26 | _, err := NewAdapter(command, transformer).Scan(scanRequest) 27 | assert.EqualError(t, err, "getting basic credentials from scan request: unsupported authorization type: Bearer") 28 | 29 | command.AssertExpectations(t) 30 | transformer.AssertExpectations(t) 31 | }) 32 | 33 | t.Run("Should return Harbor report", func(t *testing.T) { 34 | command := &aqua.MockCommand{} 35 | transformer := &MockTransformer{} 36 | 37 | artifact := harbor.Artifact{ 38 | Repository: "library/golang", 39 | Tag: "1.12.4", 40 | } 41 | scanRequest := harbor.ScanRequest{ 42 | Registry: harbor.Registry{ 43 | Authorization: "Basic cm9ib3ROYW1lOnJvYm90UGFzc3dvcmQ=", 44 | }, 45 | Artifact: artifact, 46 | } 47 | imageRef := aqua.ImageRef{ 48 | Repository: "library/golang", 49 | Tag: "1.12.4", 50 | Auth: aqua.RegistryAuth{ 51 | Username: "robotName", 52 | Password: "robotPassword", 53 | }, 54 | } 55 | 56 | aquaReport := aqua.ScanReport{} 57 | harborReport := harbor.ScanReport{} 58 | 59 | command.On("Scan", imageRef).Return(aquaReport, NoError) 60 | transformer.On("Transform", artifact, aquaReport).Return(harborReport) 61 | 62 | r, err := NewAdapter(command, transformer).Scan(scanRequest) 63 | require.NoError(t, err) 64 | require.Equal(t, harborReport, r) 65 | 66 | command.AssertExpectations(t) 67 | transformer.AssertExpectations(t) 68 | }) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /pkg/scanner/enqueuer.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence" 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/work" 8 | "github.com/google/uuid" 9 | "golang.org/x/xerrors" 10 | ) 11 | 12 | // Enqueuer wraps the Enqueue method. 13 | // Enqueue enqueues the specify ScanRequest for async processing and returns the async job's identifier. 14 | type Enqueuer interface { 15 | Enqueue(request harbor.ScanRequest) (string, error) 16 | } 17 | 18 | // NewEnqueuer constructs the default Enqueuer. 19 | func NewEnqueuer(pool *work.Pool, adapter Adapter, store persistence.Store) Enqueuer { 20 | return &enqueuer{ 21 | pool: pool, 22 | adapter: adapter, 23 | store: store, 24 | } 25 | } 26 | 27 | type enqueuer struct { 28 | store persistence.Store 29 | pool *work.Pool 30 | adapter Adapter 31 | } 32 | 33 | func (e *enqueuer) Enqueue(request harbor.ScanRequest) (string, error) { 34 | jobID := uuid.New().String() 35 | err := e.store.Create(job.ScanJob{ 36 | ID: jobID, 37 | Status: job.Pending}, 38 | ) 39 | if err != nil { 40 | return "", xerrors.Errorf("creating scan job: %w", err) 41 | } 42 | e.pool.Run(&worker{ 43 | store: e.store, 44 | adapter: e.adapter, 45 | jobID: jobID, 46 | request: request, 47 | }) 48 | return jobID, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/scanner/enqueuer_mock.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/stretchr/testify/mock" 6 | ) 7 | 8 | type MockEnqueuer struct { 9 | mock.Mock 10 | } 11 | 12 | func (e *MockEnqueuer) Enqueue(request harbor.ScanRequest) (string, error) { 13 | args := e.Called(request) 14 | return args.String(0), args.Error(1) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/scanner/enqueuer_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | -------------------------------------------------------------------------------- /pkg/scanner/transformer.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/ext" 7 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Transformer interface { 12 | Transform(artifact harbor.Artifact, source aqua.ScanReport) harbor.ScanReport 13 | } 14 | 15 | func NewTransformer(clock ext.Clock) Transformer { 16 | return &transformer{ 17 | clock: clock, 18 | } 19 | } 20 | 21 | type transformer struct { 22 | clock ext.Clock 23 | } 24 | 25 | func (t *transformer) Transform(artifact harbor.Artifact, source aqua.ScanReport) harbor.ScanReport { 26 | log.WithFields(log.Fields{ 27 | "digest": source.Digest, 28 | "image": source.Image, 29 | "summary": source.Summary, 30 | "scan_options": source.ScanOptions, 31 | "changed_results": source.ChangedResults, 32 | "partial_results": source.PartialResults, 33 | }).Debug("Transforming scan report") 34 | var items []harbor.VulnerabilityItem 35 | 36 | for _, resourceScan := range source.Resources { 37 | var pkg string 38 | switch resourceScan.Resource.Type { 39 | case aqua.Library: 40 | pkg = resourceScan.Resource.Path 41 | case aqua.Package: 42 | pkg = resourceScan.Resource.Name 43 | default: 44 | pkg = resourceScan.Resource.Name 45 | } 46 | 47 | for _, vln := range resourceScan.Vulnerabilities { 48 | items = append(items, harbor.VulnerabilityItem{ 49 | ID: vln.Name, 50 | Pkg: pkg, 51 | Version: resourceScan.Resource.Version, 52 | FixVersion: vln.FixVersion, 53 | Severity: t.getHarborSeverity(vln), 54 | Description: vln.Description, 55 | Links: t.toLinks(vln), 56 | }) 57 | } 58 | } 59 | 60 | return harbor.ScanReport{ 61 | GeneratedAt: t.clock.Now(), 62 | Scanner: etc.GetScannerMetadata(), 63 | Artifact: artifact, 64 | Severity: t.getHighestSeverity(items), 65 | Vulnerabilities: items, 66 | } 67 | } 68 | 69 | func (t *transformer) getHarborSeverity(v aqua.Vulnerability) harbor.Severity { 70 | var severity harbor.Severity 71 | switch v.AquaSeverity { 72 | case "critical": 73 | severity = harbor.SevCritical 74 | case "high": 75 | severity = harbor.SevHigh 76 | case "medium": 77 | severity = harbor.SevMedium 78 | case "low": 79 | severity = harbor.SevLow 80 | case "negligible": 81 | severity = harbor.SevNegligible 82 | default: 83 | log.WithField("severity", v.AquaSeverity).Warn("Unknown Aqua severity") 84 | severity = harbor.SevUnknown 85 | } 86 | return severity 87 | } 88 | 89 | func (t *transformer) toLinks(v aqua.Vulnerability) []string { 90 | var links []string 91 | if v.NVDURL != "" { 92 | links = append(links, v.NVDURL) 93 | } 94 | if v.VendorURL != "" { 95 | links = append(links, v.VendorURL) 96 | } 97 | return links 98 | } 99 | 100 | func (t *transformer) getHighestSeverity(items []harbor.VulnerabilityItem) (highest harbor.Severity) { 101 | highest = harbor.SevUnknown 102 | 103 | for _, v := range items { 104 | if v.Severity > highest { 105 | highest = v.Severity 106 | } 107 | } 108 | 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /pkg/scanner/transformer_mock.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type MockTransformer struct { 10 | mock.Mock 11 | } 12 | 13 | func (t *MockTransformer) Transform(artifact harbor.Artifact, source aqua.ScanReport) harbor.ScanReport { 14 | args := t.Called(artifact, source) 15 | return args.Get(0).(harbor.ScanReport) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/scanner/transformer_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/aqua" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/ext" 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTransformer_Transform(t *testing.T) { 13 | now := time.Now() 14 | 15 | artifact := harbor.Artifact{ 16 | Repository: "library/golang", 17 | Tag: "1.12.4", 18 | } 19 | 20 | aquaReport := aqua.ScanReport{ 21 | Resources: []aqua.ResourceScan{ 22 | { 23 | Resource: aqua.Resource{ 24 | Type: aqua.Package, 25 | Name: "openssl", 26 | Version: "2.8.3", 27 | }, 28 | Vulnerabilities: []aqua.Vulnerability{ 29 | { 30 | Name: "CVE-0001-0020", 31 | AquaSeverity: "high", 32 | NVDURL: "http://nvd?id=CVE-0001-0020", 33 | }, 34 | { 35 | Name: "CVE-3045-2011", 36 | AquaSeverity: "low", 37 | }, 38 | }, 39 | }, 40 | { 41 | Resource: aqua.Resource{ 42 | Type: aqua.Library, 43 | Path: "/app/main.rb", 44 | }, 45 | Vulnerabilities: []aqua.Vulnerability{ 46 | { 47 | Name: "CVE-9900-1100", 48 | AquaSeverity: "critical", 49 | }, 50 | }, 51 | }, 52 | }, 53 | } 54 | 55 | harborReport := NewTransformer(ext.NewFixedClock(now)).Transform(artifact, aquaReport) 56 | assert.Equal(t, harbor.ScanReport{ 57 | GeneratedAt: now, 58 | Artifact: harbor.Artifact{ 59 | Repository: "library/golang", 60 | Tag: "1.12.4", 61 | }, 62 | Scanner: harbor.Scanner{ 63 | Name: "Aqua Enterprise", 64 | Vendor: "Aqua Security", 65 | Version: "Unknown", 66 | }, 67 | Severity: harbor.SevCritical, 68 | Vulnerabilities: []harbor.VulnerabilityItem{ 69 | { 70 | ID: "CVE-0001-0020", 71 | Pkg: "openssl", 72 | Version: "2.8.3", 73 | Severity: harbor.SevHigh, 74 | Links: []string{ 75 | "http://nvd?id=CVE-0001-0020", 76 | }, 77 | }, 78 | { 79 | ID: "CVE-3045-2011", 80 | Pkg: "openssl", 81 | Version: "2.8.3", 82 | Severity: harbor.SevLow, 83 | Links: ([]string)(nil), 84 | }, 85 | { 86 | ID: "CVE-9900-1100", 87 | Pkg: "/app/main.rb", 88 | Version: "", 89 | Severity: harbor.SevCritical, 90 | Links: ([]string)(nil), 91 | }, 92 | }, 93 | }, harborReport) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/scanner/worker.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 5 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 6 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type worker struct { 11 | store persistence.Store 12 | adapter Adapter 13 | jobID string 14 | request harbor.ScanRequest 15 | } 16 | 17 | func (as *worker) Task() { 18 | log.Debugf("Scan worker started processing: %v", as.request.Artifact) 19 | 20 | err := as.scan() 21 | 22 | if err != nil { 23 | log.WithError(err).Error("Scan worker failed") 24 | err = as.store.UpdateStatus(as.jobID, job.Failed, err.Error()) 25 | if err != nil { 26 | log.WithError(err).Errorf("Error while updating scan job status to %s", job.Failed.String()) 27 | } 28 | } 29 | } 30 | 31 | func (as *worker) scan() error { 32 | err := as.store.UpdateStatus(as.jobID, job.Running) 33 | if err != nil { 34 | return err 35 | } 36 | report, err := as.adapter.Scan(as.request) 37 | if err != nil { 38 | return err 39 | } 40 | err = as.store.UpdateReport(as.jobID, report) 41 | if err != nil { 42 | return err 43 | } 44 | err = as.store.UpdateStatus(as.jobID, job.Finished) 45 | if err != nil { 46 | return err 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/scanner/worker_test.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | -------------------------------------------------------------------------------- /pkg/work/work.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | ) 6 | 7 | // Worker must be implemented by types that want to use the worker pool. 8 | type Worker interface { 9 | Task() 10 | } 11 | 12 | // Pool provides a pool of goroutines that can execute any Worker tasks 13 | // that are submitted 14 | type Pool struct { 15 | tasks chan Worker 16 | stop chan struct{} 17 | } 18 | 19 | func New() *Pool { 20 | return &Pool{ 21 | tasks: make(chan Worker), 22 | stop: make(chan struct{}), 23 | } 24 | } 25 | 26 | func (p *Pool) Start() { 27 | go func() { 28 | log.Trace("Work pool started") 29 | for { 30 | select { 31 | case w := <-p.tasks: 32 | go func() { 33 | log.Trace("Work pool received new task") 34 | w.Task() 35 | }() 36 | case <-p.stop: 37 | log.Trace("Work pool shutdown completed") 38 | return 39 | } 40 | } 41 | }() 42 | } 43 | 44 | // Run submits work to the pool 45 | func (p *Pool) Run(w Worker) { 46 | p.tasks <- w 47 | } 48 | 49 | // Shutdown waits for all the goroutines to shutdown. 50 | func (p *Pool) Shutdown() { 51 | log.Trace("Work pool shutdown started") 52 | close(p.stop) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/work/work_test.go: -------------------------------------------------------------------------------- 1 | package work 2 | -------------------------------------------------------------------------------- /test/integration/persistence/redis/store_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package redis 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/etc" 12 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/harbor" 13 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/job" 14 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/persistence/redis" 15 | "github.com/aquasecurity/harbor-scanner-aqua/pkg/redisx" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | tc "github.com/testcontainers/testcontainers-go" 19 | "github.com/testcontainers/testcontainers-go/wait" 20 | ) 21 | 22 | // TestStore is an integration test for the Redis persistence store. 23 | func TestStore(t *testing.T) { 24 | if testing.Short() { 25 | t.Skip("An integration test") 26 | } 27 | 28 | ctx := context.Background() 29 | redisC, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{ 30 | ContainerRequest: tc.ContainerRequest{ 31 | Image: "redis:5.0.5", 32 | ExposedPorts: []string{"6379/tcp"}, 33 | WaitingFor: wait.ForLog("Ready to accept connections"), 34 | }, 35 | Started: true, 36 | }) 37 | require.NoError(t, err, "should start redis container") 38 | defer func() { 39 | _ = redisC.Terminate(ctx) 40 | }() 41 | 42 | redisURL := getRedisURL(t, ctx, redisC) 43 | 44 | pool, err := redisx.NewPool(etc.RedisPool{ 45 | URL: redisURL, 46 | }) 47 | require.NoError(t, err) 48 | 49 | store := redis.NewStore(etc.RedisStore{ 50 | Namespace: "harbor.scanner.aqua:store", 51 | ScanJobTTL: parseDuration(t, "10s"), 52 | }, pool) 53 | 54 | t.Run("CRUD", func(t *testing.T) { 55 | scanJobID := "123" 56 | 57 | err := store.Create(job.ScanJob{ 58 | ID: scanJobID, 59 | Status: job.Pending, 60 | }) 61 | require.NoError(t, err, "saving scan job should not fail") 62 | 63 | j, err := store.Get(scanJobID) 64 | require.NoError(t, err, "getting scan job should not fail") 65 | assert.Equal(t, &job.ScanJob{ 66 | ID: scanJobID, 67 | Status: job.Pending, 68 | }, j) 69 | 70 | err = store.UpdateStatus(scanJobID, job.Running) 71 | require.NoError(t, err, "updating scan job status should not fail") 72 | 73 | j, err = store.Get(scanJobID) 74 | require.NoError(t, err, "getting scan job should not fail") 75 | assert.Equal(t, &job.ScanJob{ 76 | ID: scanJobID, 77 | Status: job.Running, 78 | }, j) 79 | 80 | scanReport := harbor.ScanReport{ 81 | Severity: harbor.SevHigh, 82 | Vulnerabilities: []harbor.VulnerabilityItem{ 83 | { 84 | ID: "CVE-2013-1400", 85 | Pkg: "openssl", 86 | Version: "2.4", 87 | FixVersion: "2.4.2", 88 | Severity: harbor.SevHigh, 89 | Links: []string{ 90 | "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-1400", 91 | }, 92 | }, 93 | }, 94 | } 95 | 96 | err = store.UpdateReport(scanJobID, scanReport) 97 | require.NoError(t, err, "updating scan job reports should not fail") 98 | 99 | j, err = store.Get(scanJobID) 100 | require.NoError(t, err, "retrieving scan job should not fail") 101 | require.NotNil(t, j, "retrieved scan job must not be nil") 102 | assert.Equal(t, scanReport, j.Report) 103 | 104 | err = store.UpdateStatus(scanJobID, job.Finished) 105 | require.NoError(t, err) 106 | 107 | time.Sleep(parseDuration(t, "12s")) 108 | 109 | j, err = store.Get(scanJobID) 110 | require.NoError(t, err, "retrieve scan job should not fail") 111 | require.Nil(t, j, "retrieved scan job should be nil, i.e. expired") 112 | }) 113 | } 114 | 115 | func getRedisURL(t *testing.T, ctx context.Context, redisC tc.Container) string { 116 | t.Helper() 117 | host, err := redisC.Host(ctx) 118 | require.NoError(t, err) 119 | port, err := redisC.MappedPort(ctx, "6379") 120 | require.NoError(t, err) 121 | return fmt.Sprintf("redis://%s:%d", host, port.Int()) 122 | } 123 | 124 | func parseDuration(t *testing.T, s string) time.Duration { 125 | t.Helper() 126 | d, err := time.ParseDuration(s) 127 | require.NoError(t, err, "should parse duration %s", s) 128 | return d 129 | } 130 | -------------------------------------------------------------------------------- /vagrant/harbor.yml: -------------------------------------------------------------------------------- 1 | # Configuration file of Harbor 2 | 3 | # The IP address or hostname to access admin UI and registry service. 4 | # DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients. 5 | hostname: nginx 6 | 7 | # http related config 8 | http: 9 | # port for http, default is 80. If https enabled, this port will redirect to https port 10 | port: 8080 11 | 12 | # Uncomment following will enable tls communication between all harbor components 13 | internal_tls: 14 | # set enabled to true means internal tls is enabled 15 | enabled: true 16 | # put your cert and key files on dir 17 | dir: /etc/harbor/pki/internal 18 | 19 | # The initial password of Harbor admin 20 | # It only works in first time to install harbor 21 | # Remember Change the admin password from UI after launching Harbor. 22 | harbor_admin_password: Harbor12345 23 | 24 | # Harbor DB configuration 25 | database: 26 | # The password for the root user of Harbor DB. Change this before any production use. 27 | password: root123 28 | # The maximum number of connections in the idle connection pool. If it <=0, no idle connections are retained. 29 | max_idle_conns: 100 30 | # The maximum number of open connections to the database. If it <= 0, then there is no limit on the number of open connections. 31 | # Note: the default number of connections is 1024 for postgres of harbor. 32 | max_open_conns: 900 33 | 34 | # The default data volume 35 | data_volume: /data 36 | 37 | # Trivy configuration 38 | # 39 | # Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases. 40 | # It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached 41 | # in the local file system. In addition, the database contains the update timestamp so Trivy can detect whether it 42 | # should download a newer version from the Internet or use the cached one. Currently, the database is updated every 43 | # 12 hours and pdublished as a new release to GitHub. 44 | trivy: 45 | # ignoreUnfixed The flag to display only fixed vulnerabilities 46 | ignore_unfixed: true 47 | # skipUpdate The flag to enable or disable Trivy DB downloads from GitHub 48 | # 49 | # You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues. 50 | # If the flag is enabled you have to download the `trivy-offline.tar.gz` archive manually, extract `trivy.db` and 51 | # `metadata.json` files and mount them in the `/home/scanner/.cache/trivy/db` path. 52 | skip_update: false 53 | # 54 | # insecure The flag to skip verifying registry certificate 55 | insecure: false 56 | # github_token The GitHub access token to download Trivy DB 57 | # 58 | # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough 59 | # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000 60 | # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult 61 | # https://developer.github.com/v3/#rate-limiting 62 | # 63 | # You can create a GitHub token by following the instructions in 64 | # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line 65 | # 66 | # github_token: xxx 67 | 68 | jobservice: 69 | # Maximum number of job workers in job service 70 | max_job_workers: 10 71 | 72 | notification: 73 | # Maximum retry count for webhook job 74 | webhook_job_max_retry: 10 75 | 76 | chart: 77 | # Change the value of absolute_url to enabled can enable absolute url in chart 78 | absolute_url: disabled 79 | 80 | # Log configurations 81 | log: 82 | # options are debug, info, warning, error, fatal 83 | level: info 84 | # configs for logs in local storage 85 | local: 86 | # Log files are rotated log_rotate_count times before being removed. If count is 0, old versions are removed rather than rotated. 87 | rotate_count: 50 88 | # Log files are rotated only if they grow bigger than log_rotate_size bytes. If size is followed by k, the size is assumed to be in kilobytes. 89 | # If the M is used, the size is in megabytes, and if G is used, the size is in gigabytes. So size 100, size 100k, size 100M and size 100G 90 | # are all valid. 91 | rotate_size: 200M 92 | # The directory on your host that store log 93 | location: /var/log/harbor 94 | 95 | #This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY! 96 | _version: 2.4.0 97 | 98 | # Global proxy 99 | # Config http proxy for components, e.g. http://my.proxy.com:3128 100 | # Components doesn't need to connect to each others via http proxy. 101 | # Remove component from `components` array if want disable proxy 102 | # for it. If you want use proxy for replication, MUST enable proxy 103 | # for core and jobservice, and set `http_proxy` and `https_proxy`. 104 | # Add domain to the `no_proxy` field, when you want disable proxy 105 | # for some special registry. 106 | proxy: 107 | http_proxy: 108 | https_proxy: 109 | no_proxy: 110 | components: 111 | - core 112 | - jobservice 113 | - trivy 114 | -------------------------------------------------------------------------------- /vagrant/install-aqua.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ -z "$AQUA_REGISTRY_USERNAME" ]; then echo "AQUA_REGISTRY_USERNAME env is unset" && exit 1; fi 4 | if [ -z "$AQUA_REGISTRY_PASSWORD" ]; then echo "AQUA_REGISTRY_PASSWORD env is unset" && exit 1; fi 5 | if [ -z "$AQUA_VERSION" ]; then echo "AQUA_VERSION env is unset" && exit 1; else echo "AQUA_VERSION env is set to '$AQUA_VERSION'"; fi 6 | 7 | HARBOR_HOME="/opt/harbor" 8 | HARBOR_PKI_DIR="/etc/harbor/pki/internal" 9 | HARBOR_SCANNER_AQUA_VERSION="0.14.0" 10 | SCANNER_UID=1000 11 | SCANNER_GID=1000 12 | 13 | mkdir -p $HARBOR_HOME/common/config/aqua-adapter 14 | mkdir -p /data/aqua-adapter/reports 15 | mkdir -p /data/aqua-adapter/opt 16 | mkdir -p /var/lib/aqua-db/data 17 | 18 | # Login to Aqua registry. 19 | echo $AQUA_REGISTRY_PASSWORD | docker login registry.aquasec.com \ 20 | --username $AQUA_REGISTRY_USERNAME \ 21 | --password-stdin 22 | 23 | # Copy the scannercli binary from the registry.aquasec.com/scanner image. 24 | docker run --rm --entrypoint "" \ 25 | --volume $HARBOR_HOME/common/config/aqua-adapter:/out registry.aquasec.com/scanner:$AQUA_VERSION \ 26 | cp /opt/aquasec/scannercli /out 27 | 28 | # Generate a private key. 29 | openssl genrsa -out $HARBOR_PKI_DIR/aqua_adapter.key 4096 30 | 31 | # Generate a certificate signing request (CSR). 32 | openssl req -sha512 -new \ 33 | -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=aqua-adapter" \ 34 | -key $HARBOR_PKI_DIR/aqua_adapter.key \ 35 | -out $HARBOR_PKI_DIR/aqua_adapter.csr 36 | 37 | # Generate an x509 v3 extension file. 38 | cat > $HARBOR_PKI_DIR/aqua_adapter_v3.ext <<-EOF 39 | authorityKeyIdentifier=keyid,issuer 40 | basicConstraints=CA:FALSE 41 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment 42 | extendedKeyUsage = serverAuth 43 | subjectAltName = @alt_names 44 | 45 | [alt_names] 46 | DNS.1=aqua-adapter 47 | EOF 48 | 49 | # Use the v3.ext file to generate a certificate for your Harbor host. 50 | openssl x509 -req -sha512 -days 365 \ 51 | -extfile $HARBOR_PKI_DIR/aqua_adapter_v3.ext \ 52 | -CA $HARBOR_PKI_DIR/harbor_internal_ca.crt \ 53 | -CAkey $HARBOR_PKI_DIR/harbor_internal_ca.key \ 54 | -CAcreateserial \ 55 | -in $HARBOR_PKI_DIR/aqua_adapter.csr \ 56 | -out $HARBOR_PKI_DIR/aqua_adapter.crt 57 | 58 | chown $SCANNER_UID:$SCANNER_GID /data/aqua-adapter/reports 59 | chown $SCANNER_UID:$SCANNER_GID /data/aqua-adapter/opt 60 | chown $SCANNER_UID:$SCANNER_GID $HARBOR_HOME/common/config/aqua-adapter/scannercli 61 | chown $SCANNER_UID:$SCANNER_GID $HARBOR_PKI_DIR/aqua_adapter.key 62 | chown $SCANNER_UID:$SCANNER_GID $HARBOR_PKI_DIR/aqua_adapter.crt 63 | 64 | cat << EOF > $HARBOR_HOME/common/config/aqua-adapter/env 65 | SCANNER_LOG_LEVEL=debug 66 | SCANNER_API_ADDR=:8443 67 | SCANNER_API_TLS_KEY=/etc/pki/aqua_adapter.key 68 | SCANNER_API_TLS_CERTIFICATE=/etc/pki/aqua_adapter.crt 69 | SCANNER_AQUA_USERNAME=administrator 70 | SCANNER_AQUA_PASSWORD=@Aqua12345 71 | SCANNER_AQUA_HOST=https://aqua-console:8443 72 | SCANNER_CLI_NO_VERIFY=true 73 | SCANNER_AQUA_REGISTRY=Harbor 74 | SCANNER_AQUA_USE_IMAGE_TAG=false 75 | SCANNER_AQUA_REPORTS_DIR=/var/lib/scanner/reports 76 | SCANNER_REDIS_URL=redis://redis:6379 77 | SCANNER_CLI_OVERRIDE_REGISTRY_CREDENTIALS=false 78 | EOF 79 | 80 | cat << EOF > $HARBOR_HOME/docker-compose.override.yml 81 | version: '2.3' 82 | services: 83 | aqua-adapter: 84 | networks: 85 | - harbor 86 | container_name: aqua-adapter 87 | # image: docker.io/aquasec/harbor-scanner-aqua:dev 88 | # image: docker.io/aquasec/harbor-scanner-aqua:$HARBOR_SCANNER_AQUA_VERSION 89 | image: public.ecr.aws/aquasecurity/harbor-scanner-aqua:$HARBOR_SCANNER_AQUA_VERSION 90 | restart: always 91 | cap_drop: 92 | - ALL 93 | depends_on: 94 | - redis 95 | volumes: 96 | - type: bind 97 | source: $HARBOR_PKI_DIR/aqua_adapter.key 98 | target: /etc/pki/aqua_adapter.key 99 | - type: bind 100 | source: $HARBOR_PKI_DIR/aqua_adapter.crt 101 | target: /etc/pki/aqua_adapter.crt 102 | - type: bind 103 | source: $HARBOR_HOME/common/config/aqua-adapter/scannercli 104 | target: /usr/local/bin/scannercli 105 | - type: bind 106 | source: /data/aqua-adapter/reports 107 | target: /var/lib/scanner/reports 108 | - type: bind 109 | source: /data/aqua-adapter/opt 110 | target: /opt/aquascans 111 | logging: 112 | driver: "syslog" 113 | options: 114 | syslog-address: "tcp://127.0.0.1:1514" 115 | tag: "aqua-adapter" 116 | env_file: 117 | $HARBOR_HOME/common/config/aqua-adapter/env 118 | aqua-db: 119 | networks: 120 | - harbor 121 | image: registry.aquasec.com/database:$AQUA_VERSION 122 | container_name: aqua-db 123 | environment: 124 | - POSTGRES_PASSWORD=lunatic0 125 | volumes: 126 | - /var/lib/aqua-db/data:/var/lib/postgresql/data 127 | aqua-console: 128 | networks: 129 | - harbor 130 | ports: 131 | - 9080:8080 132 | image: registry.aquasec.com/console:$AQUA_VERSION 133 | container_name: aqua-console 134 | environment: 135 | - ADMIN_PASSWORD=@Aqua12345 136 | - SCALOCK_DBHOST=aqua-db 137 | - SCALOCK_DBNAME=scalock 138 | - SCALOCK_DBUSER=postgres 139 | - SCALOCK_DBPASSWORD=lunatic0 140 | - SCALOCK_AUDIT_DBHOST=aqua-db 141 | - SCALOCK_AUDIT_DBNAME=slk_audit 142 | - SCALOCK_AUDIT_DBUSER=postgres 143 | - SCALOCK_AUDIT_DBPASSWORD=lunatic0 144 | - AQUA_DOCKERLESS_SCANNING=1 145 | volumes: 146 | - /var/run/docker.sock:/var/run/docker.sock 147 | depends_on: 148 | - aqua-db 149 | aqua-gateway: 150 | image: registry.aquasec.com/gateway:$AQUA_VERSION 151 | container_name: aqua-gateway 152 | environment: 153 | - SCALCOK_LOG_LEVEL=DEBUG 154 | - AQUA_CONSOLE_SECURE_ADDRESS=aqua-console:8443 155 | - SCALOCK_DBHOST=aqua-db 156 | - SCALOCK_DBNAME=scalock 157 | - SCALOCK_DBUSER=postgres 158 | - SCALOCK_DBPASSWORD=lunatic0 159 | - SCALOCK_AUDIT_DBHOST=aqua-db 160 | - SCALOCK_AUDIT_DBNAME=slk_audit 161 | - SCALOCK_AUDIT_DBUSER=postgres 162 | - SCALOCK_AUDIT_DBPASSWORD=lunatic0 163 | networks: 164 | - harbor 165 | depends_on: 166 | - aqua-db 167 | - aqua-console 168 | EOF 169 | 170 | cd /opt/harbor 171 | docker-compose up --detach 172 | 173 | # Use Harbor 2.0 REST API to register aqua-adapter as an Interrogation Service. 174 | cat << EOF > /tmp/aqua-adapter.registration.json 175 | { 176 | "name": "Aqua Enterprise $AQUA_VERSION", 177 | "url": "https://aqua-adapter:8443", 178 | "description": "Aqua Enterprise $AQUA_VERSION vulnerability scanner." 179 | } 180 | EOF 181 | 182 | curl --include \ 183 | --user admin:Harbor12345 \ 184 | --request POST \ 185 | --header "accept: application/json" \ 186 | --header "Content-Type: application/json" \ 187 | --data-binary "@/tmp/aqua-adapter.registration.json" \ 188 | "http://localhost:8080/api/v2.0/scanners" 189 | -------------------------------------------------------------------------------- /vagrant/install-docker.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # This script installs Docker CE and Docker Compose according to the official 4 | # documentation on https://docs.docker.com/engine/install/ubuntu/ 5 | # and https://docs.docker.com/compose/install/ 6 | 7 | # To list the available versions in the repo: 8 | # apt-cache madison docker-ce containerd.io 9 | 10 | CONTAINERD_VERSION="1.4.9-1" 11 | DOCKER_VERSION="5:20.10.8~3-0~ubuntu-focal" 12 | DOCKER_COMPOSE_VERSION="1.29.2" 13 | 14 | sudo apt-get update 15 | sudo apt-get install --yes apt-transport-https ca-certificates curl gnupg lsb-release 16 | 17 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 18 | 19 | echo \ 20 | "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ 21 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 22 | 23 | sudo apt-get update 24 | sudo apt-get install --yes containerd.io=$CONTAINERD_VERSION docker-ce=$DOCKER_VERSION docker-ce-cli=$DOCKER_VERSION 25 | 26 | # Add vagrant user to the docker group. 27 | sudo usermod -aG docker vagrant 28 | 29 | # Download the current stable release of Docker Compose. 30 | sudo curl -L "https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 31 | 32 | # Apply executable permissions to the binary. 33 | sudo chmod +x /usr/local/bin/docker-compose 34 | -------------------------------------------------------------------------------- /vagrant/install-go.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | GO_VERSION="1.18" 4 | 5 | wget --quiet https://golang.org/dl/go$GO_VERSION.linux-amd64.tar.gz 6 | tar -C /usr/local -xzf go$GO_VERSION.linux-amd64.tar.gz 7 | echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.profile 8 | 9 | sudo apt-get update 10 | sudo apt-get install --yes build-essential 11 | -------------------------------------------------------------------------------- /vagrant/install-harbor.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ -z "$HARBOR_VERSION" ]; then echo "HARBOR_VERSION env is unset"; else echo "HARBOR_VERSION env is set to '$HARBOR_VERSION'"; fi 4 | 5 | HARBOR_HOME="/opt/harbor" 6 | 7 | # Download the offline Harbor installer. 8 | # We prefer offline installer to avoid DockerHub rate limits. 9 | wget --quiet https://github.com/goharbor/harbor/releases/download/$HARBOR_VERSION/harbor-offline-installer-$HARBOR_VERSION.tgz 10 | 11 | # Download the corresponding *.asc file to verify that the package is genuine. 12 | wget --quiet https://github.com/goharbor/harbor/releases/download/$HARBOR_VERSION/harbor-offline-installer-$HARBOR_VERSION.tgz.asc 13 | 14 | # Obtain the public key for the *.asc file. 15 | gpg --keyserver hkps://keyserver.ubuntu.com --receive-keys 644FF454C0B4115C 16 | 17 | # Verify that the installer package is genuine. 18 | gpg --verbose --keyserver hkps://keyserver.ubuntu.com --verify harbor-offline-installer-$HARBOR_VERSION.tgz.asc 19 | 20 | tar -C /opt -xzf harbor-offline-installer-$HARBOR_VERSION.tgz 21 | 22 | rm harbor-offline-installer-$HARBOR_VERSION.tgz 23 | rm harbor-offline-installer-$HARBOR_VERSION.tgz.asc 24 | rm $HARBOR_HOME/harbor.yml.tmpl 25 | 26 | cp /vagrant/vagrant/harbor.yml $HARBOR_HOME/harbor.yml 27 | 28 | cat >> /etc/hosts <