├── .github ├── images │ ├── screenshot.png │ ├── screenshot_light.png │ ├── screenshot_results.png │ ├── screenshot_results_light.png │ ├── screenshot_sbom_results.png │ └── screenshot_sbom_results_light.png └── workflows │ ├── codeql-analysis.yml │ └── easy_release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── client ├── .browserslistrc ├── .editorconfig ├── .env ├── .gitignore ├── .prettierrc.json ├── package-lock.json ├── package.json ├── public │ ├── font │ │ └── droplet.otf │ ├── images │ │ ├── tada.svg │ │ ├── trivy.svg │ │ └── trivy_logo.svg │ ├── index.html │ └── styles │ │ └── fonts.css ├── src │ ├── App.tsx │ ├── ConfigureCreds.tsx │ ├── DefaultDisplay.tsx │ ├── ImageList.tsx │ ├── Links.tsx │ ├── Loading.tsx │ ├── Metrics.tsx │ ├── Pill.tsx │ ├── SBOM.tsx │ ├── Success.tsx │ ├── TrivyVulnerability.tsx │ ├── Vulns.tsx │ ├── VulnsFilter.tsx │ ├── Welcome.tsx │ ├── globals.d.ts │ ├── index.tsx │ └── react-app-env.d.ts ├── tsconfig.json └── yarn.lock ├── go.mod ├── go.sum ├── make.bat ├── metadata.json ├── metrics ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go └── template.yaml ├── service ├── docker-compose.yaml ├── internal │ ├── auth │ │ └── validate.go │ └── socket │ │ ├── socket.go │ │ ├── socket_darwin.go │ │ ├── socket_linux.go │ │ ├── socket_unix.go │ │ ├── socket_unix_test.go │ │ └── socket_windows.go └── main.go └── trivy.svg /.github/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot.png -------------------------------------------------------------------------------- /.github/images/screenshot_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot_light.png -------------------------------------------------------------------------------- /.github/images/screenshot_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot_results.png -------------------------------------------------------------------------------- /.github/images/screenshot_results_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot_results_light.png -------------------------------------------------------------------------------- /.github/images/screenshot_sbom_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot_sbom_results.png -------------------------------------------------------------------------------- /.github/images/screenshot_sbom_results_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/.github/images/screenshot_sbom_results_light.png -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 13 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/easy_release.yml: -------------------------------------------------------------------------------- 1 | name: Release extension 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | publish-extension: 9 | name: Build extension and push to dockerhub 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repo 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up QEMU 16 | id: qemu 17 | uses: docker/setup-qemu-action@v1 18 | 19 | - name: Login to DockerHub 20 | if: github.event_name != 'pull_request' 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USER }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | 26 | - name: Release extension 27 | run: TAG=${GITHUB_REF#refs/*/} make release-extension 28 | env: 29 | DOCKER_BUILDKIT: 1 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ui/build/bundle.* 3 | metrics/.aws-sam 4 | metrics/samconfig.toml 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the UI 2 | FROM node:lts-alpine3.15 AS client-builder 3 | WORKDIR /app/client 4 | 5 | COPY client/package.json /app/client/package.json 6 | COPY client/yarn.lock /app/client/yarn.lock 7 | 8 | ARG TARGETARCH 9 | RUN yarn config set cache-folder /usr/local/share/.cache/yarn-${TARGETARCH} 10 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn-${TARGETARCH} yarn --network-timeout 1000000 11 | 12 | COPY client /app/client 13 | RUN --mount=type=cache,target=/usr/local/share/.cache/yarn-${TARGETARCH} yarn build --network-timeout 1000000 14 | 15 | 16 | # Build the service 17 | FROM golang:1.17-alpine AS service-builder 18 | ENV CGO_ENABLED=0 19 | RUN apk add --update make 20 | WORKDIR /plugin 21 | COPY . . 22 | RUN make bin 23 | 24 | # Bring it all together 25 | FROM alpine:3.15 26 | LABEL org.opencontainers.image.title="Aqua Trivy" \ 27 | org.opencontainers.image.description="Run unlimited vulnerability scans against remote or locally stored images. Understand any security issues that may be present in images before you pull and use them." \ 28 | org.opencontainers.image.vendor="Aqua Security Software" \ 29 | com.docker.desktop.extension.api.version=">= 0.2.0" \ 30 | com.docker.desktop.extension.icon="https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/main/trivy.svg" \ 31 | com.docker.extension.publisher-url="https://trivy.dev" \ 32 | com.docker.extension.screenshots="[{\"alt\": \"Trivy Dark Screenshot\", \"url\": \"https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/main/.github/images/screenshot.png\"},{\"alt\": \"Trivy light screenshot\", \"url\": \"https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/main/.github/images/screenshot_light.png\"}]" \ 33 | com.docker.extension.detailed-description="

Free and Unlimited Vulnerability Scanning

Take control of your application security with Trivy

Trivy is the world’s most popular open source vulnerability and misconfiguration scanner. It is reliable, fast, extremely easy to use, and it works wherever you need it. " \ 34 | com.docker.extension.additional-urls="[{\"title\":\"Trivy Website\",\"url\":\"https://trivy.dev/\"},{\"title\":\"Issues\",\"url\":\"https://github.com/aquasecurity/trivy/issues\"},{\"title\":\"Slack\",\"url\":\"https://slack.aquasec.com/\"}]" \ 35 | com.docker.extension.category="security" 36 | 37 | COPY --from=client-builder /app/client/dist ui 38 | COPY --from=service-builder /plugin/bin/creds-service / 39 | COPY trivy.svg . 40 | COPY metadata.json . 41 | COPY service/docker-compose.yaml . 42 | 43 | CMD /creds-service -------------------------------------------------------------------------------- /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 2022 Aqua Security 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 | IMAGE?=aquasec/trivy-docker-extension 2 | TAG?=latest 3 | 4 | BUILDER=buildx-multi-arch 5 | 6 | STATIC_FLAGS=CGO_ENABLED=0 7 | LDFLAGS="-s -w" 8 | GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS) 9 | 10 | .PHONY: bin 11 | bin: ## Build the binary for the current plarform 12 | @echo "$(INFO_COLOR)Building...$(NO_COLOR)" 13 | $(GO_BUILD) -o bin/creds-service ./service 14 | 15 | .PHONY: build-app 16 | build-app: 17 | @npm run-script build 18 | 19 | .PHONY: build-dev 20 | build-dev: 21 | @DOCKER_BUILDKIT=1 docker build -t trivy-docker-extension:development . 22 | 23 | .PHONY: deploy-dev 24 | deploy-dev: build-dev 25 | @docker extension rm trivy-docker-extension:development || true 26 | @docker extension install trivy-docker-extension:development 27 | 28 | .PHONY: dev-debug 29 | dev-debug: 30 | @docker extension dev debug trivy-docker-extension:development 31 | 32 | .PHONY: dev-reset 33 | dev-reset: 34 | @docker extension dev reset trivy-docker-extension:development 35 | 36 | .PHONY: remove-dev 37 | remove-dev: 38 | @docker extension rm trivy-docker-extension:development || true 39 | 40 | .PHONY: prepare-buildx 41 | prepare-buildx: ## Create buildx builder for multi-arch build, if not exists 42 | docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host 43 | 44 | .PHONY: release-extension 45 | release-extension: prepare-buildx 46 | @docker buildx build --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Docker Pulls](https://img.shields.io/docker/pulls/aquasec/trivy-docker-extension?style=flat-square) 2 | ![Latest Tagged Release](https://img.shields.io/github/v/tag/aquasecurity/trivy-docker-extension?style=flat-square) 3 | 4 | # Trivy Docker Extension 5 | 6 | 7 | ## What is this? 8 | 9 | Docker are adding the concept of `Extensions` to the Docker Desktop tool. This is an extension that allows the user to run Trivy and get pretty output in return. 10 | 11 | The user can either select from a drop down of local images or type the name of an image into the drop down. 12 | 13 | ![Screenshot](.github/images/screenshot.png) 14 | 15 | ![Screenshot - Light Theme](.github/images/screenshot_light.png) 16 | 17 | 18 | When you run the scan you'll get the results 19 | 20 | ![Screenshot Results](.github/images/screenshot_results.png) 21 | 22 | ![Screenshot Results - Light Theme](.github/images/screenshot_results_light.png) 23 | 24 | When you run the scan you can optionally get the results in SBOM format 25 | 26 | ![Screenshot SBOM Results](.github/images/screenshot_sbom_results.png) 27 | 28 | ![Screenshot SBOM Results - Light Theme](.github/images/screenshot_sbom_results_light.png) 29 | 30 | ## What is is made of? 31 | 32 | The extension runs in its own container with a web interface that calls into the Docker extension API. At a high level the flow is; 33 | 34 | 1. specify an image 35 | 2. create the trivy cache volume if it does not already exist 36 | 3. run aquasec/trivy against the image providing the volume for docker.sock and the cache volume 37 | 4. process the json results and render 38 | 39 | ### But what is it made of? 40 | 41 | The extension is React app leveraging Material UI components. The Docker extension team have provided theme support so we just use that for the look and feel. 42 | 43 | The source is all in `client/src` with the main component being [App.tsx](client/src/App.tsx). This has the core code for running the extension and loads all of the child components. 44 | 45 | The rest of the tsx files are detailed below 46 | 47 | | Component | Purpose | 48 | |-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| 49 | | [DefaultDisplay](client/src/DefaultDisplay.tsx) | After the landing page has been used to initiate the first scan the view switches to the default display with the logo and search box | 50 | | [ImageList](client/src/ImageList.tsx) | Provides the functionality for loading the images stored locally and autocomplete functionality | 51 | | [Links](client/src/Links.tsx) | Header links for github, docs and slack | 52 | | [Loading](client/src/Loading.tsx) | The spinner loading blackout shim | 53 | | [Pill](client/src/Pill.tsx) | Coloured badges to denote the severity of the vulnerability | 54 | | [Success](client/src/Success.tsx) | When the scan has no vulnerabilities this :tada: message is displayed | 55 | | [Vulns](client/src/Vulns.tsx) | Renders the Accordion "table" of results - this includes the [VulnsFilter](client/src/VulnsFilter.tsx) | 56 | | [VulnsFilter](client/src/VulnsFilter.tsx) | This control has the numbers of each severity and allows filtering the "table" of results | 57 | | [Welcome](client/src/Welcome.tsx) | The Landing page - has the Trivy description and the initial scan | 58 | 59 | 60 | In addition to these there is the [TrivyVulnerability](client/src/TrivyVulnerability.tsx) which provides a class to represent a vulnerability from the Json results. 61 | 62 | ## How do I get started? 63 | 64 | ### Prereqs 65 | 66 | You will need 67 | 68 | 1. Docker Desktop release that supports extensions (currently private repo) 69 | 2. Docker Extension binary release (currently private repo) 70 | 3. NPM installed 71 | 72 | ## Local Dev 73 | 74 | ### Deploy to local Docker Desktop 75 | 76 | To launch the extension into your Docker Desktop you'll need 77 | 78 | *Linux/Mac* 79 | 80 | ```bash 81 | make deploy-dev 82 | ``` 83 | 84 | *Windows* 85 | 86 | ```bash 87 | make.bat deploy-dev 88 | ``` 89 | 90 | ### Enable debugging in local Docker Desktop 91 | 92 | To launch the extension into your Docker Desktop you'll need 93 | 94 | *Linux/Mac* 95 | 96 | ```bash 97 | make dev-debug 98 | ``` 99 | 100 | *Windows* 101 | 102 | ```bash 103 | make.bat dev-debug 104 | ``` 105 | 106 | ### Disable debugging in local Docker Desktop 107 | 108 | To launch the extension into your Docker Desktop you'll need 109 | 110 | *Linux/Mac* 111 | 112 | ```bash 113 | make dev-reset 114 | ``` 115 | 116 | *Windows* 117 | 118 | ```bash 119 | make.bat dev-reset 120 | ``` 121 | 122 | ## CI Process 123 | 124 | A bit about the CI process - on a new tag a release will be built for `linux/amd64` and `linux/arm64` using `docker buildx` with the multi arch builder. 125 | 126 | The [release-extension target](Makefile) in the `Makefile` is fairly self explanatory and will push the new image. 127 | 128 | It's worth noting the installation of QEmu on the GitHub action worker so that it can build the `arm64` image - without that there are issues finding `glibc`. 129 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This is automatically converted to a Chromium version 2 | # See: https://github.com/kilian/electron-to-chromium 3 | 4 | # One day, we could distribute a config package from pinata with a Chrome version derived by 5 | # feeding `require("electron").version` into an up-to-date `electron-to-chromium`. 6 | # https://github.com/browserslist/browserslist#shareable-configs 7 | 8 | Chrome 91 9 | 10 | # Ideally we'd do (at time of writing) "Electron 13.1.4", but the version of electron-to-chromium 11 | # in our dependency tree is a bit outdated 12 | # See https://github.com/Kilian/electron-to-chromium/blob/master/versions.js for full list 13 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | root = true 3 | end_of_line = lf 4 | 5 | indent_size = 2 6 | indent_style = space 7 | -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | BUILD_PATH=dist 2 | PUBLIC_URL=. 3 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Yarn-related 27 | .yarn/* 28 | !.yarn/patches 29 | !.yarn/releases 30 | !.yarn/plugins 31 | !.yarn/sdks 32 | !.yarn/versions 33 | .pnp.* 34 | -------------------------------------------------------------------------------- /client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aquasec/trivy-desktop-extension", 3 | "version": "0.3.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build" 8 | }, 9 | "dependencies": { 10 | "@docker/docker-mui-theme": "^0.0.6", 11 | "@emotion/react": "^11.8.2", 12 | "@emotion/styled": "^11.8.1", 13 | "@mui/icons-material": "^5.5.1", 14 | "@mui/material": "^5.5.2", 15 | "@mui/system": "^5.5.2", 16 | "@types/react": "^17.0.0", 17 | "@types/react-dom": "^17.0.0", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "react-scripts": "5.0.0", 21 | "typescript": "^4.1.2" 22 | } 23 | } -------------------------------------------------------------------------------- /client/public/font/droplet.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquasecurity/trivy-docker-extension/be570083643124add79adda3c37a13f0997d060f/client/public/font/droplet.otf -------------------------------------------------------------------------------- /client/public/images/tada.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/public/images/trivy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/images/trivy_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 49 | 54 | 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 149 | 150 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /client/public/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Droplet; 3 | src: url("../font/droplet.otf") format("opentype"); 4 | } 5 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { DockerMuiThemeProvider } from '@docker/docker-mui-theme'; 2 | import { Box } from '@mui/material'; 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import React from 'react'; 5 | import { ConfigureCreds } from './ConfigureCreds'; 6 | 7 | import { DefaultDisplay } from './DefaultDisplay'; 8 | import { Loading } from './Loading'; 9 | import { SBOM } from './SBOM'; 10 | import { Success } from './Success'; 11 | import { TrivyVulnerability } from './TrivyVulnerability'; 12 | import { Vulns } from './Vulns'; 13 | import { Welcome } from './Welcome'; 14 | import { SendMetric } from './Metrics'; 15 | 16 | 17 | export function App() { 18 | const [aquaKey, setAquaKey] = React.useState(""); 19 | const [aquaSecret, setAquaSecret] = React.useState(""); 20 | const [aquaCSPMUrl, setAquaCSPMUrl] = React.useState(""); 21 | 22 | const [scanImage, setScanImage] = React.useState(""); 23 | const [disableScan, setDisableScan] = React.useState(true); 24 | const [fixedOnly, setFixedOnly] = React.useState(true); 25 | const [uploadAqua, setUploadAqua] = React.useState(false); 26 | const [SBOMOutput, setSBOMOutput] = React.useState(false); 27 | const [SBOMContent, setSBOMContent] = React.useState(""); 28 | 29 | 30 | const [showLoginBox, setShowLoginBox] = React.useState(false); 31 | const [showUploadAqua, setShowUploadAqua] = React.useState("none") 32 | const [showSBOM, setShowSBOM] = React.useState("none"); 33 | const [showFilter, setShowFilter] = React.useState("none"); 34 | const [showSuccess, setShowSuccess] = React.useState("none"); 35 | const [showDefaultDisplay, setShowDefaultDisplay] = React.useState("none"); 36 | const [showWelcome, setShowWelcome] = React.useState("flex"); 37 | 38 | const [severityFilter, setSeverityFilter] = React.useState("all"); 39 | const [all, setAll] = React.useState(0); 40 | const [critical, setCritical] = React.useState(0); 41 | const [high, setHigh] = React.useState(0); 42 | const [medium, setMedium] = React.useState(0); 43 | const [low, setLow] = React.useState(0); 44 | const [unknown, setUnknown] = React.useState(0); 45 | 46 | const [vulnerabilities, setVulnerabilities] = React.useState([]); 47 | const [allVulnerabilities, setAllVulnerabilities] = React.useState([]); 48 | 49 | const [loadingWait, setLoadingWait] = React.useState(false); 50 | const [loggedIn, setLoggedIn] = React.useState(false); 51 | 52 | 53 | 54 | const getSeverityOrdering = (severity: string): number => { 55 | switch (severity) { 56 | case "CRITICAL": 57 | return 0; 58 | case "HIGH": 59 | return 1; 60 | case "MEDIUM": 61 | return 2; 62 | case "LOW": 63 | return 3; 64 | case "UNKNOWN": 65 | return 4; 66 | default: 67 | return 5 68 | } 69 | }; 70 | 71 | async function checkForCacheVolume() { 72 | var exists = false; 73 | await window.ddClient.docker.cli 74 | .exec("volume", [ 75 | "inspect", 76 | "trivy-docker-extension-cache" 77 | ]) 78 | .then((result: any) => { 79 | console.log(result); 80 | if (result.stdout !== "") { 81 | exists = true; 82 | } 83 | }).catch((err: Error) => { 84 | console.log(err); 85 | exists = false; 86 | }); 87 | return exists; 88 | } 89 | 90 | async function createCacheVolume() { 91 | var success = true; 92 | await window.ddClient.docker.cli 93 | .exec("volume", ["create", "trivy-docker-extension-cache"]) 94 | .then((result: any) => { 95 | if (result.stderr !== "") { 96 | success = false; 97 | } 98 | }); 99 | return success; 100 | } 101 | 102 | async function triggerTrivy() { 103 | if (scanImage === "") { 104 | return; 105 | } 106 | 107 | 108 | resetUI(); 109 | if (!(await checkForCacheVolume())) { 110 | await createCacheVolume().then((created) => { 111 | if (!created) { 112 | setLoadingWait(true); 113 | console.log("failed to create volume"); 114 | return; 115 | } 116 | }); 117 | window.ddClient.desktopUI.toast.warning( 118 | `Creating vulnerability cache volume on first run, populating this will cause a slight delay.` 119 | ); 120 | } 121 | 122 | let stdout = ""; 123 | let stderr = ""; 124 | let commandParts: string[] = [ 125 | "--rm", 126 | "-v", 127 | "/var/run/docker.sock:/var/run/docker.sock", 128 | "-v", 129 | "trivy-docker-extension-cache:/root/.cache" 130 | ]; 131 | 132 | if (uploadAqua && !SBOMOutput) { 133 | commandParts.push("-e"); 134 | commandParts.push("TRIVY_RUN_AS_PLUGIN=aqua"); 135 | commandParts.push("-e"); 136 | commandParts.push("AQUA_KEY=" + aquaKey); 137 | commandParts.push("-e"); 138 | commandParts.push("AQUA_SECRET=" + aquaSecret); 139 | commandParts.push("-e"); 140 | commandParts.push("CSPM_URL=" + aquaCSPMUrl); 141 | } 142 | 143 | 144 | commandParts.push("aquasec/trivy"); 145 | commandParts.push("--quiet"); 146 | commandParts.push("image") 147 | if (SBOMOutput) { 148 | commandParts.push("-f=cyclonedx") 149 | } else { 150 | commandParts.push("-f=json") 151 | } 152 | 153 | if (fixedOnly && !SBOMOutput) { 154 | commandParts.push("--ignore-unfixed"); 155 | } 156 | commandParts.push(scanImage); 157 | ({ stdout, stderr } = await runTrivy(commandParts, stdout, stderr)); 158 | } 159 | 160 | async function runTrivy(commandParts: string[], stdout: string, stderr: string) { 161 | SendMetric("trivy_scan_initiated", { imageName: scanImage }); 162 | await window.ddClient.docker.cli.exec( 163 | "run", commandParts, 164 | { 165 | stream: { 166 | onOutput(data: any) { 167 | stdout += data.stdout; 168 | if (data.stderr) { 169 | stderr += data.stderr; 170 | } 171 | }, 172 | onError(error: any) { 173 | window.ddClient.desktopUI.toast.error( 174 | `An error occurred while scanning ${scanImage}` 175 | ); 176 | console.error(error); 177 | }, 178 | onClose(exitCode: number) { 179 | setLoadingWait(false); 180 | setDisableScan(false); 181 | var res = { stdout: stdout, stderr: stderr }; 182 | if (exitCode === 0) { 183 | if (uploadAqua && !SBOMOutput) { 184 | window.ddClient.desktopUI.toast.success( 185 | `Results successfully uploaded to Aqua` 186 | ); 187 | } 188 | SendMetric("trivy_scan_succeeded", { imageName: scanImage }); 189 | processResult(res); 190 | } else { 191 | SendMetric("trivy_scan_failed", { imageName: scanImage }); 192 | window.ddClient.desktopUI.toast.error( 193 | `An error occurred while scanning ${scanImage}: ${res.stderr}` 194 | ); 195 | } 196 | }, 197 | }, 198 | } 199 | ); 200 | return { stdout, stderr }; 201 | } 202 | 203 | const processResult = (res: any) => { 204 | 205 | let vulns = []; 206 | if (res.stderr !== "") { 207 | return; 208 | } 209 | 210 | let output = res.stdout; 211 | 212 | if (uploadAqua) { 213 | output = output.slice(output.indexOf('{')); 214 | } 215 | 216 | console.log(output); 217 | var results = JSON.parse(output); 218 | 219 | if (SBOMOutput) { 220 | setShowSBOM("block"); 221 | setSBOMContent(results); 222 | return; 223 | } 224 | 225 | 226 | 227 | if (results.Results === undefined) { 228 | setVulnerabilities([]); 229 | setShowFilter("none"); 230 | setShowSuccess("block"); 231 | return; 232 | } 233 | 234 | let all = 0; 235 | let critical = 0; 236 | let high = 0; 237 | let medium = 0; 238 | let low = 0; 239 | let unknown = 0; 240 | 241 | 242 | for (let i = 0; i < results.Results.length; i++) { 243 | let r = results.Results[i]; 244 | if (r.Vulnerabilities === undefined) { 245 | continue; 246 | } 247 | for (let j = 0; j < r.Vulnerabilities.length; j++) { 248 | let v = r.Vulnerabilities[j]; 249 | vulns.push(new TrivyVulnerability(v)); 250 | all += 1; 251 | switch (v.Severity) { 252 | case "CRITICAL": 253 | critical += 1; 254 | break; 255 | case "HIGH": 256 | high += 1; 257 | break; 258 | case "MEDIUM": 259 | medium += 1; 260 | break; 261 | case "LOW": 262 | low += 1; 263 | break; 264 | default: 265 | unknown += 1; 266 | } 267 | } 268 | } 269 | 270 | setAll(all); 271 | setCritical(critical); 272 | setHigh(high); 273 | setMedium(medium); 274 | setLow(low); 275 | setUnknown(unknown); 276 | 277 | vulns.sort((a, b) => { 278 | if (getSeverityOrdering(a.severity) === getSeverityOrdering(b.severity)) { 279 | return a.pkgName >= b.pkgName ? 1 : -1; 280 | } 281 | return getSeverityOrdering(a.severity) >= getSeverityOrdering(b.severity) 282 | ? 1 283 | : -1; 284 | }); 285 | 286 | if (all === 0) { 287 | console.debug("No results, showing the success screen"); 288 | setShowSuccess("block"); 289 | setShowFilter("none"); 290 | } else { 291 | setShowFilter("block"); 292 | } 293 | setAllVulnerabilities(vulns); 294 | setVulnerabilities(vulns); 295 | } 296 | 297 | const runScan = () => { 298 | setLoadingWait(true); 299 | setShowDefaultDisplay("block"); 300 | setShowWelcome("none"); 301 | triggerTrivy(); 302 | } 303 | 304 | const resetUI = () => { 305 | setAll(0); 306 | setCritical(0); 307 | setHigh(0); 308 | setMedium(0); 309 | setLow(0); 310 | setUnknown(0); 311 | setVulnerabilities([]); 312 | setShowSuccess("none"); 313 | setShowSBOM("none"); 314 | setShowFilter("none"); 315 | } 316 | 317 | const triggerFilter = (e: React.MouseEvent, obj: string) => { 318 | setSeverityFilter(obj); 319 | if (obj === "all") { 320 | setVulnerabilities(allVulnerabilities); 321 | return; 322 | } 323 | const filtered = allVulnerabilities.filter((v) => v.severityClass === obj); 324 | 325 | setVulnerabilities(filtered); 326 | } 327 | 328 | const imageUpdated = () => { 329 | resetUI(); 330 | } 331 | 332 | React.useEffect(() => { 333 | if (aquaKey !== "" && aquaSecret !== "") { 334 | setShowUploadAqua(""); 335 | } else { 336 | setShowUploadAqua("none"); 337 | } 338 | }, [aquaKey, aquaSecret]); 339 | 340 | React.useEffect(() => { 341 | window.ddClient.extension.vm.service.get("/credentials").then((value: any) => { 342 | setAquaKey(value.aqua_key); 343 | setAquaSecret(value.aqua_secret); 344 | setAquaCSPMUrl(value.aqua_cspm_url); 345 | if (value.aqua_key !== "" && value.aqua_secret !== "" && value.aqua_cspm_url !== "") { 346 | setLoggedIn(true); 347 | } 348 | }).catch((err: any) => { 349 | console.log(err); 350 | }); 351 | SendMetric("trivy_extension_opened", {}); 352 | }, []); 353 | 354 | 355 | return ( 356 | 357 |
358 | 359 | {/* Entry point to the extension - large hero with description and scan box */} 360 | 372 | 373 | 391 | {/* Top level interaction point - hidden when the welcome screen is displayed */} 392 | 408 | 412 | {/* Table of vulnerabilities with the filter control included in this component */} 413 | 428 | {/* Component that is displayed when the scan completes without issue */} 429 | 433 | {/* Shim to block the screen when the scan is loading */} 434 | 437 | 438 |
439 |
440 | ); 441 | } -------------------------------------------------------------------------------- /client/src/ConfigureCreds.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | import TextField from '@mui/material/TextField'; 5 | import Dialog from '@mui/material/Dialog'; 6 | import DialogActions from '@mui/material/DialogActions'; 7 | import DialogContent from '@mui/material/DialogContent'; 8 | import DialogContentText from '@mui/material/DialogContentText'; 9 | import DialogTitle from '@mui/material/DialogTitle'; 10 | import { SendMetric } from './Metrics'; 11 | 12 | export function ConfigureCreds(props: any) { 13 | 14 | let loginDisplay = !props.loggedIn ? "flex" : "none"; 15 | let logoutDisplay = props.loggedIn ? "block" : "none"; 16 | 17 | const handleSignInClick = () => { 18 | props.setOpen(true); 19 | }; 20 | 21 | const handleClose = () => { 22 | props.setOpen(false); 23 | }; 24 | 25 | const handleSaveDetails = () => { 26 | let payload = { aqua_key: props.aquaKey, aqua_secret: props.aquaSecret, aqua_cspm_url: props.aquaCSPMUrl }; 27 | console.log(payload); 28 | window.ddClient.extension.vm.service.request({ url: "/credentials", method: "POST", headers: { 'Content-Type': 'application/json' }, data: payload }) 29 | .then(() => { 30 | window.ddClient.desktopUI.toast.success( 31 | `Successfully logged in` 32 | ); 33 | SendMetric("trivy_aqua_login_successful", { aquaKey: props.aquaKey }); 34 | props.setLoggedIn(true); 35 | props.setOpen(false); 36 | }) 37 | .catch((error: any) => { 38 | window.ddClient.desktopUI.toast.error( 39 | `Failed to validate login credentials` 40 | ); 41 | SendMetric("trivy_aqua_login_failed", { aquaKey: props.aquaKey }); 42 | props.setAquaKey(""); 43 | props.setAquaSecret(""); 44 | props.setAquaCSPMUrl(""); 45 | console.log(error); 46 | }); 47 | }; 48 | 49 | const handleSignOutClick = () => { 50 | props.setAquaKey(""); 51 | props.setAquaSecret(""); 52 | props.setAquaCSPMUrl(""); 53 | props.setLoggedIn(false); 54 | 55 | let payload = { aqua_key: "", aqua_secret: "" }; 56 | console.log(payload); 57 | window.ddClient.extension.vm.service.delete("/credentials") 58 | .then(() => { 59 | window.ddClient.desktopUI.toast.success( 60 | `Successfully logged out` 61 | ); 62 | props.setOpen(false); 63 | }) 64 | .catch((error: any) => { 65 | window.ddClient.desktopUI.toast.error( 66 | `Failed to logout` 67 | ); 68 | console.log(error); 69 | }); 70 | }; 71 | 72 | const handleAVDLinkClick = (e: any) => { 73 | { window.ddClient.host.openExternal("https://cloud.aquasec.com/cspm/#/apikeys") }; 74 | } 75 | 76 | return ( 77 | 78 | 81 | 84 | 85 | Login 86 | 87 | 88 | Please enter your Aqua Security API credentials, these are available in your Aqua CSPM Account 89 | 90 | props.setAquaKey(e.target.value)} 97 | fullWidth 98 | variant="standard" 99 | helperText="AQUA_KEY provided in your CSPM account" 100 | /> 101 | props.setAquaSecret(e.target.value)} 108 | fullWidth 109 | variant="standard" 110 | helperText="AQUA_SECRET provided in your CSPM account" 111 | /> 112 | props.setAquaCSPMUrl(e.target.value)} 119 | fullWidth 120 | variant="standard" 121 | helperText="AQUA_CSPM_URL provided in your CSPM account" 122 | /> 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | ); 133 | } -------------------------------------------------------------------------------- /client/src/DefaultDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import { Box } from '@mui/system'; 3 | 4 | import { ImageList } from './ImageList'; 5 | import { Links } from './Links'; 6 | 7 | 8 | export function DefaultDisplay(props: any) { 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | Trivy Logo 16 | 17 | 18 | aqua 19 | 20 | 21 | trivy 22 | 23 | 24 | 25 | 26 | 27 | 42 | 43 | ) 44 | } -------------------------------------------------------------------------------- /client/src/ImageList.tsx: -------------------------------------------------------------------------------- 1 | import Autocomplete from '@mui/material/Autocomplete'; 2 | import Button from '@mui/material/Button'; 3 | import FormControlLabel from '@mui/material/FormControlLabel'; 4 | import FormGroup from '@mui/material/FormGroup'; 5 | import Switch from '@mui/material/Switch'; 6 | import TextField from '@mui/material/TextField'; 7 | import { Box } from '@mui/system'; 8 | import React from 'react'; 9 | 10 | export function ImageList(props: any) { 11 | const [open, setOpen] = React.useState(false); 12 | const [scanTriggered, setScanTriggered] = React.useState(false); 13 | const [loaded, setLoaded] = React.useState(false); 14 | const [images, setImages] = React.useState([]); 15 | const loading = open && !loaded; 16 | const ignoredImages = ["aquasec/trivy", "trivy-docker-extension"]; 17 | 18 | 19 | React.useEffect(() => { 20 | let active = true; 21 | 22 | if (!loading) { 23 | setImages([]); 24 | return; 25 | } 26 | 27 | (async () => { 28 | if (active) { 29 | setLoaded(true); 30 | loadImages(); 31 | } 32 | })(); 33 | 34 | return () => { 35 | active = false; 36 | }; 37 | }, [loading]); 38 | 39 | React.useEffect(() => { 40 | if (!open) { 41 | loadImages(); 42 | } 43 | }, [open]); 44 | 45 | const loadImages = () => { 46 | let images = []; 47 | try { 48 | images = window.ddClient.docker.listImages(); 49 | } catch (imageResp) { 50 | return images; 51 | } 52 | 53 | Promise.resolve(images).then(images => { 54 | console.log(images); 55 | if (images === null || images === undefined || images.length === 0) { 56 | setImages([]); 57 | return 58 | } 59 | const listImages = images.map((images: any) => images.RepoTags) 60 | .sort() 61 | .filter((images: any) => images && ":" !== images[0]) 62 | .filter((images: any) => { 63 | for (let i = 0; i < ignoredImages.length; i++) { 64 | if (images[0].startsWith(ignoredImages[i])) { 65 | return false; 66 | } 67 | } 68 | return true; 69 | }) 70 | .flat(); 71 | 72 | if (listImages.length == 0) { 73 | 74 | } 75 | 76 | setImages(listImages); 77 | }) 78 | } 79 | 80 | const triggerScan = () => { 81 | // disable the scan button as a priority 82 | props.setSBOMOutput(false); 83 | props.setDisableScan(true); 84 | setScanTriggered(true); 85 | } 86 | 87 | React.useEffect(() => { 88 | if (props.scanImage !== "") { 89 | props.runScan(); 90 | } 91 | }, [props.fixedOnly]); 92 | 93 | 94 | React.useEffect(() => { 95 | if (scanTriggered && !props.SBOMOutput && props.scanImage !== "") { 96 | props.runScan(); 97 | setScanTriggered(false); 98 | } 99 | }, [scanTriggered]); 100 | 101 | const toggleFixedOnly = () => { 102 | props.imageUpdated(); 103 | props.setFixedOnly(!props.fixedOnly); 104 | } 105 | 106 | const toggleUploadAqua = () => { 107 | props.setUploadAqua(!props.uploadAqua); 108 | } 109 | 110 | const handleKeyDown = (event: React.KeyboardEvent) => { 111 | props.setDisableScan(false); 112 | switch (event.key) { 113 | case "Tab": { 114 | handleChange(event, event.currentTarget.value); 115 | break; 116 | } 117 | default: 118 | } 119 | }; 120 | 121 | const handleChange = (e: React.ChangeEvent<{}>, obj: string) => { 122 | props.imageUpdated(); 123 | props.setScanImage(obj); 124 | if (obj && obj !== "No images found") { 125 | props.setDisableScan(false); 126 | } else { 127 | props.setDisableScan(true); 128 | } 129 | } 130 | 131 | const handleInputChange = (e: React.SyntheticEvent<{}>, obj: string) => { 132 | props.imageUpdated(); 133 | props.setScanImage(obj); 134 | if (obj && obj !== "No images found") { 135 | props.setDisableScan(false); 136 | } else { 137 | props.setDisableScan(true); 138 | } 139 | } 140 | 141 | 142 | return ( 143 | 144 | 145 | { 152 | setOpen(true); 153 | }} 154 | onClose={() => { 155 | setOpen(false); 156 | }} 157 | loading={loading} 158 | noOptionsText="No local images found" 159 | renderInput={(params) => { 160 | params.inputProps.onKeyDown = handleKeyDown; 161 | return (); 165 | } 166 | } 167 | onChange={handleChange} 168 | onInputChange={handleInputChange} 169 | /> 170 | 171 | 177 | 178 | 179 | } label="Only show fixed vulnerabilities" /> 180 | } label="Upload results" /> 181 | 182 | 183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /client/src/Links.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Box } from '@mui/system'; 3 | import Chip from '@mui/material/Chip'; 4 | import { GitHub, MenuBook, Forum } from '@mui/icons-material'; 5 | 6 | export function Links() { 7 | 8 | const handleGithub = () => handleClick("https://github.com/aquasecurity/trivy") 9 | const handleDocumentation = () => handleClick("https://aquasecurity.github.io/trivy") 10 | const handleSlack = () => handleClick("https://aquasec.slack.com") 11 | 12 | 13 | const handleClick = (url: string) => { 14 | { window.ddClient.host.openExternal(url) }; 15 | } 16 | 17 | return ( 18 | 19 | } onClick={handleGithub} label="View in GitHub" variant="outlined" sx={{ marginRight: '0.3rem' }} /> 20 | } onClick={handleDocumentation} label="View Documentation" variant="outlined" sx={{ marginRight: '0.3rem' }} /> 21 | } onClick={handleSlack} label="Join the Community" variant="outlined" /> 22 | 23 | ) 24 | } -------------------------------------------------------------------------------- /client/src/Loading.tsx: -------------------------------------------------------------------------------- 1 | import Backdrop from '@mui/material/Backdrop'; 2 | import CircularProgress from '@mui/material/CircularProgress'; 3 | 4 | export function Loading(props: any) { 5 | 6 | return ( 7 | theme.zIndex.drawer + 1 }} 8 | open={props.showLoading} > 9 | 10 | ) 11 | }; -------------------------------------------------------------------------------- /client/src/Metrics.tsx: -------------------------------------------------------------------------------- 1 | var clientID = ""; 2 | 3 | function send(eventName: string, clientID: string, payload: Object) { 4 | fetch(`https://c0c79hb7vh.execute-api.us-east-1.amazonaws.com/dev/ddCapture`, { 5 | method: "POST", 6 | body: JSON.stringify({ 7 | client_id: clientID, 8 | user_id: clientID, 9 | events: [{ 10 | name: eventName, 11 | params: payload, 12 | }] 13 | }) 14 | }).catch((err: any) => { console.log(err); }); 15 | } 16 | 17 | export function SendMetric(eventName: string, payload: Object) { 18 | if (clientID !== "") { 19 | send(eventName, clientID, payload); 20 | } else { 21 | 22 | window.ddClient.docker.cli.exec("info", ["--format", "'{{json .}}'"]) 23 | .then((result: any) => { 24 | if (result.stderr === "") { 25 | const info = JSON.parse(result.stdout); 26 | clientID = info.ID; 27 | send(eventName, info.ID, payload); 28 | } 29 | }).catch((err: any) => { 30 | console.log(err); 31 | }) 32 | 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/Pill.tsx: -------------------------------------------------------------------------------- 1 | import Chip from "@mui/material/Chip"; 2 | 3 | export function Pill(props: any) { 4 | 5 | const getSeverity = (severity: string): string => { 6 | switch (severity) { 7 | case "CRITICAL": 8 | return "red"; 9 | case "HIGH": 10 | return "orangered"; 11 | case "MEDIUM": 12 | return "orange"; 13 | case "LOW": 14 | return "gray"; 15 | default: 16 | return "gray"; 17 | } 18 | } 19 | 20 | return ( 21 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /client/src/SBOM.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Box, Button, Card, CardContent, Stack } from '@mui/material'; 3 | 4 | 5 | export function SBOM(props: any) { 6 | 7 | 8 | const saveToClipboard = () => { 9 | navigator.clipboard.writeText(JSON.stringify(props.SBOMContent, null, 2)); 10 | } 11 | 12 | const saveSBOMToFile = () => { 13 | const element = document.createElement("a"); 14 | const file = new Blob([JSON.stringify(props.SBOMContent, null, 2)], { 15 | type: "text/plain" 16 | }); 17 | element.href = URL.createObjectURL(file); 18 | element.download = "sbom.json"; 19 | document.body.appendChild(element); 20 | element.click(); 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |                         {JSON.stringify(props.SBOMContent, null, 2)}
33 |                     
34 |
35 |
36 |
37 | ) 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /client/src/Success.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, Typography } from "@mui/material"; 2 | import { Box } from "@mui/system"; 3 | 4 | 5 | 6 | export function Success(props: any) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | Great News! 14 | 15 | Tada Logo 16 | 17 | 18 | 19 | No vulnerabilities were found in {props.scanImage} 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } -------------------------------------------------------------------------------- /client/src/TrivyVulnerability.tsx: -------------------------------------------------------------------------------- 1 | export class TrivyVulnerability { 2 | id: string 3 | title: string 4 | severity: string 5 | severityClass: string 6 | description: string 7 | pkgName: string 8 | installedVersion: string 9 | fixedVersion: string 10 | references: string[] 11 | primaryURL: string 12 | visible: boolean 13 | constructor(v: any) { 14 | this.id = v.VulnerabilityID; 15 | this.title = v.Title; 16 | this.severity = v.Severity; 17 | this.severityClass = v.Severity.toLowerCase(); 18 | this.description = v.Description; 19 | this.pkgName = v.PkgName; 20 | this.installedVersion = v.InstalledVersion ? v.InstalledVersion : ""; 21 | this.fixedVersion = v.FixedVersion ? v.FixedVersion : ""; 22 | this.references = v.References; 23 | this.primaryURL = v.PrimaryURL; 24 | this.visible = false; 25 | } 26 | } -------------------------------------------------------------------------------- /client/src/Vulns.tsx: -------------------------------------------------------------------------------- 1 | import Accordion from '@mui/material/Accordion'; 2 | import AccordionDetails from '@mui/material/AccordionDetails'; 3 | import AccordionSummary from '@mui/material/AccordionSummary'; 4 | import Typography from '@mui/material/Typography'; 5 | import { Pill } from './Pill'; 6 | import Table from '@mui/material/Table'; 7 | import TableCell from '@mui/material/TableCell'; 8 | 9 | import TableRow from '@mui/material/TableRow'; 10 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 11 | import { Box } from '@mui/system'; 12 | import { ReactChild, ReactFragment, ReactPortal } from 'react'; 13 | import { VulnsFilter } from './VulnsFilter'; 14 | import { Button } from '@mui/material'; 15 | import React from 'react'; 16 | 17 | 18 | export function Vulns(props: any) { 19 | const v = props.vulnerabilties; 20 | 21 | const handleAVDLinkClick = (e: any) => { 22 | { window.ddClient.host.openExternal(e.target.innerText) }; 23 | } 24 | 25 | const generateSBOM = () => { 26 | props.setSBOMOutput(true); 27 | } 28 | 29 | React.useEffect(() => { 30 | if (props.SBOMOutput) { 31 | props.runScan(); 32 | } 33 | }, [props.SBOMOutput]); 34 | 35 | 36 | return ( 37 | 38 | 49 | 55 | 56 | 57 | {v.map((row: { id: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; title: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; description: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; pkgName: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; installedVersion: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; fixedVersion: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; primaryURL: boolean | ReactChild | ReactFragment | ReactPortal | null | undefined; }) => ( 58 | 59 | } 62 | > 63 | 64 | 65 | 66 | {row.id} 67 | 68 | 69 | {row.pkgName} 70 | 71 | 72 | 73 | 74 | {row.title} 75 | 76 | 77 | {row.description} 78 | 79 | 80 | 81 | 82 | 83 | Package Name: 84 | 85 | 86 | 87 | 88 | {row.pkgName} 89 | 90 | 91 | 92 | 93 | 94 | 95 | Installed Version: 96 | 97 | 98 | 99 | 100 | {row.installedVersion} 101 | 102 | 103 | 104 | 105 | 106 | 107 | Fixed Version: 108 | 109 | 110 | 111 | 112 | {row.fixedVersion} 113 | 114 | 115 | 116 | 117 | 118 | 119 | More Info: 120 | 121 | 122 | 123 | 124 | {row.primaryURL} 125 | 126 | 127 | 128 |
129 |
130 |
131 | )) 132 | } 133 |
134 |
135 | ) 136 | 137 | } -------------------------------------------------------------------------------- /client/src/VulnsFilter.tsx: -------------------------------------------------------------------------------- 1 | import { ToggleButtonGroup, ToggleButton } from "@mui/material"; 2 | import { Box } from "@mui/system"; 3 | 4 | 5 | export function VulnsFilter(props: any) { 6 | return ( 7 | 13 | All ({props.all}) 14 | 16 | Critical ({props.critical}) 17 | 19 | High ({props.high}) 20 | 22 | Medium ({props.medium}) 23 | 25 | Low ({props.low}) 26 | 28 | Unknown ({props.unknown}) 29 | 30 | 31 | ) 32 | } -------------------------------------------------------------------------------- /client/src/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, Typography, Button } from "@mui/material"; 2 | import { Box } from "@mui/system"; 3 | import { ImageList } from './ImageList'; 4 | 5 | 6 | export function Welcome(props: any) { 7 | let showLoginHelp = props.loggedIn ? 'none' : 'flex'; 8 | 9 | const goToTrivy = () => { 10 | window.ddClient.host.openExternal("https://trivy.dev") 11 | } 12 | 13 | return ( 14 | 17 | 18 | Trivy Logo 19 | 20 | 21 | aqua 22 | 23 | 24 | trivy 25 | 26 | 27 | 28 | 29 | Free, open-source container image scanning. 30 | 31 | 32 | Tada Logo Scan unlimited images, no sign up required! Scans run on your machine! 33 | 34 | 35 | Select from a local stored image or enter the name of a remote image you wish to scan. 36 | 37 | 38 | 39 | 55 | 56 | 57 | 58 | 59 | 60 | New to Trivy? 61 | 62 | 63 | 64 | 65 | Aqua Customer? 66 | 67 | 68 | 69 | 70 | 71 | 72 | ) 73 | } -------------------------------------------------------------------------------- /client/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | ddClient: any; 3 | } 4 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { App } from './App'; 5 | 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/trivy-docker-extension 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.5.2 7 | github.com/labstack/echo v3.3.10+incompatible 8 | github.com/pkg/errors v0.9.1 9 | github.com/sirupsen/logrus v1.7.0 10 | github.com/stretchr/testify v1.7.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/labstack/gommon v0.3.1 // indirect 16 | github.com/mattn/go-colorable v0.1.11 // indirect 17 | github.com/mattn/go-isatty v0.0.14 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/valyala/bytebufferpool v1.0.0 // indirect 20 | github.com/valyala/fasttemplate v1.2.1 // indirect 21 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect 22 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 23 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect 24 | golang.org/x/text v0.3.6 // indirect 25 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= 2 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= 7 | github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 8 | github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= 9 | github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 10 | github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= 11 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 12 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 13 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 19 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 24 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 26 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 27 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 28 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 29 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= 30 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 31 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 32 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 33 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= 41 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 43 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 44 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 50 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | 4 | IF /I "%1"=="build-app" GOTO build-app 5 | IF /I "%1"=="build-dev" GOTO build-dev 6 | IF /I "%1"=="deploy-dev" GOTO deploy-dev 7 | IF /I "%1"=="dev-debug" GOTO dev-debug 8 | IF /I "%1"=="dev-reset" GOTO dev-reset 9 | GOTO error 10 | 11 | :build-app 12 | @npm run-script build 13 | GOTO :EOF 14 | 15 | :build-dev 16 | CALL make.bat build-app 17 | @docker build -t trivy-docker-extension:development . 18 | GOTO :EOF 19 | 20 | :deploy-dev 21 | CALL make.bat build-dev 22 | @docker extension rm trivy-docker-extension:development || true 23 | @docker extension install trivy-docker-extension:development 24 | GOTO :EOF 25 | 26 | :dev-debug 27 | @docker extension dev debug trivy-docker-extension:development 28 | GOTO :EOF 29 | 30 | :dev-reset 31 | GOTO :EOF 32 | 33 | :error 34 | IF "%1"=="" ( 35 | ECHO make: *** No targets specified and no makefile found. Stop. 36 | ) ELSE ( 37 | ECHO make: *** No rule to make target '%1%'. Stop. 38 | ) 39 | GOTO :EOF 40 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Aqua Trivy", 3 | "provider": "Aqua Security", 4 | "icon": "trivy.svg", 5 | "vm": { 6 | "composefile": "docker-compose.yaml", 7 | "exposes": { 8 | "socket": "plugin-trivy.sock" 9 | } 10 | }, 11 | "ui": { 12 | "dashboard-tab": { 13 | "title": "Trivy", 14 | "root": "/ui", 15 | "src": "index.html", 16 | "backend": { 17 | "socket": "plugin-trivy.sock" 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /metrics/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | sam build 5 | -------------------------------------------------------------------------------- /metrics/README.md: -------------------------------------------------------------------------------- 1 | # trivy-dd-metrics 2 | -------------------------------------------------------------------------------- /metrics/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aquasecurity/trivy-dd-metrics 2 | 3 | go 1.17 4 | 5 | require github.com/aws/aws-lambda-go v1.30.0 6 | -------------------------------------------------------------------------------- /metrics/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-lambda-go v1.30.0 h1:qelHgOUidrQmrfFTLiC7u6wWuuwBJ9yKcjVRkIy7834= 2 | github.com/aws/aws-lambda-go v1.30.0/go.mod h1:IF5Q7wj4VyZyUFnZ54IQqeWtctHQ9tz+KhcbDenr220= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 10 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 14 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /metrics/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | 9 | "log" 10 | 11 | "github.com/aws/aws-lambda-go/events" 12 | "github.com/aws/aws-lambda-go/lambda" 13 | ) 14 | 15 | func handleRequest(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 16 | apiResponse := events.APIGatewayProxyResponse{} 17 | 18 | measurementID := os.Getenv("MEASUREMENT_ID") 19 | secretKey := os.Getenv("API_SECRET") 20 | 21 | requestUrl := fmt.Sprintf("https://www.google-analytics.com/mp/collect?measurement_id=%s&api_secret=%s", measurementID, secretKey) 22 | req, err := http.NewRequest(http.MethodPost, requestUrl, strings.NewReader(request.Body)) 23 | if err != nil { 24 | return apiResponse, err 25 | } 26 | 27 | resp, err := http.DefaultClient.Do(req) 28 | if err != nil { 29 | log.Default().Fatal(err) 30 | return apiResponse, err 31 | } 32 | 33 | if resp.StatusCode != http.StatusNoContent { 34 | log.Default().Printf("Status code for %s is %d", request.Body, resp.StatusCode) 35 | } 36 | 37 | apiResponse.StatusCode = resp.StatusCode 38 | return apiResponse, nil 39 | } 40 | 41 | func main() { 42 | lambda.Start(handleRequest) 43 | } 44 | -------------------------------------------------------------------------------- /metrics/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | trivy-dd-metrics 5 | 6 | Globals: 7 | Function: 8 | Timeout: 5 9 | 10 | Resources: 11 | TrivyAPI: 12 | Type: AWS::Serverless::Api 13 | Properties: 14 | StageName: dev 15 | Cors: 16 | AllowMethods: "'POST'" 17 | AllowOrigin: "'*'" 18 | OpenApiVersion: "2.0" 19 | Auth: 20 | ApiKeyRequired: false 21 | 22 | TrivyDDMetricFunction: 23 | Type: AWS::Serverless::Function 24 | Properties: 25 | FunctionName: Trivy_DockerDesktop_Metrics 26 | CodeUri: ./ 27 | Handler: trivy-dd-metrics 28 | Runtime: go1.x 29 | Architectures: 30 | - x86_64 31 | Tracing: Active 32 | Events: 33 | CatchAll: 34 | Type: Api 35 | Properties: 36 | Path: /ddCapture 37 | Method: POST 38 | RestApiId: !Ref TrivyAPI 39 | Environment: 40 | Variables: 41 | MEASUREMENT_ID: VALUE 42 | API_SECRET: VALUE 43 | 44 | Outputs: 45 | TrivyDDMetricsAPI: 46 | Description: "API Gateway endpoint URL for Trivy Docker Desktop Metric Gathering" 47 | Value: !Sub "https://${TrivyAPI}.execute-api.${AWS::Region}.amazonaws.com/dev/ddCapture/" 48 | TrivyDDFunction: 49 | Description: "Trivy Docker Metrics Function" 50 | Value: !GetAtt TrivyDDMetricFunction.Arn 51 | TrivyDDFunctionIamRole: 52 | Description: "Implicit IAM Role created for Trivy DD Function" 53 | Value: !GetAtt TrivyDDMetricFunctionRole.Arn 54 | -------------------------------------------------------------------------------- /service/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | trivy_aqua_creds: 5 | 6 | services: 7 | app: 8 | image: ${DESKTOP_PLUGIN_IMAGE} 9 | cap_add: 10 | - DAC_OVERRIDE 11 | - FOWNER 12 | volumes: 13 | - trivy_aqua_creds:/creds 14 | -------------------------------------------------------------------------------- /service/internal/auth/validate.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | type Response struct { 16 | Status int `json:"status"` 17 | Message string `json:"message"` 18 | Data string `json:"data,omitempty"` 19 | Errors []string `json:"errors,omitempty"` 20 | } 21 | 22 | const cspmTokenExchangePath = "/v2/tokens" 23 | 24 | func ValidateCredentials(key, secret, cspmUrl string) (string, error) { 25 | body := `{"validity":30,"allowed_endpoints":["ANY:v2/build/twirp/buildsecurity.BuildSecurity/*"]}` 26 | 27 | req, err := http.NewRequest("POST", cspmUrl+cspmTokenExchangePath, bytes.NewBuffer([]byte(body))) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | timestampString := strconv.Itoa(int(time.Now().Unix())) 33 | someString := timestampString + "POST/v2/tokens" + body 34 | signature, err := ComputeHmac256(someString, secret) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | req.Header.Add("x-signature", signature) 40 | req.Header.Add("x-timestamp", timestampString) 41 | req.Header.Add("x-api-key", key) 42 | 43 | client := &http.Client{} 44 | resp, err := client.Do(req) 45 | 46 | if err != nil { 47 | return "", fmt.Errorf("failed sending jwt request token with error: %w", err) 48 | } 49 | 50 | defer func() { _ = resp.Body.Close() }() 51 | 52 | var response Response 53 | err = json.NewDecoder(resp.Body).Decode(&response) 54 | if err != nil { 55 | return "", fmt.Errorf("failed decoding response with error: %w", err) 56 | } 57 | 58 | if response.Status != 200 { 59 | var e = "unknown error" 60 | if len(response.Errors) > 0 { 61 | e = response.Errors[0] 62 | } 63 | return "", fmt.Errorf("failed to generate Aqua token with error: %s, %s", response.Message, e) 64 | } 65 | return response.Data, nil 66 | } 67 | 68 | func ComputeHmac256(message string, secret string) (string, error) { 69 | key := []byte(secret) 70 | h := hmac.New(sha256.New, key) 71 | _, err := h.Write([]byte(message)) 72 | if err != nil { 73 | return "", fmt.Errorf("failed compute hmac: %w", err) 74 | } 75 | return hex.EncodeToString(h.Sum(nil)), nil 76 | } 77 | -------------------------------------------------------------------------------- /service/internal/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // ListenFD listens to a file descriptor. 16 | func ListenFD(filedes string) (net.Listener, error) { 17 | fd, err := strconv.Atoi(filedes) 18 | if err != nil { 19 | return nil, errors.Wrapf(err, "cannot parse file descriptor: %s", filedes) 20 | } 21 | file := os.NewFile(uintptr(fd), fmt.Sprintf("fd:%v", filedes)) 22 | res, err := net.FileListener(file) 23 | if err != nil { 24 | return nil, errors.Wrapf(err, "cannot convert fd %v to net.Listener", fd) 25 | } 26 | return res, nil 27 | } 28 | 29 | // Listen wraps net.Listen, preferring ListenUnix when applicable, and 30 | // offers support for the "fd" network type, in which case address is 31 | // a file descriptor. 32 | func Listen(network, address string) (net.Listener, error) { 33 | switch network { 34 | case "fd": 35 | return ListenFD(address) 36 | case "unix": 37 | return ListenUnix(address) 38 | default: 39 | return net.Listen(network, address) 40 | } 41 | } 42 | 43 | // ListenOn splits a "network:address" string to invoke Listen. 44 | func ListenOn(addr string) (net.Listener, error) { 45 | ss := strings.SplitN(addr, ":", 2) 46 | if len(ss) < 2 { 47 | return nil, errors.Errorf("invalid listener address: %v", addr) 48 | } 49 | return Listen(ss[0], ss[1]) 50 | } 51 | 52 | // Client Creates a client connected to the specified socket 53 | func Client(addr string) *http.Client { 54 | return &http.Client{ 55 | Transport: &http.Transport{ 56 | DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { 57 | return DialSocket(addr) 58 | }, 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /service/internal/socket/socket_darwin.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | const maxUnixSocketPathLen = 104 - 1 // NULL 4 | -------------------------------------------------------------------------------- /service/internal/socket/socket_linux.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | const maxUnixSocketPathLen = 108 - 1 // NULL 4 | -------------------------------------------------------------------------------- /service/internal/socket/socket_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package socket 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | // ListenUnix wraps `net.ListenUnix`. 14 | func ListenUnix(path string) (*net.UnixListener, error) { 15 | if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 16 | return nil, err 17 | } 18 | // Make sure the parent directory exists. 19 | dir := filepath.Dir(path) 20 | if err := os.MkdirAll(dir, 0755); err != nil { 21 | return nil, err 22 | } 23 | short, err := shortenUnixSocketPath(path) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return net.ListenUnix("unix", &net.UnixAddr{Name: short, Net: "unix"}) 28 | } 29 | 30 | func DialSocket(socket string) (net.Conn, error) { 31 | return net.Dial("unix", socket) 32 | } 33 | 34 | func shortenUnixSocketPath(path string) (string, error) { 35 | if len(path) <= maxUnixSocketPathLen { 36 | return path, nil 37 | } 38 | // absolute path is too long, attempt to use a relative path 39 | p, err := relative(path) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | if len(p) > maxUnixSocketPathLen { 45 | return "", fmt.Errorf("absolute and relative socket path %s longer than %d characters", p, maxUnixSocketPathLen) 46 | } 47 | return p, nil 48 | } 49 | 50 | func relative(p string) (string, error) { 51 | // Assume the parent directory exists already but the child (the socket) 52 | // hasn't been created. 53 | path2, err := filepath.EvalSymlinks(filepath.Dir(p)) 54 | if err != nil { 55 | return "", err 56 | } 57 | dir, err := os.Getwd() 58 | if err != nil { 59 | return "", err 60 | } 61 | dir2, err := filepath.EvalSymlinks(dir) 62 | if err != nil { 63 | return "", err 64 | } 65 | rel, err := filepath.Rel(dir2, path2) 66 | if err != nil { 67 | return "", err 68 | } 69 | return filepath.Join(rel, filepath.Base(p)), nil 70 | } 71 | -------------------------------------------------------------------------------- /service/internal/socket/socket_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package socket 5 | 6 | import ( 7 | "fmt" 8 | "io/ioutil" 9 | "net" 10 | "os" 11 | "path" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestMaxPathLength(t *testing.T) { 19 | dir, err := ioutil.TempDir("", "test-max-path-length") 20 | require.Nil(t, err) 21 | defer func() { 22 | assert.Nil(t, os.RemoveAll(dir)) 23 | }() 24 | path := path.Join(dir, "socket") 25 | for { 26 | l, err := net.Listen("unix", path) 27 | if err != nil { 28 | if len(path) > maxUnixSocketPathLen { 29 | return 30 | } 31 | fmt.Printf("path length %d is <= maximum %d\n", len(path), maxUnixSocketPathLen) 32 | t.Fail() 33 | return 34 | } 35 | if len(path) > maxUnixSocketPathLen { 36 | fmt.Printf("path length %d is > maximum %d\n", len(path), maxUnixSocketPathLen) 37 | t.Fail() 38 | } 39 | require.Nil(t, l.Close()) 40 | path = path + "1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /service/internal/socket/socket_windows.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/Microsoft/go-winio" 8 | ) 9 | 10 | // ListenUnix wraps `winio.ListenUnix`. 11 | // It provides API compatibility for named pipes with the Unix domain socket API. 12 | func ListenUnix(path string) (net.Listener, error) { 13 | return winio.ListenPipe(path, &winio.PipeConfig{ 14 | MessageMode: true, // Use message mode so that CloseWrite() is supported 15 | InputBufferSize: 65536, // Use 64KB buffers to improve performance 16 | OutputBufferSize: 65536, 17 | }) 18 | } 19 | 20 | func DialSocket(socket string) (net.Conn, error) { 21 | timeout := 1 * time.Second 22 | return winio.DialPipe(socket, &timeout) 23 | } 24 | -------------------------------------------------------------------------------- /service/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/aquasecurity/trivy-docker-extension/service/internal/auth" 13 | "github.com/aquasecurity/trivy-docker-extension/service/internal/socket" 14 | "github.com/labstack/echo" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | const credsFile = "/creds/.aqua" 19 | 20 | type Credentials struct { 21 | AquaKey string `json:"aqua_key"` 22 | AquaSecret string `json:"aqua_secret"` 23 | AquaCSPMUrl string `json:"aqua_cspm_url"` 24 | } 25 | 26 | func main() { 27 | var socketPath = flag.String("socket", "/run/guest-services/plugin-trivy.sock", "Unix domain socket to listen on") 28 | var testPort = flag.Int("testPort", 0, "Test port to expose instead of socket") 29 | flag.Parse() 30 | unixSocket := "unix:" + *socketPath 31 | logrus.Infof("Starting listening on %s", unixSocket) 32 | router := echo.New() 33 | router.HideBanner = true 34 | 35 | startURL := "" 36 | 37 | if *testPort != 0 { 38 | startURL = fmt.Sprintf(":%d", *testPort) 39 | } else { 40 | ln, err := socket.ListenOn(unixSocket) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | router.Listener = ln 45 | } 46 | 47 | router.POST("/credentials", writeCredentials) 48 | router.GET("/credentials", getCredentials) 49 | router.DELETE("/credentials", deleteCredentials) 50 | 51 | log.Fatal(router.Start(startURL)) 52 | } 53 | 54 | func deleteCredentials(ctx echo.Context) error { 55 | logrus.Info("Received delete credentials request") 56 | return os.Remove(credsFile) 57 | } 58 | 59 | func writeCredentials(ctx echo.Context) error { 60 | logrus.Info("Recieved credential write request") 61 | 62 | if err := os.MkdirAll(filepath.Dir(credsFile), os.ModePerm); err != nil { 63 | return internalError(ctx, err) 64 | } 65 | 66 | creds := new(Credentials) 67 | if err := ctx.Bind(creds); err != nil { 68 | return internalError(ctx, err) 69 | } 70 | validated, err := auth.ValidateCredentials(creds.AquaKey, creds.AquaSecret, creds.AquaCSPMUrl) 71 | if err != nil || validated == "" { 72 | return internalError(ctx, err) 73 | } 74 | content, err := json.Marshal(creds) 75 | if err != nil { 76 | return internalError(ctx, err) 77 | } 78 | return os.WriteFile(credsFile, content, os.ModePerm) 79 | } 80 | 81 | func getCredentials(ctx echo.Context) error { 82 | logrus.Info("Recieved credential get request") 83 | var creds Credentials 84 | content, err := os.ReadFile(credsFile) 85 | if err != nil { 86 | return ctx.JSON(http.StatusOK, creds) 87 | } 88 | if err != json.Unmarshal(content, &creds) { 89 | logrus.Errorf("Error occurred while unmarshalling creds file, returning empty creds file: %w", err) 90 | return ctx.JSON(http.StatusOK, creds) 91 | } 92 | return ctx.JSON(http.StatusOK, creds) 93 | } 94 | 95 | func internalError(ctx echo.Context, err error) error { 96 | logrus.Error(err) 97 | return ctx.JSON(http.StatusInternalServerError, HTTPMessageBody{Message: err.Error()}) 98 | } 99 | 100 | type HTTPMessageBody struct { 101 | Message string 102 | } 103 | -------------------------------------------------------------------------------- /trivy.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 49 | 54 | 59 | 64 | 69 | 74 | 79 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 149 | 150 | --------------------------------------------------------------------------------