├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── jenkins-security-scan.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TESTING.md ├── docs └── images │ ├── build_options.png │ ├── build_report.png │ ├── build_summary.png │ ├── freestyle_build_step.png │ └── workflow.png ├── pom.xml └── src └── main ├── java └── com │ └── anchore │ └── jenkins │ └── plugins │ └── anchore │ ├── AnchoreAction.java │ ├── AnchoreBuilder.java │ ├── AnchoreProjectAction.java │ ├── Annotation.java │ ├── BuildConfig.java │ ├── BuildWorker.java │ ├── ConsoleLog.java │ └── Util.java ├── resources ├── com │ └── anchore │ │ └── jenkins │ │ └── plugins │ │ └── anchore │ │ ├── AnchoreAction │ │ ├── index.jelly │ │ └── summary.jelly │ │ ├── AnchoreBuilder │ │ ├── config.jelly │ │ ├── global.jelly │ │ ├── help-anchoreioPass.html │ │ ├── help-anchoreioUser.html │ │ ├── help-anchoreui.html │ │ ├── help-autoSubscribeTagUpdates.html │ │ ├── help-bailOnFail.html │ │ ├── help-bailOnPluginFail.html │ │ ├── help-debug.html │ │ ├── help-engineRetries.html │ │ ├── help-engineRetryInterval.html │ │ ├── help-engineaccount.html │ │ ├── help-enginepass.html │ │ ├── help-engineurl.html │ │ ├── help-engineuser.html │ │ ├── help-excludeFromBaseImage.html │ │ ├── help-forceAnalyze.html │ │ ├── help-name.html │ │ └── help-policyBundleId.html │ │ ├── AnchoreProjectAction │ │ ├── detailGraph.jelly │ │ ├── floatingBox.jelly │ │ └── jobMain.jelly │ │ └── Annotation │ │ └── config.jelly └── index.jelly └── webapp ├── css └── anchore.css ├── help ├── help-Annotations.html ├── help-OverrideAEAccount.html ├── help-OverrideAECredentials.html └── help-OverrideAEURL.html ├── images └── anchore.png └── js └── renderOutput.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bradleyjones 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "feat" 10 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | # build stuff 15 | target/ 16 | 17 | # IDE 18 | .idea 19 | *.iml 20 | 21 | # hpi:run data 22 | /work/ 23 | 24 | # OS 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2015-2016 Anchore, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: # Should be run in project root - results go in `target/` 3 | docker volume create --name maven-repo && \ 4 | docker run --rm -it \ 5 | -v maven-repo:/root/.m2 \ 6 | -v "${shell pwd}":/usr/src/mymaven \ 7 | -w /usr/src/mymaven \ 8 | maven:3.9.6-eclipse-temurin-17-focal \ 9 | mvn clean install 10 | 11 | 12 | .PHONY: run-jenkins 13 | run-jenkins: 14 | docker run -p 8080:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk17 15 | 16 | 17 | .PHONY: run-jenkins-oldest # The minimum version supported by the project 18 | run-jenkins-oldest: 19 | docker run -p 8080:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:2.426.3-lts-jdk11 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anchore Container Image Scanner Plugin 2 | 3 | > Older versions of this plugin may not be safe to use. Please review the following warnings before using an older version: 4 | >- [Password stored in plaintext](https://jenkins.io/security/advisory/2018-07-30/#SECURITY-1039) 5 | >- [Credentials stored in plaintext](https://jenkins.io/security/advisory/2019-11-21/#SECURITY-1539) 6 | 7 | > ANCHORE ENGINE USERS NOTICE 8 | >- As of 2023, [Anchore Engine](https://github.com/anchore/anchore-engine) is no longer maintained. There will be no future versions released. Users are advised to use Syft and Grype. 9 | >- Future versions of this Jenkins plugin _may_ break support for existing anchore-engine installations. No timeframe is scheduled. 10 | 11 | ## Description 12 | 13 | Anchore is a container inspection and analytics platform that enables operators to analyze, inspect, perform security scans, and evaluate custom policies against container images. The Anchore plugin can be used in a Pipeline job or added as a build step to a Freestyle job to automate the process of running an anchore analysis, evaluating custom anchore policies against images, and performing image anchore security scans.  14 | 15 | ## Anchore Jenkins Plugin 16 | 17 | Anchore has been designed to plug seamlessly into the CI/CD workflow. A developer commits code into the source code management system. This change triggers Jenkins to start a build which creates a container image. 18 | 19 | In the typical workflow this container image is then run through automated testing. If an image does not meet your organization’s requirements for security or compliance then it makes little sense to invest the time required to perform automated tests on the image, it would be better to “learn fast” by failing the build and returning the appropriate reports back to the developer to allow the issue to be addressed. 20 | 21 | 22 | ![](docs/images/workflow.png) 23 | 24 | ## Plugin Usage 25 | 26 | The plugin uses Anchore to scan a container image. It interacts with your Anchore deployment via its API. The usage model generally conforms to the following flow: 27 | 28 | 1. A Jenkins job will build a container image, and push the image to a registry that is pre-configured in your Anchore deployment (see pre-requisites below) 29 | 2. The anchore build step will interact with your Anchore deployment by 'adding' the image (which instructs your Anchore deployment to pull the image from the registry), and then performing a policy evaluation check on the image. The build step can optionally be configured to fail the build if the policy evaluation results in a 'STOP' action. 30 | 3. The plugin will store the resulting policy evaluation results with the job, for later inspection/review 31 | 32 | The plugin can be used in Freestyle and Pipeline jobs. 33 | 34 | ### Pre-Requisites 35 | 36 | Before getting started 37 | 38 | 1. Anchore must be installed within your environment, with its service API being accessible from all Jenkins workers. [Click here](https://docs.anchore.com/current/docs/deployment/) to get started with Anchore  39 | 2. A docker registry must exist and be configured within Anchore, as the plugin will be instructing your Anchore deployment to pull images from a registry. 40 | 3. All authentication credentials/Anchore API endpoint information must be available as input to the plugin at configuration time. 41 | 42 | Once the plugin is installed, configure the plugin to interact with 43 | Anchore in Jenkins global settings or at build time. 44 | 45 | ## Installation 46 | 47 | Anchore plugin is published in the Jenkins plugin registry. Follow [this guide](https://jenkins.io/doc/book/managing/plugins/#from-the-web-ui) and install *__Anchore Container Image Scanner Plugin__* 48 | 49 | ## Configuration 50 | 51 | Configuring the plugin in Jenkins global settings makes it available to any Jenkins build using the plugin. The plugin can also be configured at the build time which overrides the global settings if any. The build time overrides are applicable only to the specific build 52 | 53 | For global settings, go to the __Manage Jenkins > Configure System__ view and look for _Anchore Container Image Scanner_ section 54 | 55 | - Input _Anchore Enterprise URL_ to point to your Anchore installation 56 | >Note: Ensure that the /v2 route is included in the URL *or* /v1 if using a pre v4.9 version of Enteprise. 57 | - Input Anchore account username and password for _Anchore Enterprise Username_ and _Anchore Enterprise Password_ respectively 58 | - (Optional) If your Anchore deployment uses a user created certificate that is not signed by a standard certificate authority then select uncheck _Verify SSL_ 59 | - (Optional) For a verbose log of plugin execution check _Enable DEBUG logging_ 60 | 61 | ## Adding Anchore Scanning to Jenkins Build 62 | 63 | The Anchore plugin can be added a build step for a Freestyle or Pipeline 64 | build. Typically the flow is as follows. 65 | 66 | A Jenkins job will: 67 | 68 | 1. Build a container image 69 | 2. Push the image to a Docker Registry, typically a staging registry for QA 70 | 3. Use Anchore plugin in a Pipeline job or add Anchore Container Image Scanner build step to a Freestyle job to instruct your Anchore deployment to analyze the image 71 | 1. Anchore downloads (pulls) the image layers from the staging registry 72 | 2. Anchore performs analysis on the image 73 | 3. Anchore performs a policy evaluation on the image. 74 | 4. The Anchore plugin polls the Anchore API for a user defined period until the analysis and policy evaluation is complete 75 | 5. Based on user configuration, the Anchore plugin may fail the build in the case of a Policy violation or allow the built to continue with warnings. 76 | 77 | When run, the Anchore plugin will look for a file named _anchore\_images_ in the project workspace. This file should contain the name(s) of containers to be scanned and optionally include the Dockerfile.  78 | 79 | ### Pipeline Reference 80 | See [here](https://www.jenkins.io/doc/pipeline/steps/anchore-container-scanner/) for documentation on the plugin pipeline command. 81 | 82 | ### Pipeline Examples 83 | >Note: These examples use scripted pipeline snippets, but the specific commands can also be used in declarative pipeline scripts. 84 | > 85 | >These examples take a single, publicly-available image tag ('debian:latest'), put it into a file, and send that file to Anchore for analysis with various analysis options specified. 86 | 87 | This first example imagines a scenario where your team is just getting started with Anchore, and anticipates that the tool will find some issues that will need to be fixed. In order to not block the CI build until those issues have been fixed, the `bailOnFail` parameter has been temporarily set to `false`. 88 | ``` 89 | node { 90 | def imageLine = 'debian:latest' 91 | writeFile file: 'anchore_images', text: imageLine 92 | anchore name: 'anchore_images', engineCredentialsId: 'my_credentials_id', bailOnFail: false 93 | } 94 | ``` 95 | 96 | Once the issues identified by the scan have been fixed that parameter should be set (or removed so it can default) to `true`. This will help prevent new security issues from being introduced. 97 | ``` 98 | node { 99 | def imageLine = 'debian:latest' 100 | writeFile file: 'anchore_images', text: imageLine 101 | anchore name: 'anchore_images', engineCredentialsId: 'my_credentials_id' 102 | } 103 | ``` 104 | 105 | However, other teams within your organization are now starting to use anchore to analyze the images they are building. This has the potential to introduce many more images into the system, but you may want to be able to easily generate reports that only highlight the results of scans on your team's images. Annotations are a great way to do that! Using annotations, you can easily filter Anchore search results to only return data about the images you are concerned with. 106 | 107 | To update your pipeline to include annotations on images your team is building, do the following. 108 | ``` 109 | node { 110 | def imageLine = 'debian:latest' 111 | writeFile file: 'anchore_images', text: imageLine 112 | anchore name: 'anchore_images', engineCredentialsId: 'my_credentials_id', annotations: [[key: 'image_owner', value: 'my_team']] 113 | } 114 | ``` 115 | 116 | As your organization's DevOps maturity progresses, you may find that you not only want to report on image scan data from different teams differently, you may also want to customize those scans to more closely control how the images you are developing are being scanned. 117 | 118 | Information about how to customize policy bundles is available [here](https://docs.anchore.com/current/docs/overview/concepts/policy/bundles). To apply a specific policy bundle to your image scans, not the unique UUID of your policy and update your pipeline script as shown below. 119 | ``` 120 | node { 121 | def imageLine = 'debian:latest' 122 | writeFile file: 'anchore_images', text: imageLine 123 | anchore 'anchore_images', engineCredentialsId: 'my_credentials_id', annotations: [[key: 'my_key', value: 'my_value']], policyBundleId: 'myUUID' 124 | } 125 | ``` 126 | 127 | ### Freestyle  128 | 129 | In the example below an _Execute Shell_ build step is used to build and push a container image to a local registry. 130 | ``` 131 | TAG=$(date "+%H%M%S%d%m%Y") 132 | IMAGENAME=build.example.com/myapp 133 | docker build -t $IMAGENAME:$TAG . 134 | docker push $IMAGENAME:$TAG 135 | ``` 136 | 137 | We will add a single line to create the _anchore\_images_ file that is read by the Anchore plugin 138 | 139 | >Note: Multiple lines can be added if the build produces more than a single container image. 140 | 141 | ``` 142 | TAG=$(date "+%H%M%S%d%m%Y") 143 | IMAGENAME=build.example.com/myapp 144 | docker build -t $IMAGENAME:$TAG . 145 | docker push $IMAGENAME:$TAG 146 | 147 | # Line added to create anchore_images file 148 | echo "$IMAGENAME:$TAG ${WORKSPACE}/Dockerfile " > anchore_images 149 | ``` 150 | 151 | After the image has been built and pushed to the staging registry the Anchore plugin should be invoked.  152 | 153 | Dropdown _Add build step_ and select the _Anchore Container Image Scanner_ 154 | 155 | ![](docs/images/freestyle_build_step.png) 156 | 157 | A new build step labeled **Anchore Build Options** will appear in your job 158 | 159 | ![](docs/images/build_options.png) 160 | 161 | The plugin creates an Anchore Report directory that includes a JSON file with the results of the policy evaluation. 162 | 163 | The status of the scan status is indicated in the build summary (GO = Pass, STOP = Fail, WARN=Warning) 164 | 165 | ![](docs/images/build_summary.png) 166 | 167 | Clicking on the Anchore Report link will render the vulnerabilities and policy evaluation in the Jenkins web UI 168 | 169 | ![](docs/images/build_report.png) 170 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Local development testing 2 | ## Deploying a test build 3 | 4 | A build can be executed using the Makefile command `make build` 5 | 6 | It is important you bump the version in `pom.xml` prior to starting a build. 7 | 8 | The build will run some tests, linting and if all compliant will produce a `.hpi` file in `anchore-container-scanner-plugin/target/anchore-container-scanner.hpi` 9 | 10 | You can upload this .hpi file into Jenkins using the `Deploy Plugin` section in `Dashboard > Manage Jenkins > Plugins > Advanced settings`. 11 | 12 | > ** You *must* restart Jenkins for the new plugin to take effect, every time any plugin is changed** 13 | 14 | You should check that the Anchore Container Scanner plugin settings pane appears in the `Manage Jenkins > System` page. 15 | Sometimes the plugin install borks and you need to uninstall it within Jenkins (restart Jenkins), rebuild it (`make build`) and re-install it (restart Jenkins). 16 | 17 | ## Configuring the plugin within Jenkins 18 | 19 | Run Anchore Enterprise locally so it is available on your machines localhost. 20 | 21 | Within Jenkins go to `Manage Jenkins > System` and find the Anchore settings section. 22 | 23 | For `Anchore Enterprise URL` enter `http://host.docker.internal:8228/v2` 24 | 25 | Tick `Enable DEBUG logging` for testing as it helps troubleshooting. 26 | 27 | 28 | ## Setting up a test job 29 | 30 | Each job can have its own Enterprise override configuration. This can be useful if A/B testing APIs or other systems within Enterprise. 31 | 32 | From the Jenkins Dashboard click `New Item`, enter a name, then select `Freestyle project`. 33 | 34 | The way the Anchore plugin works, is by reading a line of image tags from a file, and sending those to Enterprise for analysis. By default this file is called `anchore_images`. 35 | 36 | This is usually populated by a real test workload, but we can mock it by adding a `Build step` of type `Execute Shell`. Then enter something like the following: 37 | 38 | ``` 39 | echo 'alpine:latest' > anchore_images 40 | echo 'node:5.5-slim' >> anchore_images 41 | ``` 42 | 43 | > These images must be pullable by Enterprise, so if they're private ensure you have registry credentials within Enterprise. Registry credentials within Enterprise will also help bypass the Docker pull rate limit. 44 | 45 | Then add a `Build Step` of `Anchore Container Image Scanner`, here you can override any of the default settings, but usually the default config will be enough. 46 | 47 | Save the new `Freestyle Job` and it will appear in the Dashboard. From here you can execute it by clicking `Build Now` 48 | 49 | Anchore will gate the Job based on if the images passed the specified/default policy. Once a build is complete a new tab called `Anchore Report` will appear in the left hand menu. 50 | -------------------------------------------------------------------------------- /docs/images/build_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/anchore-container-scanner-plugin/cd6db70db199395f9eb391a5f8613b8f5785eaab/docs/images/build_options.png -------------------------------------------------------------------------------- /docs/images/build_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/anchore-container-scanner-plugin/cd6db70db199395f9eb391a5f8613b8f5785eaab/docs/images/build_report.png -------------------------------------------------------------------------------- /docs/images/build_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/anchore-container-scanner-plugin/cd6db70db199395f9eb391a5f8613b8f5785eaab/docs/images/build_summary.png -------------------------------------------------------------------------------- /docs/images/freestyle_build_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/anchore-container-scanner-plugin/cd6db70db199395f9eb391a5f8613b8f5785eaab/docs/images/freestyle_build_step.png -------------------------------------------------------------------------------- /docs/images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/anchore-container-scanner-plugin/cd6db70db199395f9eb391a5f8613b8f5785eaab/docs/images/workflow.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.jenkins-ci.plugins 7 | plugin 8 | 4.88 9 | 10 | 11 | 12 | anchore-container-scanner 13 | 3.3.1-SNAPSHOT 14 | hpi 15 | Anchore Container Image Scanner Plugin 16 | Integrates Jenkins with the Anchore Image Scanner 17 | 18 | https://github.com/jenkinsci/anchore-container-scanner-plugin 19 | 20 | 21 | 22 | Apache 2 License 23 | http://opensource.org/licenses/Apache-2.0 24 | 25 | 26 | 27 | 28 | 29 | 2.462.3 30 | 31 | 8 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | nurmi 43 | Daniel Nurmi 44 | nurmi@anchore.com 45 | 46 | 47 | swathigangisetty 48 | Swathi Gangisetty 49 | swathi@anchore.com 50 | 51 | 52 | 53 | 54 | scm:git:git://github.com/jenkinsci/${project.artifactId}-plugin.git 55 | scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git 56 | http://github.com/jenkinsci/${project.artifactId}-plugin 57 | anchore-container-scanner-3.1.1 58 | 59 | 67 | 68 | 69 | repo.jenkins-ci.org 70 | https://repo.jenkins-ci.org/public/ 71 | 72 | 73 | 74 | 75 | repo.jenkins-ci.org 76 | https://repo.jenkins-ci.org/public/ 77 | 78 | 79 | 80 | 81 | 88 | 89 | org.jenkins-ci.plugins 90 | structs 91 | 338.v848422169819 92 | 93 | 94 | org.apache.httpcomponents 95 | httpclient 96 | 4.5.14 97 | 98 | 99 | org.jenkins-ci.plugins 100 | credentials 101 | 1381.v2c3a_12074da_b_ 102 | 103 | 104 | 105 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-dependency-plugin 125 | 126 | 127 | download-webresources 128 | generate-resources 129 | 130 | unpack 131 | 132 | 133 | 134 | 135 | org.webjars 136 | jquery 137 | 1.12.4 138 | **/jquery*.min.js 139 | 140 | 141 | org.webjars 142 | datatables 143 | 1.10.25 144 | 145 | **/jquery.dataTables.min.js,**/dataTables.bootstrap.min.js,**/dataTables.bootstrap.min.css 146 | 147 | 148 | 149 | org.webjars 150 | bootstrap 151 | 3.4.1 152 | **/bootstrap.min.css,**/bootstrap.min.js,**/fonts/* 153 | 154 | 155 | ${project.build.directory}/webjars 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | org.apache.maven.plugins 164 | maven-resources-plugin 165 | 166 | 167 | copy-downloaded-resources 168 | generate-resources 169 | 170 | copy-resources 171 | 172 | 173 | ${project.build.directory}/external-resources 174 | 175 | 176 | js 177 | ${project.build.directory}/webjars/META-INF/resources/webjars/jquery/1.12.4 178 | 179 | 180 | ${project.build.directory}/webjars/META-INF/resources/webjars/datatables/1.10.25 181 | 182 | 183 | ${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/3.4.1 184 | 185 | 186 | 187 | src/main/webapp 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | org.jenkins-ci.tools 198 | maven-hpi-plugin 199 | 200 | 8 201 | 202 | 203 | ${project.build.directory}/external-resources 204 | false 205 | 206 | 207 | 208 | ${project.build.directory}/external-resources 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/AnchoreAction.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import hudson.model.Action; 4 | import hudson.model.Job; 5 | import hudson.model.Run; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Set; 11 | import jenkins.model.Jenkins; 12 | import jenkins.model.lazy.LazyBuildMixIn; 13 | import jenkins.tasks.SimpleBuildStep; 14 | import net.sf.json.JSONObject; 15 | 16 | /** 17 | * Anchore plugin results for a given build are stored and subsequently retrieved from an instance of this class. Rendering/display of 18 | * the results is defined in the appropriate index and summary jelly files. This Jenkins Action is associated with a build (and not the 19 | * project which is one level up) 20 | */ 21 | public class AnchoreAction implements SimpleBuildStep.LastBuildAction { 22 | 23 | private Run build; 24 | private String gateStatus; 25 | private String gateOutputUrl; 26 | private Map queryOutputUrls; 27 | private String gateSummary; 28 | private String cveListingUrl; 29 | private int stopActionCount; 30 | private int warnActionCount; 31 | private int goActionCount; 32 | 33 | // For backwards compatibility 34 | @Deprecated 35 | private String gateReportUrl; 36 | @Deprecated 37 | private Map queries; 38 | 39 | 40 | public AnchoreAction(Run build, String gateStatus, final String jenkinsOutputDirName, String gateReport, 41 | Map queryReports, String gateSummary, String cveListingFileName, 42 | int stopActionCount, int warnActionCount, int goActionCount) { 43 | this.build = build; 44 | this.gateStatus = gateStatus; 45 | this.stopActionCount = stopActionCount; 46 | this.warnActionCount = warnActionCount; 47 | this.goActionCount = goActionCount; 48 | this.gateOutputUrl = "../artifact/" + jenkinsOutputDirName + "/" + gateReport; 49 | 50 | this.queryOutputUrls = new HashMap(); 51 | for (Map.Entry entry : queryReports.entrySet()) { 52 | String k = entry.getKey(); 53 | String v = entry.getValue(); 54 | String newv = "../artifact/" + jenkinsOutputDirName + "/" + v; 55 | this.queryOutputUrls.put(k, newv); 56 | } 57 | 58 | // original maps conversion method 59 | /* 60 | this.queryOutputUrls = Maps.transformValues(queryReports, new Function() { 61 | 62 | @Override 63 | public String apply(@Nullable String queryOutput) { 64 | return "../artifact/" + jenkinsOutputDirName + "/" + queryOutput; 65 | } 66 | }); 67 | */ 68 | this.gateSummary = gateSummary; 69 | if (null != cveListingFileName && cveListingFileName.trim().length() > 0) { 70 | this.cveListingUrl = "../artifact/" + jenkinsOutputDirName + "/" + cveListingFileName; 71 | } 72 | } 73 | 74 | @Override 75 | public String getIconFileName() { 76 | return Jenkins.RESOURCE_PATH + "/plugin/anchore-container-scanner/images/anchore.png"; 77 | } 78 | 79 | @Override 80 | public String getDisplayName() { 81 | return "Anchore Report (" + gateStatus + ")"; 82 | } 83 | 84 | @Override 85 | public String getUrlName() { 86 | return "anchore-results"; 87 | } 88 | 89 | public Run getBuild() { 90 | return this.build; 91 | } 92 | 93 | public String getGateStatus() { 94 | return gateStatus; 95 | } 96 | 97 | public String getGateOutputUrl() { 98 | return encodeURL(this.gateOutputUrl); 99 | } 100 | 101 | public Map getQueryOutputUrls() { 102 | // queryOutputUrls was a guava TransformedEntriesMap object in plugin version < 1.0.13 and is loaded as such. Plugin versions >= 103 | // 1.0.13 changed the type definition and lose the transformer function required for reading the map contents. This results in 104 | // a failure to load the member. Transfer the contents from the underlying guava map to a native java map using the keys and 105 | // some hacky guess work 106 | 107 | /* Find bugs does not like the instanceof check, falling back to try-catch approach 108 | if (!(this.queryOutputUrls instanceof HashMap)) { 109 | String base_path = this.gateOutputUrl.substring(0, this.gateOutputUrl.lastIndexOf('/')); 110 | int query_num = 0; 111 | Map fixedQueryOutputUrls = new HashMap<>(); 112 | for (String key : this.queryOutputUrls.keySet()) { 113 | fixedQueryOutputUrls.put(key, base_path + "/anchore_query_" + String.valueOf(++query_num) + ".json"); 114 | } 115 | return fixedQueryOutputUrls; 116 | }*/ 117 | 118 | Map fixedQueryOutputUrls = new HashMap<>(); 119 | try { 120 | // Fetch values in the map to verify the underlying map is functional 121 | if (null != this.queryOutputUrls) { 122 | for (Map.Entry entry : this.queryOutputUrls.entrySet()) { 123 | fixedQueryOutputUrls.put(entry.getKey(), encodeURL(entry.getValue())); 124 | } 125 | } 126 | } catch (Exception e) { 127 | String base_path = encodeURL(this.gateOutputUrl.substring(0, this.gateOutputUrl.lastIndexOf('/'))); 128 | int query_num = 0; 129 | for (String key : this.queryOutputUrls.keySet()) { 130 | fixedQueryOutputUrls.put(key, base_path + "/anchore_query_" + String.valueOf(++query_num) + ".json"); 131 | } 132 | } 133 | return fixedQueryOutputUrls; 134 | } 135 | 136 | public JSONObject getGateSummary() { 137 | // gateSummary was a JSON object in plugin version <= 1.0.12. Jenkins does not handle this type change correctly post upgrade. 138 | // Summary data from the previous versions is lost during deserialization due to the type change and plugin versions > 1.0.12 139 | // won't be able to render the summary table only for builds that were executed using older versions of the plugin. This check 140 | // is necessary to ensure plugin doesn't exception out in the process 141 | if (null != this.gateSummary && this.gateSummary.trim().length() > 0) { 142 | return JSONObject.fromObject(this.gateSummary); 143 | } else { 144 | return null; 145 | } 146 | } 147 | 148 | public String getCveListingUrl() { 149 | return encodeURL(cveListingUrl); 150 | } 151 | 152 | public String getGateReportUrl() { 153 | return this.gateReportUrl; 154 | } 155 | 156 | public Map getQueries() { 157 | return this.queries; 158 | } 159 | 160 | public int getGoActionCount(){ 161 | return this.goActionCount; 162 | } 163 | 164 | public int getStopActionCount(){ 165 | return this.stopActionCount; 166 | } 167 | 168 | public int getWarnActionCount(){ 169 | return this.warnActionCount; 170 | } 171 | 172 | @Override 173 | public Collection getProjectActions() { 174 | Job job = this.build.getParent(); 175 | return Collections.singleton(new AnchoreProjectAction(job)); 176 | } 177 | 178 | /** 179 | * Gets the Anchore result of the previous build, if it's recorded, or null. 180 | * @return the previous AnchoreAction 181 | */ 182 | public AnchoreAction getPreviousResult() { 183 | Run b = this.build; 184 | while(true) { 185 | b = b.getPreviousBuild(); 186 | if (b == null) { 187 | return null; 188 | } 189 | AnchoreAction r = b.getAction(AnchoreAction.class); 190 | if (r != null) { 191 | if (r == this) { 192 | throw new IllegalStateException(this + " was attached to both " + b + " and " + this.build); 193 | } 194 | if (r.build.number != b.number) { 195 | throw new IllegalStateException(r + " was attached to both " + b + " and " + r.build); 196 | } 197 | return r; 198 | } 199 | } 200 | } 201 | 202 | private static String encodeURL(String s) { 203 | if (s == null) { 204 | return s; 205 | } 206 | return s.replaceAll("\"", "%22") 207 | .replaceAll("\n", "%0A") 208 | .replaceAll("\r", ""); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/AnchoreBuilder.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | 4 | import com.anchore.jenkins.plugins.anchore.Util.GATE_ACTION; 5 | import com.cloudbees.plugins.credentials.CredentialsMatchers; 6 | import com.cloudbees.plugins.credentials.CredentialsProvider; 7 | import com.cloudbees.plugins.credentials.common.StandardListBoxModel; 8 | import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; 9 | import com.cloudbees.plugins.credentials.domains.DomainRequirement; 10 | import com.google.common.base.Strings; 11 | import hudson.AbortException; 12 | import hudson.Extension; 13 | import hudson.FilePath; 14 | import hudson.Launcher; 15 | import hudson.model.AbstractProject; 16 | import hudson.model.Run; 17 | import hudson.model.TaskListener; 18 | import hudson.security.ACL; 19 | import hudson.tasks.BuildStepDescriptor; 20 | import hudson.tasks.Builder; 21 | import hudson.util.FormValidation; 22 | import hudson.util.ListBoxModel; 23 | import hudson.util.Secret; 24 | import java.io.IOException; 25 | import java.util.Collections; 26 | import java.util.List; 27 | import java.util.logging.Logger; 28 | import javax.annotation.Nonnull; 29 | import jenkins.model.Jenkins; 30 | import jenkins.tasks.SimpleBuildStep; 31 | import net.sf.json.JSONObject; 32 | import org.jenkinsci.Symbol; 33 | import org.kohsuke.stapler.DataBoundConstructor; 34 | import org.kohsuke.stapler.DataBoundSetter; 35 | import org.kohsuke.stapler.QueryParameter; 36 | import org.kohsuke.stapler.StaplerRequest; 37 | 38 | /** 39 | *

Anchore Plugin enables Jenkins users to scan container images, generate analysis, evaluate gate policy, and execute customizable 40 | * queries. The plugin can be used in a freestyle project as a step or invoked from a pipeline script

41 | * 42 | *

Requirements:

43 | * 44 | *
  1. Jenkins installed and configured either as a single system, or with multiple configured jenkins worker nodes
  2. 45 | * 46 | *
  3. Each host on which jenkins jobs will run must have docker installed and the jenkins user (or whichever user you have configured 47 | * jenkins to run jobs as) must be allowed to interact with docker (either directly or via sudo)
  4. 48 | * 49 | *
  5. Each host on which jenkins jobs will run must have the latest anchore container image installed in the local docker host. To 50 | * install, run 'docker pull anchore/jenkins:latest' on each jenkins host to make the image available to the plugin. The plugin will 51 | * start an instance of the anchore/jenkins:latest docker container named 'jenkins_anchore' by default, on each host that runs a 52 | * jenkins job that includes an Anchore Container Image Scanner step.
53 | */ 54 | public class AnchoreBuilder extends Builder implements SimpleBuildStep { 55 | 56 | // Log handler for logging above INFO level events to jenkins log 57 | private static final Logger LOG = Logger.getLogger(AnchoreBuilder.class.getName()); 58 | 59 | // Assigning the defaults here for pipeline builds 60 | private String name; 61 | private String engineRetries = DescriptorImpl.DEFAULT_ENGINE_RETRIES; 62 | private String engineRetryInterval = DescriptorImpl.DEFAULT_ENGINE_RETRY_INTERVAL; 63 | private boolean bailOnFail = DescriptorImpl.DEFAULT_BAIL_ON_FAIL; 64 | private boolean bailOnPluginFail = DescriptorImpl.DEFAULT_BAIL_ON_PLUGIN_FAIL; 65 | private String policyBundleId = DescriptorImpl.DEFAULT_POLICY_BUNDLE_ID; 66 | private List annotations; 67 | private boolean autoSubscribeTagUpdates = DescriptorImpl.DEFAULT_AUTOSUBSCRIBE_TAG_UPDATES; 68 | private boolean forceAnalyze = DescriptorImpl.DEFAULT_FORCE_ANALYZE; 69 | private boolean excludeFromBaseImage = DescriptorImpl.DEFAULT_EXCLUDE_FROM_BASE_IMAGE; 70 | 71 | // Override global config. Supported for anchore-enterprise mode config only 72 | private String anchoreui = DescriptorImpl.EMPTY_STRING; 73 | private String engineurl = DescriptorImpl.EMPTY_STRING; 74 | private String engineCredentialsId = DescriptorImpl.EMPTY_STRING; 75 | private String engineaccount = DescriptorImpl.EMPTY_STRING; 76 | private boolean engineverify = false; 77 | // More flags to indicate boolean override, ugh! 78 | private boolean isEngineverifyOverrride = false; 79 | 80 | // Getters are used by config.jelly 81 | public String getName() { 82 | return name; 83 | } 84 | 85 | public String getEngineRetries() { 86 | return engineRetries; 87 | } 88 | 89 | public String getEngineRetryInterval() { 90 | return engineRetryInterval; 91 | } 92 | 93 | public boolean getBailOnFail() { 94 | return bailOnFail; 95 | } 96 | 97 | public boolean getBailOnPluginFail() { 98 | return bailOnPluginFail; 99 | } 100 | 101 | public String getPolicyBundleId() { 102 | return policyBundleId; 103 | } 104 | 105 | public List getAnnotations() { 106 | return annotations; 107 | } 108 | 109 | public boolean getAutoSubscribeTagUpdates() { 110 | return autoSubscribeTagUpdates; 111 | } 112 | 113 | public boolean getForceAnalyze() { 114 | return forceAnalyze; 115 | } 116 | 117 | public boolean getExcludeFromBaseImage() { 118 | return excludeFromBaseImage; 119 | } 120 | 121 | public String getAnchoreui() { 122 | return anchoreui; 123 | } 124 | 125 | public String getEngineurl() { 126 | return engineurl; 127 | } 128 | 129 | public String getEngineCredentialsId() { 130 | return engineCredentialsId; 131 | } 132 | 133 | public String getEngineaccount() { 134 | return engineaccount; 135 | } 136 | 137 | public boolean getEngineverify() { 138 | return engineverify; 139 | } 140 | 141 | 142 | @DataBoundSetter 143 | public void setEngineRetries(String engineRetries) { 144 | this.engineRetries = engineRetries; 145 | } 146 | 147 | @DataBoundSetter 148 | public void setEngineRetryInterval(String engineRetryInterval) { 149 | this.engineRetryInterval = engineRetryInterval; 150 | } 151 | 152 | @DataBoundSetter 153 | public void setBailOnFail(boolean bailOnFail) { 154 | this.bailOnFail = bailOnFail; 155 | } 156 | 157 | @DataBoundSetter 158 | public void setBailOnPluginFail(boolean bailOnPluginFail) { 159 | this.bailOnPluginFail = bailOnPluginFail; 160 | } 161 | 162 | @DataBoundSetter 163 | public void setPolicyBundleId(String policyBundleId) { 164 | this.policyBundleId = policyBundleId; 165 | } 166 | 167 | @DataBoundSetter 168 | public void setAnnotations(List annotations) { 169 | this.annotations = annotations; 170 | } 171 | 172 | @DataBoundSetter 173 | public void setAutoSubscribeTagUpdates(boolean autoSubscribeTagUpdates) { 174 | this.autoSubscribeTagUpdates = autoSubscribeTagUpdates; 175 | } 176 | 177 | @DataBoundSetter 178 | public void setForceAnalyze(boolean forceAnalyze) { 179 | this.forceAnalyze = forceAnalyze; 180 | } 181 | 182 | @DataBoundSetter 183 | public void setExcludeFromBaseImage(boolean excludeFromBaseImage) { 184 | this.excludeFromBaseImage = excludeFromBaseImage; 185 | } 186 | 187 | @DataBoundSetter 188 | public void setAnchoreui(String anchoreui) { 189 | this.anchoreui = anchoreui; 190 | } 191 | 192 | @DataBoundSetter 193 | public void setEngineurl(String engineurl) { 194 | this.engineurl = engineurl; 195 | } 196 | 197 | @DataBoundSetter 198 | public void setEngineCredentialsId(String engineCredentialsId) { 199 | this.engineCredentialsId = engineCredentialsId; 200 | } 201 | 202 | @DataBoundSetter 203 | public void setEngineaccount(String engineaccount) { 204 | this.engineaccount = engineaccount; 205 | } 206 | 207 | @DataBoundSetter 208 | public void setEngineverify(boolean engineverify) { 209 | this.engineverify = engineverify; 210 | this.isEngineverifyOverrride = true; 211 | } 212 | 213 | // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" or "DataBoundSetter" 214 | @DataBoundConstructor 215 | public AnchoreBuilder(String name) { 216 | this.name = name; 217 | } 218 | 219 | @Override 220 | public void perform(@Nonnull Run run, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) 221 | throws InterruptedException, IOException { 222 | 223 | LOG.warning( 224 | "Starting Anchore Container Image Scanner step, project: " + run.getParent().getDisplayName() + ", job: " + run.getNumber()); 225 | 226 | boolean failedByGate = false; 227 | BuildConfig config = null; 228 | BuildWorker worker = null; 229 | DescriptorImpl globalConfig = getDescriptor(); 230 | ConsoleLog console = new ConsoleLog("AnchorePlugin", listener.getLogger(), globalConfig.getDebug()); 231 | 232 | GATE_ACTION finalAction; 233 | 234 | try { 235 | 236 | /* Fetch Jenkins creds first, can't push this lower down the chain since it requires Jenkins instance object */ 237 | String engineuser = null; 238 | String enginepass = null; 239 | if (!Strings.isNullOrEmpty(engineCredentialsId)) { 240 | console.logDebug("Found build override for anchore-enterprise credentials. Processing Jenkins credential ID "); 241 | try { 242 | StandardUsernamePasswordCredentials creds = CredentialsProvider 243 | .findCredentialById(engineCredentialsId, StandardUsernamePasswordCredentials.class, run, 244 | Collections.emptyList()); 245 | if (null != creds) { 246 | engineuser = creds.getUsername(); 247 | enginepass = creds.getPassword().getPlainText(); 248 | } else { 249 | throw new AbortException("Cannot find Jenkins credentials by ID: \'" + engineCredentialsId 250 | + "\'. Ensure credentials are defined in Jenkins before using them"); 251 | } 252 | } catch (AbortException e) { 253 | throw e; 254 | } catch (Exception e) { 255 | console.logError("Error looking up Jenkins credentials by ID: \'" + engineCredentialsId + "\'", e); 256 | throw new AbortException("Error looking up Jenkins credentials by ID: \'" + engineCredentialsId); 257 | } 258 | } 259 | 260 | String anchoreui = globalConfig.getAnchoreui(); 261 | 262 | /* Instantiate config and a new build worker */ 263 | config = new BuildConfig(name, engineRetries, engineRetryInterval, bailOnFail, 264 | bailOnPluginFail, policyBundleId, annotations, autoSubscribeTagUpdates, forceAnalyze, excludeFromBaseImage, globalConfig.getDebug(), anchoreui, 265 | // messy build time overrides, ugh! 266 | !Strings.isNullOrEmpty(engineurl) ? engineurl : globalConfig.getEngineurl(), 267 | !Strings.isNullOrEmpty(engineuser) ? engineuser : globalConfig.getEngineuser(), 268 | !Strings.isNullOrEmpty(enginepass) ? enginepass : globalConfig.getEnginepass().getPlainText(), 269 | !Strings.isNullOrEmpty(engineaccount) ? engineaccount : globalConfig.getEngineaccount(), 270 | isEngineverifyOverrride ? engineverify : globalConfig.getEngineverify()); 271 | worker = new BuildWorker(run, workspace, launcher, listener, config); 272 | 273 | if (Strings.isNullOrEmpty(anchoreui)) { 274 | console.logInfo("Anchore UI URL is not set. Links to Anchore UI will not be available"); 275 | } 276 | 277 | /* Log any build time overrides are at play */ 278 | if (!Strings.isNullOrEmpty(engineurl)) { 279 | console.logInfo("Build override set for Anchore Engine URL"); 280 | } 281 | if (!Strings.isNullOrEmpty(engineuser) && !Strings.isNullOrEmpty(enginepass)) { 282 | console.logInfo("Build override set for Anchore Engine credentials"); 283 | } 284 | if (!Strings.isNullOrEmpty(engineaccount)) { 285 | console.logInfo("Build override set for Anchore Engine account"); 286 | } 287 | if (isEngineverifyOverrride) { 288 | console.logInfo("Build override set for Anchore Engine verify SSL"); 289 | } 290 | 291 | /* Run analysis */ 292 | worker.runAnalyzer(); 293 | 294 | /* Run gates */ 295 | finalAction = worker.runGates(); 296 | 297 | /* Run queries and continue even if it fails */ 298 | try { 299 | worker.runQueries(); 300 | } catch (Exception e) { 301 | console.logWarn("Recording failure to execute Anchore queries and moving on with plugin operation", e); 302 | } 303 | 304 | /* Setup reports */ 305 | worker.setupBuildReports(); 306 | 307 | /* Evaluate result of step based on gate action */ 308 | if (null != finalAction) { 309 | if (config.getBailOnFail() && (GATE_ACTION.STOP.equals(finalAction) || GATE_ACTION.FAIL.equals(finalAction))) { 310 | console.logWarn("Failing Anchore Container Image Scanner Plugin step due to final result " + finalAction); 311 | failedByGate = true; 312 | throw new AbortException("Failing Anchore Container Image Scanner Plugin step due to final result " + finalAction); 313 | } else { 314 | console.logInfo("Marking Anchore Container Image Scanner step as successful, final result " + finalAction); 315 | } 316 | } else { 317 | console.logInfo("Marking Anchore Container Image Scanner step as successful, no final result"); 318 | } 319 | 320 | } catch (Exception e) { 321 | if (failedByGate) { 322 | throw e; 323 | } else if ((null != config && config.getBailOnPluginFail()) || bailOnPluginFail) { 324 | console.logError("Failing Anchore Container Image Scanner Plugin step due to errors in plugin execution", e); 325 | if (e instanceof AbortException) { 326 | throw e; 327 | } else { 328 | throw new AbortException("Failing Anchore Container Image Scanner Plugin step due to errors in plugin execution"); 329 | } 330 | } else { 331 | console.logWarn("Marking Anchore Container Image Scanner step as successful despite errors in plugin execution"); 332 | } 333 | } finally { 334 | // Wrap cleanup in try catch block to ensure this finally block does not throw an exception 335 | if (null != worker) { 336 | try { 337 | worker.cleanup(); 338 | } catch (Exception e) { 339 | console.logDebug("Failed to cleanup after the plugin, ignoring the errors", e); 340 | } 341 | } 342 | console.logInfo("Completed Anchore Container Image Scanner step"); 343 | LOG.warning("Completed Anchore Container Image Scanner step, project: " + run.getParent().getDisplayName() + ", job: " + run 344 | .getNumber()); 345 | } 346 | } 347 | 348 | @Override 349 | public DescriptorImpl getDescriptor() { 350 | return (DescriptorImpl) super.getDescriptor(); 351 | } 352 | 353 | @Symbol("anchore") // For Jenkins pipeline workflow. This lets pipeline refer to step using the defined identifier 354 | @Extension // This indicates to Jenkins that this is an implementation of an extension point. 355 | public static final class DescriptorImpl extends BuildStepDescriptor { 356 | 357 | // Default job level config that may be used both by config.jelly and an instance of AnchoreBuilder 358 | public static final String DEFAULT_NAME = "anchore_images"; 359 | public static final String DEFAULT_ENGINE_RETRIES = "300"; 360 | public static final String DEFAULT_ENGINE_RETRY_INTERVAL = "5"; 361 | public static final boolean DEFAULT_BAIL_ON_FAIL = true; 362 | public static final boolean DEFAULT_BAIL_ON_PLUGIN_FAIL = true; 363 | public static final String DEFAULT_PLUGIN_MODE = "anchoreengine"; 364 | public static final String DEFAULT_POLICY_BUNDLE_ID = ""; 365 | public static final String EMPTY_STRING = ""; 366 | public static final boolean DEFAULT_AUTOSUBSCRIBE_TAG_UPDATES = true; 367 | public static final boolean DEFAULT_FORCE_ANALYZE = false; 368 | public static final boolean DEFAULT_EXCLUDE_FROM_BASE_IMAGE = false; 369 | 370 | // Global configuration 371 | private boolean debug; 372 | private String anchoreui; 373 | private String engineurl; 374 | private String engineuser; 375 | private Secret enginepass; 376 | private String engineaccount; 377 | private boolean engineverify; 378 | 379 | // Upgrade case, you can never really remove these variables once they are introduced 380 | @Deprecated 381 | private boolean enabled; 382 | 383 | public void setDebug(boolean debug) { 384 | this.debug = debug; 385 | } 386 | 387 | @Deprecated 388 | public void setEnabled(boolean enabled) { 389 | this.enabled = enabled; 390 | } 391 | 392 | public void setAnchoreui(String anchoreui) { 393 | this.anchoreui = anchoreui; 394 | } 395 | 396 | public void setEngineurl(String engineurl) { 397 | this.engineurl = engineurl; 398 | } 399 | 400 | public void setEngineuser(String engineuser) { 401 | this.engineuser = engineuser; 402 | } 403 | 404 | public void setEnginepass(Secret enginepass) { 405 | this.enginepass = enginepass; 406 | } 407 | 408 | public void setEngineaccount(String engineaccount) { 409 | this.engineaccount = engineaccount; 410 | } 411 | 412 | public void setEngineverify(boolean engineverify) { 413 | this.engineverify = engineverify; 414 | } 415 | 416 | public boolean getDebug() { 417 | return debug; 418 | } 419 | 420 | @Deprecated 421 | public boolean getEnabled() { 422 | return enabled; 423 | } 424 | 425 | public String getAnchoreui() { 426 | return anchoreui; 427 | } 428 | 429 | public String getEngineurl() { 430 | return engineurl; 431 | } 432 | 433 | public String getEngineuser() { 434 | return engineuser; 435 | } 436 | 437 | public Secret getEnginepass() { 438 | return enginepass; 439 | } 440 | 441 | public String getEngineaccount() { 442 | return engineaccount; 443 | } 444 | 445 | public boolean getEngineverify() { 446 | return engineverify; 447 | } 448 | 449 | public DescriptorImpl() { 450 | load(); 451 | } 452 | 453 | @Override 454 | public boolean isApplicable(Class aClass) { 455 | return true; 456 | } 457 | 458 | @Override 459 | public String getDisplayName() { 460 | return "Anchore Container Image Scanner"; 461 | } 462 | 463 | @Override 464 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 465 | req.bindJSON(this, formData); // Use stapler request to bind 466 | save(); 467 | return true; 468 | } 469 | 470 | /** 471 | * Performs on-the-fly validation of the form field 'name' (Image list file) 472 | * 473 | * @param value This parameter receives the value that the user has typed in the 'Image list file' box 474 | * @return Indicates the outcome of the validation. This is sent to the browser.

Note that returning {@link 475 | * FormValidation#error(String)} does not prevent the form from being saved. It just means that a message will be displayed to the 476 | * user 477 | */ 478 | @SuppressWarnings("unused") 479 | public FormValidation doCheckName(@QueryParameter String value) { 480 | if (!Strings.isNullOrEmpty(value)) { 481 | return FormValidation.ok(); 482 | } else { 483 | return FormValidation.error("Please enter a valid file name"); 484 | } 485 | } 486 | 487 | @SuppressWarnings("unused") 488 | public ListBoxModel doFillEngineCredentialsIdItems(@QueryParameter String credentialsId) { 489 | StandardListBoxModel result = new StandardListBoxModel(); 490 | 491 | if (!Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) { 492 | return result.includeCurrentValue(credentialsId); 493 | } 494 | 495 | return result.includeEmptyValue() 496 | .includeMatchingAs(ACL.SYSTEM, Jenkins.getActiveInstance(), StandardUsernamePasswordCredentials.class, 497 | Collections.emptyList(), CredentialsMatchers.always()); 498 | } 499 | } 500 | } 501 | 502 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/AnchoreProjectAction.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import hudson.Functions; 4 | import hudson.model.Action; 5 | import hudson.model.Job; 6 | import hudson.model.Run; 7 | import hudson.util.Area; 8 | import hudson.util.ColorPalette; 9 | import hudson.util.DataSetBuilder; 10 | import hudson.util.Graph; 11 | import hudson.util.ShiftedCategoryAxis; 12 | import hudson.util.StackedAreaRenderer2; 13 | import hudson.util.ChartUtil.NumberOnlyBuildLabel; 14 | import jenkins.model.Jenkins; 15 | 16 | import org.jfree.chart.ChartFactory; 17 | import org.jfree.chart.JFreeChart; 18 | import org.jfree.chart.axis.CategoryAxis; 19 | import org.jfree.chart.axis.CategoryLabelPositions; 20 | import org.jfree.chart.axis.NumberAxis; 21 | import org.jfree.chart.plot.CategoryPlot; 22 | import org.jfree.chart.plot.PlotOrientation; 23 | import org.jfree.chart.renderer.category.StackedAreaRenderer; 24 | import org.jfree.data.category.CategoryDataset; 25 | import org.jfree.ui.RectangleInsets; 26 | import org.kohsuke.stapler.Stapler; 27 | import org.kohsuke.stapler.StaplerRequest; 28 | import org.kohsuke.stapler.StaplerResponse; 29 | 30 | import javax.servlet.http.HttpServletResponse; 31 | 32 | import java.awt.Color; 33 | import java.io.IOException; 34 | 35 | /** 36 | * Project action object which displays the trend report on the project top page. 37 | */ 38 | public class AnchoreProjectAction implements Action { 39 | private final static class AnchoreTrendGraph extends Graph { 40 | private static final int MAX_HISTORY_DEFAULT = 100; 41 | private AnchoreAction base; 42 | private String relPath; 43 | 44 | private static Area calcDefaultSize() { 45 | Area res = Functions.getScreenResolution(); 46 | if(res!=null && res.width<=800) 47 | return new Area(250,100); 48 | else 49 | return new Area(500,200); 50 | } 51 | 52 | /** 53 | * Initialize the trend graph from a base AnchoreAction using a calculated default size. 54 | * 55 | * @param base the most recent AnchoreAction up to which the trend is shown 56 | * @param relPath URL rel path for tooltip URLs 57 | */ 58 | protected AnchoreTrendGraph(AnchoreAction base, String relPath){ 59 | this(base, calcDefaultSize(), relPath); 60 | } 61 | 62 | /** 63 | * Initialize the trend graph from a base AnchoreAction using a given default size. 64 | * 65 | * @param base the most recent AnchoreAction up to which the trend is shown 66 | * @param defaultSize graph's default size 67 | * @param relPath URL rel path for tooltip URLs 68 | */ 69 | private AnchoreTrendGraph(AnchoreAction base, Area defaultSize, String relPath){ 70 | super(base.getBuild().getTimestamp(), defaultSize.width, defaultSize.height); 71 | this.base = base; 72 | this.relPath = relPath; 73 | } 74 | 75 | private CategoryDataset buildDataSet() { 76 | DataSetBuilder dsb = new DataSetBuilder<>(); 77 | 78 | int cap = Integer.getInteger(AnchoreAction.class.getName() + ".anchore.trend.max", AnchoreTrendGraph.MAX_HISTORY_DEFAULT); 79 | int count = 0; 80 | for (AnchoreAction a = this.base; a != null; a = a.getPreviousResult()) { 81 | if (++count > cap) { 82 | break; 83 | } 84 | dsb.add(a.getGoActionCount(), "0_go", new NumberOnlyBuildLabel(a.getBuild())); 85 | dsb.add(a.getWarnActionCount(), "1_warn", new NumberOnlyBuildLabel(a.getBuild())); 86 | dsb.add(a.getStopActionCount(), "2_stop", new NumberOnlyBuildLabel(a.getBuild())); 87 | } 88 | return dsb.build(); 89 | } 90 | 91 | @Override 92 | protected JFreeChart createGraph(){ 93 | CategoryDataset dataset = buildDataSet(); 94 | final JFreeChart chart = ChartFactory.createStackedAreaChart( 95 | null, // chart title 96 | null, // category axis label 97 | "count", // range axis label 98 | dataset, 99 | PlotOrientation.VERTICAL, 100 | false, // include legend 101 | true, // generate tooltips 102 | false // generate urls 103 | ); 104 | 105 | chart.setBackgroundPaint(Color.white); 106 | 107 | final CategoryPlot plot = chart.getCategoryPlot(); 108 | 109 | plot.setBackgroundPaint(Color.WHITE); 110 | plot.setOutlinePaint(null); 111 | plot.setForegroundAlpha(0.8f); 112 | plot.setRangeGridlinesVisible(true); 113 | plot.setRangeGridlinePaint(Color.BLACK); 114 | 115 | CategoryAxis domainAxis = new ShiftedCategoryAxis(null); 116 | plot.setDomainAxis(domainAxis); 117 | domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90); 118 | domainAxis.setLowerMargin(0.0); 119 | domainAxis.setUpperMargin(0.0); 120 | domainAxis.setCategoryMargin(0.0); 121 | 122 | final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); 123 | rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 124 | 125 | StackedAreaRenderer ar = new StackedAreaRenderer2() { 126 | @Override 127 | public String generateURL(CategoryDataset data, int row, int column) { 128 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) data.getColumnKey(column); 129 | return relPath + label.getRun().getNumber() + "/anchore-results/"; 130 | } 131 | 132 | @Override 133 | public String generateToolTip(CategoryDataset data, int row, int column) { 134 | NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) data.getColumnKey(column); 135 | AnchoreAction a = label.getRun().getAction(AnchoreAction.class); 136 | switch (row) { 137 | case 0: 138 | return label.getRun().getDisplayName() + ": " + a.getGoActionCount() + " Go Actions"; 139 | case 1: 140 | return label.getRun().getDisplayName() + ": " + a.getWarnActionCount() + " Warn Actions"; 141 | default: 142 | return label.getRun().getDisplayName() + ": " + a.getStopActionCount() + " Stop Actions"; 143 | } 144 | } 145 | }; 146 | ar.setSeriesPaint(0, ColorPalette.BLUE); // Go 147 | ar.setSeriesPaint(1, ColorPalette.YELLOW); // Warn 148 | ar.setSeriesPaint(2, ColorPalette.RED); // Stop 149 | plot.setRenderer(ar); 150 | 151 | plot.setInsets(new RectangleInsets(0, 0, 0, 5.0)); 152 | 153 | return chart; 154 | } 155 | } 156 | 157 | /** 158 | * Parent that owns this action. 159 | */ 160 | public final Job job; 161 | 162 | /** 163 | * Create new AnchoreProjectAction instance. 164 | * 165 | * @param job instance of Jenkins Job 166 | */ 167 | public AnchoreProjectAction(Job job) { 168 | this.job = job; 169 | } 170 | 171 | @Override 172 | public String getIconFileName() { 173 | return Jenkins.RESOURCE_PATH + "/plugin/anchore-container-scanner/images/anchore.png"; 174 | } 175 | 176 | @Override 177 | public String getDisplayName() { 178 | return "Anchore Report"; 179 | } 180 | 181 | @Override 182 | public String getUrlName() { 183 | return "anchore"; 184 | } 185 | 186 | /** 187 | * Redirects the index page to the last report. 188 | * 189 | * @param request Stapler request 190 | * @param response Stapler response 191 | * @throws IOException in case of an error 192 | */ 193 | public void doIndex(final StaplerRequest request, final StaplerResponse response) throws IOException { 194 | Run lastRun = this.job.getLastCompletedBuild(); 195 | if (lastRun != null) { 196 | AnchoreAction a = lastRun.getAction(AnchoreAction.class); 197 | if (a != null) 198 | response.sendRedirect2(String.format("../%d/%s", lastRun.getNumber(), a.getUrlName())); 199 | } 200 | } 201 | 202 | /** 203 | * @return the most current AnchoreAction of the associated job 204 | */ 205 | public AnchoreAction getLastAnchoreAction() { 206 | final Run tb = this.job.getLastSuccessfulBuild(); 207 | 208 | Run b = this.job.getLastBuild(); 209 | while (b != null) { 210 | AnchoreAction a = b.getAction(AnchoreAction.class); 211 | if (a != null && (!b.isBuilding())) { 212 | return a; 213 | } 214 | if (b == tb) { 215 | // no Anchore result available 216 | return null; 217 | } 218 | b = b.getPreviousBuild(); 219 | } 220 | return null; 221 | } 222 | 223 | private String getRelPath(StaplerRequest req) { 224 | String relPath = req.getParameter("rel"); 225 | if (relPath == null) { 226 | return ""; 227 | } 228 | return relPath; 229 | } 230 | 231 | /** 232 | * Generates the Anchore trend graph 233 | * @return graph object 234 | */ 235 | public Graph getTrendGraph() { 236 | final AnchoreAction a = getLastAnchoreAction(); 237 | if (a != null) { 238 | return new AnchoreTrendGraph(a, getRelPath(Stapler.getCurrentRequest())); 239 | }else{ 240 | Stapler.getCurrentResponse().setStatus(HttpServletResponse.SC_NOT_FOUND); 241 | return null; 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/Annotation.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import hudson.Extension; 4 | import hudson.model.AbstractDescribableImpl; 5 | import hudson.model.Descriptor; 6 | import java.io.Serializable; 7 | import org.kohsuke.stapler.DataBoundConstructor; 8 | 9 | /** 10 | * Wrapper class for Anchore query 11 | */ 12 | public class Annotation extends AbstractDescribableImpl implements Serializable { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | private String key; 17 | private String value; 18 | 19 | public String getKey() { 20 | return key; 21 | } 22 | 23 | public String getValue() { 24 | return value; 25 | } 26 | 27 | @DataBoundConstructor 28 | public Annotation(String key, String value) { 29 | this.key = key; 30 | this.value = value; 31 | } 32 | 33 | @Extension 34 | public static class DescriptorImpl extends Descriptor { 35 | 36 | @Override 37 | public String getDisplayName() { 38 | return "Anchore Enterprise Image Annotation"; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/BuildConfig.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | 4 | import com.anchore.jenkins.plugins.anchore.Util.API_VERSION; 5 | import java.util.List; 6 | 7 | /** 8 | * Holder for all Anchore configuration - includes global and project level attributes. A convenience class for capturing a snapshot of 9 | * the config at the beginning of plugin execution and caching it for use during that specific execution 10 | */ 11 | public class BuildConfig { 12 | 13 | // Build configuration 14 | private String name; 15 | private String engineRetries; 16 | private String engineRetryInterval; 17 | private boolean bailOnFail; 18 | private boolean bailOnPluginFail; 19 | private String policyBundleId; 20 | private List annotations; 21 | private boolean autoSubscribeTagUpdates; 22 | private boolean forceAnalyze; 23 | private boolean excludeFromBaseImage; 24 | 25 | // Global configuration 26 | private boolean debug; 27 | private String anchoreui; 28 | private String engineurl; 29 | private String engineuser; 30 | private String enginepass; 31 | private String engineaccount; 32 | private boolean engineverify; 33 | private API_VERSION engineApiVersion; 34 | 35 | public BuildConfig(String name, String engineRetries, String engineRetryInterval, boolean bailOnFail, boolean bailOnPluginFail, 36 | String policyBundleId, List annotations, boolean autoSubscribeTagUpdates, boolean forceAnalyze, boolean excludeFromBaseImage, 37 | boolean debug, String anchoreui, String engineurl, String engineuser, String enginepass, String engineaccount, boolean engineverify) { 38 | this.name = name; 39 | this.engineRetries = engineRetries; 40 | this.engineRetryInterval = engineRetryInterval; 41 | this.bailOnFail = bailOnFail; 42 | this.bailOnPluginFail = bailOnPluginFail; 43 | this.policyBundleId = policyBundleId; 44 | this.annotations = annotations; 45 | this.autoSubscribeTagUpdates = autoSubscribeTagUpdates; 46 | this.forceAnalyze = forceAnalyze; 47 | this.excludeFromBaseImage = excludeFromBaseImage; 48 | this.debug = debug; 49 | this.anchoreui = anchoreui; 50 | this.engineurl = engineurl; 51 | this.engineuser = engineuser; 52 | this.enginepass = enginepass; 53 | this.engineaccount = engineaccount; 54 | this.engineverify = engineverify; 55 | this.engineApiVersion = Util.GET_API_VERSION_FROM_URL(engineurl); 56 | } 57 | 58 | public String getName() { 59 | return name; 60 | } 61 | 62 | public String getEngineRetries() { 63 | return engineRetries; 64 | } 65 | 66 | public String getEngineRetryInterval() { 67 | return engineRetryInterval; 68 | } 69 | 70 | public boolean getBailOnFail() { 71 | return bailOnFail; 72 | } 73 | 74 | public boolean getBailOnPluginFail() { 75 | return bailOnPluginFail; 76 | } 77 | 78 | public String getPolicyBundleId() { 79 | return policyBundleId; 80 | } 81 | 82 | public List getAnnotations() { 83 | return annotations; 84 | } 85 | 86 | public boolean getAutoSubscribeTagUpdates() { 87 | return autoSubscribeTagUpdates; 88 | } 89 | 90 | public boolean getForceAnalyze() { 91 | return forceAnalyze; 92 | } 93 | 94 | public boolean getExcludeFromBaseImage() { 95 | return excludeFromBaseImage; 96 | } 97 | 98 | public boolean getDebug() { 99 | return debug; 100 | } 101 | 102 | public String getAnchoreui() { 103 | return anchoreui; 104 | } 105 | 106 | public String getEngineurl() { 107 | return engineurl; 108 | } 109 | 110 | public String getEngineuser() { 111 | return engineuser; 112 | } 113 | 114 | public String getEnginepass() { 115 | return enginepass; 116 | } 117 | 118 | public String getEngineaccount() { 119 | return engineaccount; 120 | } 121 | 122 | public boolean getEngineverify() { 123 | return engineverify; 124 | } 125 | 126 | public API_VERSION getEngineApiVersion() { 127 | return engineApiVersion; 128 | } 129 | 130 | public void print(ConsoleLog consoleLog) { 131 | consoleLog.logInfo("[global] debug: " + String.valueOf(debug)); 132 | 133 | // Global or build properties 134 | consoleLog.logInfo("[build] engineurl: " + engineurl); 135 | consoleLog.logInfo("[build] engineuser: " + engineuser); 136 | consoleLog.logInfo("[build] enginepass: " + "****"); 137 | consoleLog.logInfo("[build] engineaccount: " + engineaccount); 138 | consoleLog.logInfo("[build] engineverify: " + String.valueOf(engineverify)); 139 | 140 | // Build properties 141 | consoleLog.logInfo("[build] name: " + name); 142 | consoleLog.logInfo("[build] engineRetries: " + engineRetries); 143 | consoleLog.logInfo("[build] engineRetryInterval: " + engineRetryInterval); 144 | consoleLog.logInfo("[build] policyBundleId: " + policyBundleId); 145 | if (null != annotations && !annotations.isEmpty()) { 146 | for (Annotation a : annotations) { 147 | consoleLog.logInfo("[build] annotation: " + a.getKey() + "=" + a.getValue()); 148 | } 149 | } 150 | consoleLog.logInfo("[build] bailOnFail: " + bailOnFail); 151 | consoleLog.logInfo("[build] bailOnPluginFail: " + bailOnPluginFail); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/BuildWorker.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import com.anchore.jenkins.plugins.anchore.Util.API_VERSION; 4 | import com.anchore.jenkins.plugins.anchore.Util.GATE_ACTION; 5 | import com.anchore.jenkins.plugins.anchore.Util.GATE_SUMMARY_COLUMN; 6 | import com.google.common.base.Strings; 7 | import com.google.common.base.Joiner; 8 | import hudson.AbortException; 9 | import hudson.FilePath; 10 | import hudson.Launcher; 11 | import hudson.PluginWrapper; 12 | import hudson.model.Run; 13 | import hudson.model.TaskListener; 14 | import hudson.tasks.ArtifactArchiver; 15 | import java.io.BufferedReader; 16 | import java.io.BufferedWriter; 17 | import java.io.IOException; 18 | import java.io.InputStreamReader; 19 | import java.io.OutputStreamWriter; 20 | import java.net.URLEncoder; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Iterator; 25 | import java.util.LinkedHashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.logging.Logger; 29 | import jenkins.model.Jenkins; 30 | import net.sf.json.JSONArray; 31 | import net.sf.json.JSONObject; 32 | import net.sf.json.JSONSerializer; 33 | 34 | import org.apache.commons.codec.binary.Base64; 35 | import org.apache.http.Header; 36 | import org.apache.http.auth.AuthScope; 37 | import org.apache.http.auth.UsernamePasswordCredentials; 38 | import org.apache.http.client.CredentialsProvider; 39 | import org.apache.http.client.methods.CloseableHttpResponse; 40 | import org.apache.http.client.methods.HttpGet; 41 | import org.apache.http.client.methods.HttpPost; 42 | import org.apache.http.client.protocol.HttpClientContext; 43 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 44 | import org.apache.http.conn.ssl.SSLContextBuilder; 45 | import org.apache.http.conn.ssl.TrustSelfSignedStrategy; 46 | import org.apache.http.entity.StringEntity; 47 | import org.apache.http.impl.client.BasicCredentialsProvider; 48 | import org.apache.http.impl.client.CloseableHttpClient; 49 | import org.apache.http.impl.client.HttpClients; 50 | import org.apache.http.message.BasicHeader; 51 | import org.apache.http.util.EntityUtils; 52 | 53 | /** 54 | * A helper class to ensure concurrent jobs don't step on each other's toes. Anchore plugin instantiates a new instance of this class 55 | * for each individual job i.e. invocation of perform(). Global and project configuration at the time of execution is loaded into 56 | * worker instance via its constructor. That specific worker instance is responsible for the bulk of the plugin operations for a given 57 | * job. 58 | */ 59 | public class BuildWorker { 60 | 61 | private static final Logger LOG = Logger.getLogger(BuildWorker.class.getName()); 62 | 63 | // TODO refactor 64 | private static final String GATES_OUTPUT_PREFIX = "anchore_gates"; 65 | private static final String CVE_LISTING_PREFIX = "anchore_security"; 66 | private static final String JENKINS_DIR_NAME= "AnchoreReport"; 67 | private static final String JSON_FILE_EXTENSION = ".json"; 68 | private static final String AE_VULNS_PREFIX = "anchoreengine-api-response-vulnerabilities-"; 69 | private static final String AE_EVAL_PREFIX = "anchoreengine-api-response-evaluation-"; 70 | 71 | // Private members 72 | Run build; 73 | FilePath workspace; 74 | Launcher launcher; 75 | TaskListener listener; 76 | BuildConfig config; 77 | 78 | 79 | /* Initialized by the constructor */ 80 | private ConsoleLog console; // Log handler for logging to build console 81 | private boolean analyzed; 82 | 83 | // Initialized by Jenkins workspace prep 84 | private String buildId; 85 | private String jenkinsOutputDirName; 86 | private Map queryOutputMap; // TODO rename 87 | private Map input_image_dfile = new LinkedHashMap<>(); 88 | private Map input_image_imageDigest = new LinkedHashMap<>(); 89 | private String gateOutputFileName; 90 | private GATE_ACTION finalAction; 91 | private JSONObject gateSummary; 92 | private int totalStopActionCount = 0; 93 | private int totalWarnActionCount = 0; 94 | private int totalGoActionCount = 0; 95 | private String cveListingFileName; 96 | 97 | public BuildWorker(Run build, FilePath workspace, Launcher launcher, TaskListener listener, BuildConfig config) 98 | throws AbortException { 99 | try { 100 | // Initialize build 101 | this.build = build; 102 | 103 | // Initialize workspace reference 104 | this.workspace = workspace; 105 | 106 | // Verify and initialize build listener 107 | if (null != listener) { 108 | this.listener = listener; 109 | } else { 110 | LOG.warning("Anchore Container Image Scanner plugin cannot initialize Jenkins task listener"); 111 | throw new AbortException("Cannot initialize Jenkins task listener. Aborting step"); 112 | } 113 | 114 | // Verify and initialize configuration 115 | if (null != config) { 116 | this.config = config; 117 | } else { 118 | LOG.warning("Anchore Container Image Scanner cannot find the required configuration"); 119 | throw new AbortException( 120 | "Configuration for the plugin is invalid. Configure the plugin under Manage Jenkins->Configure System->Anchore " 121 | + "Configuration first. Add the Anchore Container Image Scanner step in your project and retry"); 122 | } 123 | 124 | // Initialize build logger to log output to consoleLog, use local logging methods only after this initializer completes 125 | console = new ConsoleLog("AnchoreWorker", this.listener.getLogger(), this.config.getDebug()); 126 | 127 | console.logDebug("Initializing build worker"); 128 | 129 | // Verify and initialize Jenkins launcher for executing processes 130 | // TODO is this necessary? Can't we use the launcher reference that was passed in 131 | this.launcher = workspace.createLauncher(listener); 132 | // Node jenkinsNode = build.getBuiltOn(); 133 | // if (null != jenkinsNode) { 134 | // this.launcher = jenkinsNode.createLauncher(listener); 135 | // if (null == this.launcher) { 136 | // console.logError("Cannot initialize Jenkins process executor"); 137 | // throw new AbortException("Cannot initialize Jenkins process executor. Aborting step"); 138 | // } 139 | // } else { 140 | // console.logError("Cannot access Jenkins node running the build"); 141 | // throw new AbortException("Cannot access Jenkins node running the build. Aborting step"); 142 | // } 143 | 144 | // Initialize analyzed flag to false to indicate that analysis step has not run 145 | this.analyzed = false; 146 | 147 | // Print versions and build configuration 148 | printConfig(); 149 | 150 | // Check config 151 | checkConfig(); 152 | 153 | // Initialize Jenkins workspace 154 | initializeJenkinsWorkspace(); 155 | 156 | // Initialize Anchore workspace 157 | initializeAnchoreWorkspace(); 158 | 159 | console.logDebug("Build worker initialized"); 160 | } catch (Exception e) { 161 | try { 162 | if (console != null) { 163 | console.logError("Failed to initialize worker for plugin execution", e); 164 | } 165 | cleanJenkinsWorkspaceQuietly(); 166 | } catch (Exception innere) { 167 | 168 | } finally { 169 | throw new AbortException("Failed to initialize worker for plugin execution, check logs for corrective action"); 170 | } 171 | } 172 | } 173 | 174 | public void runAnalyzer() throws AbortException { 175 | runAnalyzerEngine(); 176 | } 177 | 178 | private static CloseableHttpClient makeHttpClient(boolean verify, String account) { 179 | CloseableHttpClient httpclient = null; 180 | List

headers = new ArrayList<>(); 181 | if (!Strings.isNullOrEmpty(account)) { 182 | Header header = new BasicHeader("x-anchore-account", account); 183 | headers.add(header); 184 | } 185 | 186 | if (verify) { 187 | httpclient = HttpClients.custom().setDefaultHeaders(headers).build(); 188 | } else { 189 | //SSLContextBuilder builder; 190 | 191 | //SSLConnectionSocketFactory sslsf=null; 192 | 193 | try { 194 | SSLContextBuilder builder = new SSLContextBuilder(); 195 | builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); 196 | SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), 197 | SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); 198 | httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).setDefaultHeaders(headers).build(); 199 | } catch (Exception e) { 200 | System.out.println(e); 201 | } 202 | } 203 | return (httpclient); 204 | } 205 | 206 | private void runAnalyzerEngine() throws AbortException { 207 | String imageDigest = null; 208 | String username = config.getEngineuser(); 209 | String password = config.getEnginepass(); 210 | String account = config.getEngineaccount(); 211 | boolean sslverify = config.getEngineverify(); 212 | 213 | CredentialsProvider credsProvider = new BasicCredentialsProvider(); 214 | credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); 215 | HttpClientContext context = HttpClientContext.create(); 216 | context.setCredentialsProvider(credsProvider); 217 | 218 | try { 219 | for (Map.Entry entry : input_image_dfile.entrySet()) { 220 | String tag = entry.getKey(); 221 | String dfile = entry.getValue(); 222 | List queryList = new ArrayList<>(); 223 | String queryStr = null; 224 | 225 | console.logInfo("Submitting " + tag + " for analysis"); 226 | 227 | try (CloseableHttpClient httpclient = makeHttpClient(sslverify, account)) { 228 | // Prep POST request 229 | String theurl = config.getEngineurl().replaceAll("/+$", "") + "/images"; 230 | 231 | 232 | String should_auto_subscribe = config.getAutoSubscribeTagUpdates() ? "true" : "false"; 233 | queryList.add("auto_subscribe=" + should_auto_subscribe); 234 | 235 | String should_force_image_add = config.getForceAnalyze() ? "true" : "false"; 236 | queryList.add("force=" + should_force_image_add); 237 | 238 | if (!queryList.isEmpty()){ 239 | queryStr = Joiner.on('&').skipNulls().join(queryList); 240 | } 241 | 242 | if (!Strings.isNullOrEmpty(queryStr)) { 243 | theurl += "?" + queryStr; 244 | } 245 | console.logDebug("Adding image using Enterprise API " + config.getEngineApiVersion()); 246 | JSONObject jsonBody = new JSONObject(); 247 | 248 | // Prep request body 249 | if (config.getEngineApiVersion() == API_VERSION.v1) { 250 | throw new AbortException("Requires Anchore Enterprise v2 API that can be found in Anchore Enterprise >= 4.9"); 251 | } else { 252 | JSONObject jTag = new JSONObject(); 253 | 254 | jTag.put("pull_string", tag); 255 | if (null != dfile) { 256 | jTag.put("dockerfile", dfile); 257 | } 258 | 259 | if (null != config.getAnnotations() && !config.getAnnotations().isEmpty()) { 260 | JSONObject annotations = new JSONObject(); 261 | for (Annotation a : config.getAnnotations()) { 262 | annotations.put(a.getKey(), a.getValue()); 263 | } 264 | jsonBody.put("annotations", annotations); 265 | } 266 | 267 | JSONObject tagSource = new JSONObject(); 268 | 269 | tagSource.put("tag", jTag); 270 | 271 | jsonBody.put("source", tagSource); 272 | } 273 | 274 | String body = jsonBody.toString(); 275 | 276 | HttpPost httppost = new HttpPost(theurl); 277 | httppost.addHeader("Content-Type", "application/json"); 278 | httppost.setEntity(new StringEntity(body)); 279 | 280 | console.logDebug("anchore-enterprise add image URL: " + theurl); 281 | console.logDebug("anchore-enterprise add image payload: " + body); 282 | 283 | try (CloseableHttpResponse response = httpclient.execute(httppost, context)) { 284 | int statusCode = response.getStatusLine().getStatusCode(); 285 | if (statusCode != 200) { 286 | String serverMessage = EntityUtils.toString(response.getEntity()); 287 | console.logError( 288 | "anchore-enterprise add image failed. URL: " + theurl + ", status: " + response.getStatusLine() + ", error: " 289 | + serverMessage); 290 | throw new AbortException("Failed to analyze " + tag 291 | + " due to error adding image to anchore-enterprise. Check above logs for errors from anchore-enterprise"); 292 | } else { 293 | // Read the response body. 294 | String responseBody = EntityUtils.toString(response.getEntity()); 295 | // TODO EntityUtils.consume(entity2); 296 | 297 | JSONObject respJson = JSONObject.fromObject(responseBody); 298 | imageDigest = JSONObject.fromObject(respJson).getString("image_digest"); 299 | 300 | console.logInfo("Analysis request accepted, received image digest " + imageDigest); 301 | input_image_imageDigest.put(tag, imageDigest); 302 | } 303 | } catch (Throwable e) { 304 | throw e; 305 | } 306 | } catch (Throwable e) { 307 | throw e; 308 | } 309 | } 310 | analyzed = true; 311 | } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 312 | throw e; 313 | } catch (Exception e) { // caught unknown exception, log it and wrap its 314 | console.logError("Failed to add image(s) to anchore-enterprise due to an unexpected error", e); 315 | throw new AbortException( 316 | "Failed to add image(s) to anchore-enterprise due to an unexpected error. Please refer to above logs for more information"); 317 | } 318 | } 319 | 320 | private void writeResponseToFile(Integer counter, FilePath jenkinsOutputDirFP, String responseBody) throws AbortException { 321 | // Write api response to a file as it is 322 | String jenkinsAEResponseFileName = AE_EVAL_PREFIX + (counter) + JSON_FILE_EXTENSION; 323 | FilePath jenkinsAEResponseFP = new FilePath(jenkinsOutputDirFP, jenkinsAEResponseFileName); 324 | 325 | try { 326 | console.logDebug("Writing anchore-enterprise policy evaluation response to " + jenkinsAEResponseFP.getRemote()); 327 | try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(jenkinsAEResponseFP.write(), StandardCharsets.UTF_8))) { 328 | bw.write(responseBody); 329 | } 330 | } catch (IOException | InterruptedException e) { 331 | console.logWarn("Failed to write anchore-enterprise policy evaluation response to " + jenkinsAEResponseFP.getRemote(), e); 332 | throw new AbortException("Failed to write anchore-enterprise policy evaluation response to " + jenkinsAEResponseFP.getRemote()); 333 | } 334 | 335 | } 336 | 337 | public GATE_ACTION runGates() throws AbortException { 338 | if (config.getEngineApiVersion() == API_VERSION.v1) { 339 | throw new AbortException("Requires Anchore Enterprise v2 API that can be found in Anchore Enterprise >= 4.9"); 340 | } 341 | console.logDebug("Using Enterprise API " + config.getEngineApiVersion()); 342 | return runGatesEngineV2(); 343 | } 344 | 345 | private GATE_ACTION runGatesEngineV2() throws AbortException { 346 | String username = config.getEngineuser(); 347 | String password = config.getEnginepass(); 348 | String account = config.getEngineaccount(); 349 | String anchoreui = config.getAnchoreui(); 350 | boolean sslverify = config.getEngineverify(); 351 | 352 | CredentialsProvider credsProvider = new BasicCredentialsProvider(); 353 | credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); 354 | HttpClientContext context = HttpClientContext.create(); 355 | context.setCredentialsProvider(credsProvider); 356 | 357 | //Credentials defaultcreds = new UsernamePasswordCredentials(username, password); 358 | FilePath jenkinsOutputDirFP = new FilePath(workspace, jenkinsOutputDirName); 359 | FilePath jenkinsGatesOutputFP = new FilePath(jenkinsOutputDirFP, gateOutputFileName); 360 | int counter = 0; 361 | 362 | finalAction = GATE_ACTION.PASS; 363 | if (analyzed) { 364 | try { 365 | JSONArray gate_results = new JSONArray(); 366 | 367 | for (Map.Entry entry : input_image_imageDigest.entrySet()) { 368 | String tag = entry.getKey(); 369 | String imageDigest = entry.getValue(); 370 | 371 | console.logInfo("Waiting for analysis of " + tag + ", polling status periodically"); 372 | 373 | Boolean anchore_eval_status = false; 374 | String imageURL = 375 | config.getEngineurl().replaceAll("/+$", "") + "/images/" + imageDigest; 376 | 377 | int tryCount = 0; 378 | int maxCount = Integer.parseInt(config.getEngineRetries()); 379 | int retryInterval = Integer.parseInt(config.getEngineRetryInterval()); 380 | Boolean done = false; 381 | HttpGet httpgetCheckAnalysis = new HttpGet(imageURL); 382 | httpgetCheckAnalysis.addHeader("Content-Type", "application/json"); 383 | int statusCode; 384 | String serverMessage = null; 385 | boolean sleep = false; 386 | JSONArray evaluations = null; 387 | 388 | do { // try this at least once regardless what the retry count is 389 | if (sleep) { 390 | console.logDebug("Snoozing before retrying anchore-enterprise get policy evaluation"); 391 | Thread.sleep(1000 * retryInterval); 392 | sleep = false; 393 | } 394 | 395 | tryCount++; 396 | try (CloseableHttpClient httpclient = makeHttpClient(sslverify, account)) { 397 | console.logDebug("Attempting anchore-enterprise check for image analysis (" + tryCount + "/" + maxCount + ")"); 398 | 399 | try (CloseableHttpResponse responseCheckAnalysis = httpclient.execute(httpgetCheckAnalysis, context)) { 400 | statusCode = responseCheckAnalysis.getStatusLine().getStatusCode(); 401 | 402 | if (statusCode != 200) { 403 | serverMessage = EntityUtils.toString(responseCheckAnalysis.getEntity()); 404 | console.logDebug( 405 | "anchore-enterprise get analysis status failed. URL: " + imageURL + ", status: " + responseCheckAnalysis.getStatusLine() 406 | + ", error: " + serverMessage); 407 | sleep = true; 408 | } else { 409 | // Look for analyzed image before proceeding 410 | String responseBodyCheckAnalysis = EntityUtils.toString(responseCheckAnalysis.getEntity()); 411 | JSONObject imageResponse = (JSONObject) JSONSerializer.toJSON(responseBodyCheckAnalysis); 412 | String imageAnalysisStatus = imageResponse.getString("analysis_status"); 413 | 414 | if (imageAnalysisStatus.equals("analysis_failed")) { 415 | console.logWarn("anchore-enterprise reporting analysis failed for " + imageDigest); 416 | throw new AbortException("Analysis failed for " + imageDigest); 417 | } else if (!imageAnalysisStatus.equals("analyzed")) { 418 | console.logDebug("anchore-enterprise get analysis status: " + imageAnalysisStatus); 419 | sleep = true; 420 | } else { 421 | String imageRegistry = imageResponse.getJSONArray("image_detail").getJSONObject(0).getString("registry"); 422 | String imageRepo = imageResponse.getJSONArray("image_detail").getJSONObject(0).getString("repo"); 423 | String imageTag = imageResponse.getJSONArray("image_detail").getJSONObject(0).getString("tag"); 424 | 425 | // Get the list of ancestors to determine base image 426 | String ancestorsURL = imageURL + "/ancestors"; 427 | HttpGet httpgetAncestors = new HttpGet(ancestorsURL); 428 | httpgetAncestors.addHeader("Content-Type", "application/json"); 429 | 430 | try (CloseableHttpResponse responseAncestors = httpclient.execute(httpgetAncestors, context)) { 431 | statusCode = responseAncestors.getStatusLine().getStatusCode(); 432 | if (statusCode != 200) { 433 | serverMessage = EntityUtils.toString(responseAncestors.getEntity()); 434 | console.logDebug( 435 | "anchore-enterprise get ancestors failed. URL: " + ancestorsURL + ", status: " + responseAncestors.getStatusLine() 436 | + ", error: " + serverMessage); 437 | sleep = true; 438 | } else { 439 | // Get the base image from ancestors 440 | String responseBodyAncestors = EntityUtils.toString(responseAncestors.getEntity()); 441 | 442 | String policyCheckURL = null; 443 | 444 | JSONArray ancestors = (JSONArray) JSONSerializer.toJSON(responseBodyAncestors); 445 | if (ancestors.size() < 1) { 446 | console.logDebug("anchore-enterprise get ancestors response contains no records for image: " + ancestorsURL); 447 | policyCheckURL = 448 | config.getEngineurl().replaceAll("/+$", "") + "/images/" + imageDigest + "/check?tag=" + tag 449 | + "&detail=true"; 450 | } else { 451 | String baseImageDigest = null; 452 | 453 | // Get the chosen_base image from API 454 | for (int i = 0; i < ancestors.size(); i++) { 455 | JSONObject ancestor = ancestors.getJSONObject(i); 456 | // If chosen_base_image key exists in the response, use it. 457 | // This is required for compatibility with < Anchore Enterprise 5.7 458 | if (ancestor.has("chosen_base_image")) { 459 | if (ancestor.getBoolean("chosen_base_image")) { 460 | console.logDebug("found base image from API"); 461 | baseImageDigest = ancestor.getString("image_digest"); 462 | break; 463 | } 464 | } 465 | } 466 | 467 | // Get the last ancestor to determine the base image if no chosen_base image from API 468 | // This is required for compatibility with < Anchore Enterprise 5.7 469 | if (baseImageDigest == null) { 470 | JSONObject lastAncestor = ancestors.getJSONObject(ancestors.size() - 1); 471 | baseImageDigest = lastAncestor.getString("image_digest"); 472 | } 473 | 474 | policyCheckURL = 475 | config.getEngineurl().replaceAll("/+$", "") + "/images/" + imageDigest + "/check?tag=" + tag 476 | + "&detail=true&base_digest=" + baseImageDigest; 477 | } 478 | 479 | if (!Strings.isNullOrEmpty(config.getPolicyBundleId())) { 480 | policyCheckURL += "&policy_id=" + config.getPolicyBundleId(); 481 | } 482 | console.logDebug("anchore-enterprise get policy evaluation URL: " + policyCheckURL); 483 | 484 | HttpGet httpgetPolicyCheck = new HttpGet(policyCheckURL); 485 | httpgetPolicyCheck.addHeader("Content-Type", "application/json"); 486 | 487 | try (CloseableHttpResponse responsePolicyCheck = httpclient.execute(httpgetPolicyCheck, context)) { 488 | statusCode = responsePolicyCheck.getStatusLine().getStatusCode(); 489 | 490 | if (statusCode != 200) { 491 | serverMessage = EntityUtils.toString(responsePolicyCheck.getEntity()); 492 | console.logDebug( 493 | "anchore-enterprise get policy evaluation failed. URL: " + policyCheckURL + ", status: " + statusCode 494 | + ", error: " + serverMessage); 495 | sleep = true; 496 | } else { 497 | // Read the response body. 498 | String responseBodyPolicyCheck = EntityUtils.toString(responsePolicyCheck.getEntity()); 499 | 500 | JSONObject topDocument = (JSONObject) JSONSerializer.toJSON(responseBodyPolicyCheck); 501 | evaluations = topDocument.getJSONArray("evaluations"); 502 | JSONObject policyJsonObject = evaluations.getJSONObject(0); 503 | JSONObject evaluationDetails = policyJsonObject.getJSONObject("details"); 504 | JSONArray evaluationFindings = evaluationDetails.getJSONArray("findings"); 505 | String gate_resulting_action = policyJsonObject.getString("final_action"); 506 | String gate_resulting_reason = policyJsonObject.getString("final_action_reason"); 507 | String gate_result_details = ""; 508 | 509 | if (evaluations.size() < 1) { 510 | // try again until we get an eval 511 | console 512 | .logDebug("anchore-enterprise get policy evaluation response contains no evaluations records. May snooze and retry"); 513 | sleep = true; 514 | } else { 515 | counter = counter + 1; 516 | 517 | if (gate_resulting_action.equals("stop")) { 518 | if (gate_resulting_reason.equals("policy_evaluation")) { 519 | gate_result_details = "Policy evaluation failed"; 520 | } else { 521 | // Catch all for other stop actions 522 | gate_result_details = "Failed due to " + gate_resulting_reason; 523 | } 524 | } 525 | 526 | // remove records where inherited_from_base is true 527 | if (config.getExcludeFromBaseImage()) { 528 | for (Iterator it = evaluationFindings.iterator(); it.hasNext();) { 529 | JSONObject finding = (JSONObject) it.next(); 530 | if (finding.getString("inherited_from_base").equals("true")) { 531 | it.remove(); 532 | } 533 | } 534 | 535 | // Check for case where all findings are inherited from base image 536 | if ((evaluationFindings.size() == 0) && gate_resulting_action.equals("stop") && gate_resulting_reason.equals("policy_evaluation")) { 537 | console.logInfo("No findings to evaluate after excluding inherited_from_base. Failure is in base image."); 538 | gate_result_details = "Failure inherited from base image"; 539 | } 540 | 541 | // convert back to a string of the whole response with the changes 542 | evaluationDetails.put("findings", evaluationFindings); 543 | policyJsonObject.put("details", evaluationDetails); 544 | evaluations.set(0, policyJsonObject); 545 | topDocument.put("evaluations", evaluations); 546 | responseBodyPolicyCheck = topDocument.toString(); 547 | } 548 | 549 | writeResponseToFile(counter, jenkinsOutputDirFP, responseBodyPolicyCheck); 550 | 551 | JSONObject gate_result = new JSONObject(); 552 | 553 | gate_result.put("image_digest", imageDigest); 554 | if (!Strings.isNullOrEmpty(anchoreui)) { 555 | String encodedImageRegistry = URLEncoder.encode(imageRegistry, StandardCharsets.UTF_8.toString()); 556 | String encodedImageRepo = URLEncoder.encode(imageRepo, StandardCharsets.UTF_8.toString()); 557 | if (!Strings.isNullOrEmpty(account)) { 558 | gate_result.put("repo_tag", anchoreui.replaceAll("/+$", "")+"/"+account+"/artifacts/image/"+encodedImageRegistry+"/"+encodedImageRepo+"/"+imageTag+"/"+imageDigest+" "+topDocument.getString("evaluated_tag")); 559 | } 560 | else { 561 | gate_result.put("repo_tag", anchoreui.replaceAll("/+$", "")+"/artifacts/image/"+encodedImageRegistry+"/"+encodedImageRepo+"/"+imageTag+"/"+imageDigest+" "+topDocument.getString("evaluated_tag")); 562 | } 563 | } else { 564 | gate_result.put("repo_tag", topDocument.getString("evaluated_tag")); 565 | } 566 | gate_result.put("final_action", gate_resulting_action); 567 | gate_result.put("failure_details", gate_result_details); 568 | gate_result.put("gate_results", evaluationFindings); 569 | 570 | gate_results.add(gate_result); 571 | 572 | console.logDebug("anchore-enterprise get policy evaluation result: " + gate_resulting_action.toString()); 573 | 574 | // we actually got a real result 575 | // this is the only way this gets flipped to true 576 | anchore_eval_status = policyJsonObject.getString("status").equals("pass"); 577 | console.logDebug("anchore-enterprise get policy evaluation status: " + anchore_eval_status); 578 | 579 | done = true; 580 | console.logInfo("Completed analysis and processed policy evaluation result"); 581 | } 582 | } 583 | } 584 | } 585 | } 586 | } 587 | } 588 | } catch (Throwable e) { 589 | throw e; 590 | } 591 | } catch (Throwable e) { 592 | throw e; 593 | } 594 | } while (!done && tryCount < maxCount); 595 | 596 | if (!done) { 597 | if (statusCode != 200) { 598 | console.logWarn( 599 | "anchore-enterprise get policy evaluation failed. HTTP method: GET, URL: " + imageURL + ", status: " + statusCode 600 | + ", error: " + serverMessage); 601 | } 602 | console.logWarn("Exhausted all attempts polling anchore-enterprise. Analysis is incomplete for " + imageDigest); 603 | throw new AbortException( 604 | "Timed out waiting for anchore-enterprise analysis to complete (increasing engineRetries might help). Check above logs " 605 | + "for errors from anchore-enterprise"); 606 | } else { 607 | // only set to stop if an eval is successful and is reporting fail 608 | if (!anchore_eval_status) { 609 | finalAction = GATE_ACTION.FAIL; 610 | } 611 | } 612 | } 613 | 614 | try { 615 | console.logDebug("Writing policy evaluation result to " + jenkinsGatesOutputFP.getRemote()); 616 | try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(jenkinsGatesOutputFP.write(), StandardCharsets.UTF_8))) { 617 | bw.write(gate_results.toString()); 618 | } 619 | } catch (IOException | InterruptedException e) { 620 | console.logWarn("Failed to write policy evaluation output to " + jenkinsGatesOutputFP.getRemote(), e); 621 | throw new AbortException("Failed to write policy evaluation output to " + jenkinsGatesOutputFP.getRemote()); 622 | } 623 | 624 | generateGatesSummaryV2(gate_results); 625 | 626 | console.logInfo("Anchore Container Image Scanner Plugin step result - " + finalAction); 627 | return finalAction; 628 | } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 629 | throw e; 630 | } catch (Exception e) { // caught unknown exception, log it and wrap it 631 | console.logError("Failed to execute anchore-enterprise policy evaluation due to an unexpected error", e); 632 | throw new AbortException( 633 | "Failed to execute anchore-enterprise policy evaluation due to an unexpected error. Please refer to above logs for more " 634 | + "information"); 635 | } 636 | } else { 637 | console.logError( 638 | "Image(s) were not added to anchore-enterprise (or a prior attempt to add images may have failed). Re-submit image(s) to " 639 | + "anchore-enterprise before attempting policy evaluation"); 640 | throw new AbortException("Submit image(s) to anchore-enterprise for analysis before attempting policy evaluation"); 641 | } 642 | 643 | } 644 | 645 | private void runVulnerabilityListing() throws AbortException { 646 | if (analyzed) { 647 | String username = config.getEngineuser(); 648 | String password = config.getEnginepass(); 649 | String account = config.getEngineaccount(); 650 | boolean sslverify = config.getEngineverify(); 651 | 652 | FilePath jenkinsOutputDirFP = new FilePath(workspace, jenkinsOutputDirName); 653 | int counter = 0; 654 | 655 | CredentialsProvider credsProvider = new BasicCredentialsProvider(); 656 | credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); 657 | HttpClientContext context = HttpClientContext.create(); 658 | context.setCredentialsProvider(credsProvider); 659 | 660 | try { 661 | JSONObject securityJson = new JSONObject(); 662 | JSONArray columnsJson = new JSONArray(); 663 | for (String column : Arrays.asList("Tag", "CVE ID", "Severity", "Vulnerability Package", "Fix Available", "Inherited From Base", "URL")) { 664 | JSONObject columnJson = new JSONObject(); 665 | columnJson.put("title", column); 666 | columnsJson.add(columnJson); 667 | } 668 | JSONArray dataJson = new JSONArray(); 669 | 670 | for (Map.Entry entry : input_image_imageDigest.entrySet()) { 671 | String input = entry.getKey(); 672 | String digest = entry.getValue(); 673 | 674 | try (CloseableHttpClient httpclient = makeHttpClient(sslverify, account)) { 675 | String ancestorsURL = config.getEngineurl().replaceAll("/+$", "") + "/images/" + digest + "/ancestors"; 676 | HttpGet httpgetAncestors = new HttpGet(ancestorsURL); 677 | httpgetAncestors.addHeader("Content-Type", "application/json"); 678 | 679 | try (CloseableHttpResponse responseAncestors = httpclient.execute(httpgetAncestors, context)) { 680 | int statusCode = responseAncestors.getStatusLine().getStatusCode(); 681 | if (statusCode != 200) { 682 | String serverMessage = EntityUtils.toString(responseAncestors.getEntity()); 683 | console.logDebug( 684 | "anchore-enterprise get ancestors failed. URL: " + ancestorsURL + ", status: " + responseAncestors.getStatusLine() 685 | + ", error: " + serverMessage); 686 | throw new AbortException("Failed to fetch vulnerability listing from anchore-enterprise"); 687 | } else { 688 | // Get the last ancestor to determine the base image 689 | String responseBodyAncestors = EntityUtils.toString(responseAncestors.getEntity()); 690 | 691 | String vulnListURL = null; 692 | 693 | JSONArray ancestors = (JSONArray) JSONSerializer.toJSON(responseBodyAncestors); 694 | if (ancestors.size() < 1) { 695 | console.logDebug("anchore-enterprise get ancestors response contains no records for image: " + ancestorsURL); 696 | vulnListURL = config.getEngineurl().replaceAll("/+$", "") + "/images/" + digest + "/vuln/all"; 697 | } else { 698 | String baseImageDigest = null; 699 | 700 | // Get the chosen_base image from API 701 | for (int i = 0; i < ancestors.size(); i++) { 702 | JSONObject ancestor = ancestors.getJSONObject(i); 703 | // If chosen_base_image key exists in the response, use it. 704 | // This is required for compatibility with < Anchore Enterprise 5.7 705 | if (ancestor.has("chosen_base_image")) { 706 | if (ancestor.getBoolean("chosen_base_image")) { 707 | console.logDebug("found base image from API"); 708 | baseImageDigest = ancestor.getString("image_digest"); 709 | break; 710 | } 711 | } 712 | } 713 | 714 | // Get the last ancestor to determine the base image if no chosen_base image from API 715 | // This is required for compatibility with < Anchore Enterprise 5.7 716 | if (baseImageDigest == null) { 717 | JSONObject lastAncestor = ancestors.getJSONObject(ancestors.size() - 1); 718 | baseImageDigest = lastAncestor.getString("image_digest"); 719 | } 720 | 721 | vulnListURL = config.getEngineurl().replaceAll("/+$", "") + "/images/" + digest + "/vuln/all" 722 | + "?base_digest=" + baseImageDigest; 723 | } 724 | 725 | console.logInfo("Querying vulnerability listing for " + input); 726 | HttpGet httpget = new HttpGet(vulnListURL); 727 | httpget.addHeader("Content-Type", "application/json"); 728 | 729 | console.logDebug("anchore-enterprise get vulnerability listing URL: " + vulnListURL); 730 | try (CloseableHttpResponse response = httpclient.execute(httpget, context)) { 731 | statusCode = response.getStatusLine().getStatusCode(); 732 | if (statusCode != 200) { 733 | String serverMessage = EntityUtils.toString(response.getEntity()); 734 | console.logWarn( 735 | "anchore-enterprise get vulnerability listing failed. URL: " + vulnListURL + ", status: " + response.getStatusLine() 736 | + ", error: " + serverMessage); 737 | throw new AbortException("Failed to fetch vulnerability listing from anchore-enterprise"); 738 | } else { 739 | String responseBody = EntityUtils.toString(response.getEntity()); 740 | // Write api response to a file as it is 741 | String jenkinsAEResponseFileName = AE_VULNS_PREFIX + (++counter) + JSON_FILE_EXTENSION; 742 | FilePath jenkinsAEResponseFP = new FilePath(jenkinsOutputDirFP, jenkinsAEResponseFileName); 743 | try { 744 | console.logDebug("Writing anchore-enterprise vulnerabilities listing response to " + jenkinsAEResponseFP.getRemote()); 745 | try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(jenkinsAEResponseFP.write(), StandardCharsets.UTF_8))) { 746 | bw.write(responseBody); 747 | } 748 | } catch (IOException | InterruptedException e) { 749 | console.logWarn("Failed to write anchore-enterprise vulnerabilities listing response to " + jenkinsAEResponseFP.getRemote(), e); 750 | throw new AbortException("Failed to write anchore-enterprise vulnerabilities listing response to " + jenkinsAEResponseFP.getRemote()); 751 | } 752 | 753 | JSONObject responseJson = JSONObject.fromObject(responseBody); 754 | JSONArray vulList = responseJson.getJSONArray("vulnerabilities"); 755 | for (int i = 0; i < vulList.size(); i++) { 756 | JSONObject vulnJson = vulList.getJSONObject(i); 757 | JSONArray vulnArray = new JSONArray(); 758 | if (ancestors.size() < 1) { 759 | vulnArray.addAll(Arrays 760 | .asList(input, vulnJson.getString("vuln"), vulnJson.getString("severity"), vulnJson.getString("package"), 761 | vulnJson.getString("fix"), "false", vulnJson.getString("url"))); 762 | } else { 763 | if (config.getExcludeFromBaseImage()) { 764 | if (vulnJson.getString("inherited_from_base").equals("true")) { 765 | continue; 766 | } else { 767 | vulnArray.addAll(Arrays 768 | .asList(input, vulnJson.getString("vuln"), vulnJson.getString("severity"), vulnJson.getString("package"), 769 | vulnJson.getString("fix"), vulnJson.getString("inherited_from_base"), vulnJson.getString("url"))); 770 | } 771 | 772 | } else { 773 | vulnArray.addAll(Arrays 774 | .asList(input, vulnJson.getString("vuln"), vulnJson.getString("severity"), vulnJson.getString("package"), 775 | vulnJson.getString("fix"), vulnJson.getString("inherited_from_base"), vulnJson.getString("url"))); 776 | } 777 | } 778 | dataJson.add(vulnArray); 779 | } 780 | } 781 | } catch (Throwable t) { 782 | throw t; 783 | } 784 | } 785 | } catch (Throwable t) { 786 | throw t; 787 | } 788 | } catch (Throwable t) { 789 | throw t; 790 | } 791 | } 792 | securityJson.put("columns", columnsJson); 793 | securityJson.put("data", dataJson); 794 | 795 | cveListingFileName = CVE_LISTING_PREFIX + JSON_FILE_EXTENSION; 796 | FilePath jenkinsQueryOutputFP = new FilePath(jenkinsOutputDirFP, cveListingFileName); 797 | try { 798 | console.logDebug("Writing vulnerability listing result to " + jenkinsQueryOutputFP.getRemote()); 799 | try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(jenkinsQueryOutputFP.write(), StandardCharsets.UTF_8))) { 800 | bw.write(securityJson.toString()); 801 | } 802 | } catch (IOException | InterruptedException e) { 803 | console.logWarn("Failed to write vulnerability listing to " + jenkinsQueryOutputFP.getRemote(), e); 804 | throw new AbortException("Failed to write vulnerability listing to " + jenkinsQueryOutputFP.getRemote()); 805 | } 806 | } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 807 | throw e; 808 | } catch (Exception e) { // caught unknown exception, log it and wrap it 809 | console.logError("Failed to fetch vulnerability listing from anchore-enterprise due to an unexpected error", e); 810 | throw new AbortException( 811 | "Failed to fetch vulnerability listing from anchore-enterprise due to an unexpected error. Please refer to above logs for " 812 | + "more information"); 813 | } 814 | } else { 815 | console.logError( 816 | "Image(s) were not added to anchore-enterprise (or a prior attempt to add images may have failed). Re-submit image(s) to " 817 | + "anchore-enterprise before attempting vulnerability listing"); 818 | throw new AbortException("Submit image(s) to anchore-enterprise for analysis before attempting vulnerability listing"); 819 | } 820 | } 821 | 822 | private void generateGatesSummaryV2(JSONArray gatesJson) { 823 | console.logDebug("Summarizing policy evaluation results"); 824 | 825 | if (gatesJson != null) { 826 | JSONArray summaryRows = new JSONArray(); 827 | 828 | for (Object gateResult : gatesJson) { 829 | JSONArray evaluationFindingContent = JSONObject.fromObject(gateResult).getJSONArray("gate_results"); 830 | String repoTag = JSONObject.fromObject(gateResult).getString("repo_tag"); 831 | String imageDigest = JSONObject.fromObject(gateResult).getString("image_digest"); 832 | String final_action = JSONObject.fromObject(gateResult).getString("final_action"); 833 | String failure_details = JSONObject.fromObject(gateResult).getString("failure_details"); 834 | int stop = 0, warn = 0, go = 0, stop_wl = 0, warn_wl = 0, go_wl = 0; 835 | 836 | for (Object finding : evaluationFindingContent) { 837 | if (null != finding) { 838 | 839 | JSONObject currentFinding = JSONObject.fromObject(finding); 840 | 841 | Boolean isAllowlisted = currentFinding.getBoolean("allowlisted"); 842 | 843 | switch (currentFinding.getString("action").toLowerCase()) { 844 | case "stop": 845 | stop++; 846 | stop_wl = isAllowlisted ? ++stop_wl : stop_wl; 847 | break; 848 | case "warn": 849 | warn++; 850 | warn_wl = isAllowlisted ? ++warn_wl : warn_wl; 851 | break; 852 | case "go": 853 | go++; 854 | go_wl = isAllowlisted ? ++go_wl : go_wl; 855 | break; 856 | default: 857 | break; 858 | } 859 | } 860 | } 861 | 862 | totalStopActionCount += (stop - stop_wl); 863 | totalWarnActionCount += (warn - warn_wl); 864 | totalGoActionCount += (go - go_wl); 865 | 866 | if (!Strings.isNullOrEmpty(repoTag)) { 867 | console.logInfo("Policy evaluation summary for " + repoTag + " - stop: " + (stop - stop_wl) + " (+" + stop_wl 868 | + " allowlisted), warn: " + (warn - warn_wl) + " (+" + warn_wl + " allowlisted), go: " + (go - go_wl) + " (+" 869 | + go_wl + " allowlisted), final: " + final_action); 870 | 871 | JSONObject summaryRow = new JSONObject(); 872 | summaryRow.put(GATE_SUMMARY_COLUMN.Repo_Tag.toString(), repoTag); 873 | summaryRow.put(GATE_SUMMARY_COLUMN.Stop_Actions.toString(), (stop - stop_wl)); 874 | summaryRow.put(GATE_SUMMARY_COLUMN.Warn_Actions.toString(), (warn - warn_wl)); 875 | summaryRow.put(GATE_SUMMARY_COLUMN.Go_Actions.toString(), (go - go_wl)); 876 | summaryRow.put(GATE_SUMMARY_COLUMN.Final_Action.toString(), final_action); 877 | summaryRow.put(GATE_SUMMARY_COLUMN.Stop_Action_Details.toString(), failure_details); 878 | summaryRows.add(summaryRow); 879 | } else { 880 | console.logInfo("Policy evaluation summary for " + imageDigest + " - stop: " + (stop - stop_wl) + " (+" + stop_wl 881 | + " allowlisted), warn: " + (warn - warn_wl) + " (+" + warn_wl + " allowlisted), go: " + (go - go_wl) + " (+" 882 | + go_wl + " allowlisted), final: " + final_action); 883 | JSONObject summaryRow = new JSONObject(); 884 | summaryRow.put(GATE_SUMMARY_COLUMN.Repo_Tag.toString(), repoTag.toString()); 885 | summaryRow.put(GATE_SUMMARY_COLUMN.Stop_Actions.toString(), (stop - stop_wl)); 886 | summaryRow.put(GATE_SUMMARY_COLUMN.Warn_Actions.toString(), (warn - warn_wl)); 887 | summaryRow.put(GATE_SUMMARY_COLUMN.Go_Actions.toString(), (go - go_wl)); 888 | summaryRow.put(GATE_SUMMARY_COLUMN.Final_Action.toString(), final_action); 889 | summaryRow.put(GATE_SUMMARY_COLUMN.Stop_Action_Details.toString(), failure_details); 890 | summaryRows.add(summaryRow); 891 | 892 | //console.logWarn("Repo_Tag element not found in gate output, skipping summary computation for " + imageKey); 893 | console.logWarn("Repo_Tag element not found in gate output, using imageDigest: " + imageDigest); 894 | } 895 | } 896 | gateSummary = new JSONObject(); 897 | gateSummary.put("header", generateDataTablesColumnsForGateSummary()); 898 | gateSummary.put("rows", summaryRows); 899 | } else { // could not load gates output to json object 900 | console.logWarn("Invalid input to generate gates summary"); 901 | } 902 | } 903 | 904 | public void runQueries() throws AbortException { 905 | runVulnerabilityListing(); 906 | } 907 | 908 | public void setupBuildReports() throws AbortException { 909 | try { 910 | // store anchore output json files using jenkins archiver (for remote storage as well) 911 | console.logDebug("Archiving results"); 912 | // FilePath buildWorkspaceFP = build.getWorkspace(); 913 | // if (null != buildWorkspaceFP) { 914 | ArtifactArchiver artifactArchiver = new ArtifactArchiver(jenkinsOutputDirName + "/"); 915 | artifactArchiver.perform(build, workspace, launcher, listener); 916 | // } else { 917 | // console.logError("Unable to archive results due to an invalid reference to Jenkins build workspace"); 918 | // throw new AbortException("Unable to archive results due to an invalid reference to Jenkins build workspace"); 919 | // } 920 | 921 | // add the link in jenkins UI for anchore results 922 | console.logDebug("Setting up build results"); 923 | 924 | 925 | if (finalAction != null) { 926 | build.addAction(new AnchoreAction(build, finalAction.toString(), jenkinsOutputDirName, gateOutputFileName, queryOutputMap, 927 | gateSummary.toString(), cveListingFileName, totalStopActionCount, totalWarnActionCount, totalGoActionCount)); 928 | } else { 929 | build.addAction(new AnchoreAction(build, "", jenkinsOutputDirName, gateOutputFileName, queryOutputMap, gateSummary.toString(), 930 | cveListingFileName, totalStopActionCount, totalWarnActionCount, totalGoActionCount)); 931 | } 932 | // } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 933 | // throw e; 934 | } catch (Exception e) { // caught unknown exception, log it and wrap it 935 | console.logError("Failed to setup build results due to an unexpected error", e); 936 | throw new AbortException( 937 | "Failed to setup build results due to an unexpected error. Please refer to above logs for more information"); 938 | } 939 | } 940 | 941 | public void cleanup() { 942 | try { 943 | console.logDebug("Cleaning up build artifacts"); 944 | int rc; 945 | 946 | // Clear Jenkins workspace 947 | if (!Strings.isNullOrEmpty(jenkinsOutputDirName)) { 948 | try { 949 | console.logDebug("Deleting Jenkins workspace " + jenkinsOutputDirName); 950 | cleanJenkinsWorkspaceQuietly(); 951 | // FilePath jenkinsOutputDirFP = new FilePath(build.getWorkspace(), jenkinsOutputDirName); 952 | // jenkinsOutputDirFP.deleteRecursive(); 953 | } catch (IOException | InterruptedException e) { 954 | console.logDebug("Unable to delete Jenkins workspace " + jenkinsOutputDirName, e); 955 | } 956 | } 957 | } catch (RuntimeException e) { // caught unknown exception, log it 958 | console.logDebug("Failed to clean up build artifacts due to an unexpected error", e); 959 | } 960 | } 961 | 962 | /** 963 | * Print versions info and configuration 964 | */ 965 | private void printConfig() { 966 | console.logInfo("Jenkins version: " + Jenkins.VERSION); 967 | List plugins; 968 | if (Jenkins.getActiveInstance() != null && Jenkins.getActiveInstance().getPluginManager() != null 969 | && (plugins = Jenkins.getActiveInstance().getPluginManager().getPlugins()) != null) { 970 | for (PluginWrapper plugin : plugins) { 971 | if (plugin.getShortName() 972 | .equals("anchore-container-scanner")) { // artifact ID of the plugin, TODO is there a better way to get this 973 | console.logInfo(plugin.getDisplayName() + " version: " + plugin.getVersion()); 974 | break; 975 | } 976 | } 977 | } 978 | config.print(console); 979 | } 980 | 981 | /** 982 | * Checks for minimum required config for executing step 983 | */ 984 | private void checkConfig() throws AbortException { 985 | if (Strings.isNullOrEmpty(config.getName())) { 986 | console.logError("Image list file not found"); 987 | throw new AbortException( 988 | "Image list file not specified. Please provide a valid image list file name in the Anchore Container Image Scanner step " 989 | + "and try again"); 990 | } 991 | 992 | try { 993 | if (!new FilePath(workspace, config.getName()).exists()) { 994 | console.logError("Cannot find image list file \"" + config.getName() + "\" under " + workspace); 995 | throw new AbortException("Cannot find image list file \'" + config.getName() 996 | + "\'. Please ensure that image list file is created prior to Anchore Container Image Scanner step"); 997 | } 998 | } catch (AbortException e) { 999 | throw e; 1000 | } catch (Exception e) { 1001 | console.logWarn("Unable to access image list file \"" + config.getName() + "\" under " + workspace, e); 1002 | throw new AbortException("Unable to access image list file " + config.getName() 1003 | + ". Please ensure that image list file is created prior to Anchore Container Image Scanner step"); 1004 | } 1005 | } 1006 | 1007 | private void initializeJenkinsWorkspace() throws AbortException { 1008 | try { 1009 | console.logDebug("Initializing Jenkins workspace"); 1010 | 1011 | if (Strings.isNullOrEmpty(buildId = build.getParent().getDisplayName() + "_" + build.getNumber())) { 1012 | console.logWarn("Unable to generate a unique identifier for this build due to invalid configuration"); 1013 | throw new AbortException("Unable to generate a unique identifier for this build due to invalid configuration"); 1014 | } 1015 | 1016 | // ArtifactArchiver.perform() cannot parse file paths with commas, which buildId will have in some cases, for 1017 | // example if this is a matrix job. So replace any commas in it with underscores to separate the matrix values. 1018 | jenkinsOutputDirName = JENKINS_DIR_NAME; 1019 | FilePath jenkinsReportDir = new FilePath(workspace, jenkinsOutputDirName); 1020 | 1021 | // Create output directories 1022 | if (!jenkinsReportDir.exists()) { 1023 | console.logDebug("Creating workspace directory " + jenkinsOutputDirName); 1024 | jenkinsReportDir.mkdirs(); 1025 | } 1026 | 1027 | queryOutputMap = new LinkedHashMap<>(); // maintain the ordering of queries 1028 | gateOutputFileName = GATES_OUTPUT_PREFIX + JSON_FILE_EXTENSION; 1029 | 1030 | } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 1031 | throw e; 1032 | } catch (Exception e) { // caught unknown exception, log it and wrap it 1033 | console.logWarn("Failed to initialize Jenkins workspace", e); 1034 | throw new AbortException("Failed to initialize Jenkins workspace due to to an unexpected error"); 1035 | } 1036 | } 1037 | 1038 | private void initializeAnchoreWorkspace() throws AbortException { 1039 | initializeAnchoreWorkspaceEngine(); 1040 | } 1041 | 1042 | private void initializeAnchoreWorkspaceEngine() throws AbortException { 1043 | try { 1044 | console.logDebug("Initializing Anchore workspace (enginemode)"); 1045 | 1046 | // get the input and store it in tag/dockerfile map 1047 | FilePath inputImageFP = new FilePath(workspace, config.getName()); // Already checked in checkConfig() 1048 | try (BufferedReader br = new BufferedReader(new InputStreamReader(inputImageFP.read(), StandardCharsets.UTF_8))) { 1049 | String line; 1050 | int count = 0; 1051 | while ((line = br.readLine()) != null) { 1052 | String imgId = null; 1053 | String jenkinsDFile = null; 1054 | String dfilecontents = null; 1055 | Iterable iterable = Util.IMAGE_LIST_SPLITTER.split(line); 1056 | Iterator partIterator; 1057 | 1058 | if (null != iterable && null != (partIterator = iterable.iterator()) && partIterator.hasNext()) { 1059 | imgId = partIterator.next(); 1060 | 1061 | if (partIterator.hasNext()) { 1062 | jenkinsDFile = partIterator.next(); 1063 | 1064 | StringBuilder b = new StringBuilder(); 1065 | FilePath myfp = new FilePath(workspace, jenkinsDFile); 1066 | try (BufferedReader mybr = new BufferedReader(new InputStreamReader(myfp.read(), StandardCharsets.UTF_8))) { 1067 | String myline; 1068 | while ((myline = mybr.readLine()) != null) { 1069 | b.append(myline + '\n'); 1070 | } 1071 | } 1072 | console.logDebug("Dockerfile contents: " + b.toString()); 1073 | byte[] encodedBytes = Base64.encodeBase64(b.toString().getBytes(StandardCharsets.UTF_8)); 1074 | dfilecontents = new String(encodedBytes, StandardCharsets.UTF_8); 1075 | 1076 | } 1077 | } 1078 | if (null != imgId) { 1079 | console.logDebug("Image tag/digest: " + imgId); 1080 | console.logDebug("Base64 encoded Dockerfile contents: " + dfilecontents); 1081 | input_image_dfile.put(imgId, dfilecontents); 1082 | } 1083 | } 1084 | } 1085 | } catch (AbortException e) { // probably caught one of the thrown exceptions, let it pass through 1086 | throw e; 1087 | } catch (Exception e) { // caught unknown exception, console.log it and wrap it 1088 | console.logError("Failed to initialize Anchore workspace due to an unexpected error", e); 1089 | throw new AbortException( 1090 | "Failed to initialize Anchore workspace due to an unexpected error. Please refer to above logs for more information"); 1091 | } 1092 | } 1093 | 1094 | private JSONArray generateDataTablesColumnsForGateSummary() { 1095 | JSONArray headers = new JSONArray(); 1096 | for (GATE_SUMMARY_COLUMN column : GATE_SUMMARY_COLUMN.values()) { 1097 | JSONObject header = new JSONObject(); 1098 | header.put("data", column.toString()); 1099 | header.put("title", column.toString().replaceAll("_", " ")); 1100 | headers.add(header); 1101 | } 1102 | return headers; 1103 | } 1104 | 1105 | private void cleanJenkinsWorkspaceQuietly() throws IOException, InterruptedException { 1106 | FilePath jenkinsOutputDirFP = new FilePath(workspace, jenkinsOutputDirName); 1107 | jenkinsOutputDirFP.deleteRecursive(); 1108 | } 1109 | } 1110 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/ConsoleLog.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import hudson.AbortException; 4 | import java.io.PrintStream; 5 | import java.util.Date; 6 | import java.util.logging.Logger; 7 | 8 | /** 9 | * Logging mechanism for outputting messages to Jenkins build console 10 | */ 11 | public class ConsoleLog { 12 | 13 | private static final Logger LOG = Logger.getLogger(ConsoleLog.class.getName()); 14 | private static final String LOG_FORMAT = "%1$tY-%1$tm-%1$tdT%1$tH:%1$tM:%1$tS.%1$tL %2$-6s %3$-15s %4$s"; 15 | 16 | private String name; 17 | private PrintStream logger; 18 | private boolean enableDebug; 19 | 20 | public PrintStream getLogger() { 21 | return logger; 22 | } 23 | 24 | public boolean isEnableDebug() { 25 | return enableDebug; 26 | } 27 | 28 | public ConsoleLog(String name, PrintStream logger, boolean enableDebug) throws AbortException { 29 | if (null != logger) { 30 | this.name = name; 31 | this.logger = logger; 32 | this.enableDebug = enableDebug; 33 | } else { 34 | LOG.warning("Cannot instantiate console logger"); 35 | throw new AbortException("Cannot instantiate console logger"); 36 | } 37 | } 38 | 39 | public void logDebug(String msg) { 40 | if (enableDebug) { 41 | logger.println(String.format(LOG_FORMAT, new Date(), "DEBUG", name, msg)); 42 | } 43 | } 44 | 45 | public void logDebug(String msg, Throwable t) { 46 | logDebug(msg); 47 | if (null != t) { 48 | t.printStackTrace(logger); 49 | } 50 | } 51 | 52 | public void logInfo(String msg) { 53 | logger.println(String.format(LOG_FORMAT, new Date(), "INFO", name, msg)); 54 | } 55 | 56 | public void logWarn(String msg) { 57 | logger.println(String.format(LOG_FORMAT, new Date(), "WARN", name, msg)); 58 | } 59 | 60 | public void logWarn(String msg, Throwable t) { 61 | logWarn(msg); 62 | if (null != t) { 63 | t.printStackTrace(logger); 64 | } 65 | } 66 | 67 | public void logError(String msg) { 68 | logger.println(String.format(LOG_FORMAT, new Date(), "ERROR", name, msg)); 69 | } 70 | 71 | public void logError(String msg, Throwable t) { 72 | logError(msg); 73 | if (null != t) { 74 | t.printStackTrace(logger); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/anchore/jenkins/plugins/anchore/Util.java: -------------------------------------------------------------------------------- 1 | package com.anchore.jenkins.plugins.anchore; 2 | 3 | import com.google.common.base.Splitter; 4 | import java.util.regex.Pattern; 5 | 6 | public class Util { 7 | 8 | // This is probably the slowest way of formatting strings, should do for now but please figure out a better way 9 | public static final Splitter IMAGE_LIST_SPLITTER = Splitter.on(Pattern.compile("\\s+")).trimResults().omitEmptyStrings(); 10 | 11 | public enum GATE_ACTION {STOP, WARN, GO, PASS, FAIL} 12 | 13 | public enum LOG_LEVEL {DEBUG, WARN, INFO, ERROR} 14 | 15 | public enum GATE_SUMMARY_COLUMN {Repo_Tag, Stop_Actions, Warn_Actions, Go_Actions, Final_Action, Stop_Action_Details} 16 | 17 | public enum API_VERSION {v1, v2} 18 | 19 | public static final API_VERSION GET_API_VERSION_FROM_URL(String engineUrl) { 20 | if (engineUrl.endsWith("v2") || engineUrl.endsWith("v2/")){ 21 | return API_VERSION.v2; 22 | } 23 | return API_VERSION.v1; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/com/anchore/jenkins/plugins/anchore/AnchoreAction/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 61 | 62 | 63 |
64 |
65 | 66 |

Anchore Policy Evaluation Summary

67 | 68 | 69 | 72 |
73 | 74 |

Anchore Policy Evaluation Report

75 | 76 | 77 | 78 | 81 |
82 | 83 | 84 | 85 |
86 |

Anchore Image Query Report (${e.key})

87 | 88 | 89 | 92 |
93 |
94 | 95 | 96 |
97 |

Common Vulnerabilities and Exposures (CVE) List

98 | 99 | 100 | 103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 |

Anchore Policy Evaluation Report

111 |