├── .dockerignore ├── .github └── CODE_OF_CONDUCT.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.autobuild ├── ISSUE_TEMPLATE.md ├── LICENSE ├── Makefile ├── NOTICE ├── NOTICE2 ├── README.md ├── api ├── api.go ├── router.go └── v1 │ ├── models.go │ ├── router.go │ └── routes.go ├── cmd ├── cmd.go ├── config.go └── serve.go ├── config.api.example.yml ├── docs ├── README.md ├── postman │ ├── README.md │ ├── collections │ │ └── Port_Authority Examples.postman_collection.json │ └── environments │ │ └── Port_Authority - minikube.postman_environment.json └── webhook-example │ ├── README.md │ ├── admission-controller.example.json │ ├── image-review.example.yml │ ├── imagepolicywebhookflow.png │ └── imagepolicywebhookflow.xml ├── glide.lock ├── glide.yaml ├── imgs ├── ahab-small.png └── ahab.png ├── main.go ├── minikube ├── clair │ ├── clair │ │ ├── config.yml │ │ ├── deployment.yml │ │ └── service.yml │ └── postgres │ │ ├── deployment.yaml │ │ └── service.yml └── portauthority │ ├── portauthority-local │ ├── config.yml │ ├── deployment.yml │ └── service.yml │ ├── portauthority │ ├── config.yml │ ├── deployment.yml │ └── service.yml │ └── postgres │ ├── deployment.yml │ └── service.yml └── pkg ├── clair ├── clair.go └── client │ ├── client.go │ ├── error.go │ ├── fixes.go │ ├── layers.go │ ├── namespaces.go │ ├── notifications.go │ ├── request.go │ └── vulnerabilities.go ├── commonerr └── errors.go ├── crawler ├── k8s.go └── registry.go ├── datastore ├── datastore.go ├── model.go └── pgsql │ ├── container.go │ ├── crawler.go │ ├── image.go │ ├── pgsql.go │ └── policy.go ├── docker ├── auth.go ├── docker.go └── registry │ ├── authchallenge.go │ ├── basictransport.go │ ├── errortransport.go │ ├── json.go │ ├── manifest.go │ ├── registry.go │ ├── repositories.go │ ├── tags.go │ └── tokentransport.go ├── formatter └── formatter.go └── stopper └── stopper.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # files not to be included in docker build 2 | 3 | config*.yaml 4 | config*.yml 5 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at 59 | [TTS-OpenSource-Office@target.com](mailto:TTS-OpenSource-Office@target.com). All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | !.gitignore 3 | vendor/* 4 | bin/* 5 | portauthority 6 | !minikube/portauthority/* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9.2 4 | 5 | sudo: required 6 | 7 | script: 8 | - make deps 9 | - make build-linux 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to portauthority 2 | 3 | ### Issues 4 | [Issues](issues/new/) are always welcome! 5 | 6 | ### Pull Requests 7 | 8 | These rules must be followed for any contributions to be merged into master. A Git installation is required. 9 | 10 | 1. Fork this repo 11 | 1. Go get the original code: 12 | 13 | `go get github.com/target/portauthority` 14 | 15 | 1. Navigate to the original code: 16 | 17 | `$GOPATH/src/github.com/target/portauthority` 18 | 19 | 1. Add a remote branch pointing to your fork: 20 | 21 | `git remote add fork https://github.com/your_fork/portauthority` 22 | 23 | 1. Implement desired changes 24 | 1. Validate the changes meet your desired use case 25 | 1. Update documentation 26 | 1. Push to your fork: 27 | 28 | `git push fork master` 29 | 30 | 1. Open a pull request. Thank you for your contribution! A dialog will ensue. 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | #Make sure we are patching all packages 4 | RUN apk update \ 5 | && apk upgrade \ 6 | && apk add --no-cache \ 7 | ca-certificates \ 8 | && update-ca-certificates 9 | 10 | COPY portauthority /usr/bin/ 11 | ENTRYPOINT ["portauthority"] 12 | -------------------------------------------------------------------------------- /Dockerfile.autobuild: -------------------------------------------------------------------------------- 1 | #Builder stage to create the binary 2 | FROM golang:1.9.2-alpine3.7 as builder 3 | 4 | RUN apk add --update \ 5 | curl git; 6 | 7 | ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 8 | 9 | RUN curl https://glide.sh/get | sh 10 | 11 | ADD . /go/src/github.com/target/portauthority 12 | WORKDIR /go/src/github.com/target/portauthority 13 | 14 | RUN glide install -v 15 | 16 | RUN VERSION=$(git for-each-ref refs/tags --sort=-taggerdate --format='%(refname:short)' --count=1) && echo version=$VERSION; \ 17 | if [ "$VERSION" == "" ] || [ -z "$VERSION" ]; \ 18 | then echo "DEV BUILD" && go build -ldflags "-X main.appVersion=dev" -o portauthority; \ 19 | else echo "TAG BUILD" && go build -ldflags "-X main.appVersion=$VERSION" -o portauthority; \ 20 | fi 21 | 22 | 23 | #Final image stage 24 | FROM alpine:3.8 25 | 26 | #Make sure we are patching all packages 27 | RUN apk update \ 28 | && apk upgrade \ 29 | && apk add --no-cache \ 30 | ca-certificates \ 31 | && update-ca-certificates 32 | 33 | COPY --from=builder /go/src/github.com/target/portauthority/portauthority /usr/bin/ 34 | ENTRYPOINT ["portauthority"] 35 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Below are required fields for qualifying your issue to our support team. 2 | 3 | ### Subject of the issue 4 | - [ ] Briefly describe your issue. More details can be added below. 5 | 6 | ### Your environment 7 | - [ ] Port Authority version 8 | - [ ] Golang version 9 | - [ ] Operating System 10 | - [ ] Clair, Kubernetes, Minikube, PostgreSQL versions (if relevant) 11 | 12 | ### Expected behaviour 13 | - [ ] Tell us what should happen 14 | 15 | ### Actual behaviour 16 | - [ ] Tell us what happens instead, including any relevant errors or stacktraces 17 | - [ ] Verify you have redacted any sensitive hostnames, credentials, application names, etc from these errors/stacktraces 18 | 19 | ### Steps to Reproduce 20 | - [ ] Steps go here 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Copyright (C) 2018 Target Brands, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## Go parameters ## 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | GOGET=$(GOCMD) get 7 | BINARY_NAME=portauthority 8 | BINARY_MAC=$(BINARY_NAME)_mac 9 | 10 | ## Actions ## 11 | all: clean deps build-mac build-linux 12 | 13 | clean: 14 | $(GOCLEAN) 15 | rm -f $(BINARY_NAME) 16 | rm -f $(BINARY_MAC) 17 | run-mac: clean build-mac 18 | ./$(BINARY_MAC) 19 | deps: | glide 20 | @echo "Installing dependencies" 21 | @glide install -v 22 | 23 | ## File Targets ## 24 | deploy-minikube: clean-minikube 25 | @echo "Deploying officially built Port Authority" 26 | @echo "Applying Clair postgres deployment files" 27 | kubectl apply -f ./minikube/clair/postgres 28 | kubectl rollout status deployment/clair-postgres-deployment 29 | sleep 5 30 | @echo "Applying Clair deployment files" 31 | kubectl apply -f ./minikube/clair/clair 32 | kubectl rollout status deployment/clair-deployment 33 | @echo "Applying portauthority postgres deployment files" 34 | kubectl apply -f ./minikube/portauthority/postgres 35 | kubectl rollout status deployment/portauthority-postgres-deployment 36 | sleep 5 37 | @echo "Applying portauthority deployment files" 38 | kubectl apply -f ./minikube/portauthority/portauthority 39 | kubectl rollout status deployment/portauthority-deployment 40 | 41 | ## File Targets ## 42 | deploy-minikube-dev: clean build-linux docker-build clean-minikube 43 | @echo "Deploying locally built devloper build of Port Authority" 44 | @echo "Applying Clair postgres deployment files" 45 | kubectl apply -f ./minikube/clair/postgres 46 | kubectl rollout status deployment/clair-postgres-deployment 47 | sleep 5 48 | @echo "Applying Clair deployment files" 49 | kubectl apply -f ./minikube/clair/clair 50 | kubectl rollout status deployment/clair-deployment 51 | @echo "Applying portauthority postgres deployment files" 52 | kubectl apply -f ./minikube/portauthority/postgres 53 | kubectl rollout status deployment/portauthority-postgres-deployment 54 | sleep 5 55 | @echo "Applying portauthority deployment files" 56 | kubectl apply -f ./minikube/portauthority/portauthority-local 57 | kubectl rollout status deployment/portauthority-deployment 58 | 59 | clean-minikube: 60 | @echo "Cleaning up previous portauthority deployments (postgres will remain)" 61 | kubectl delete service portauthority-postgres-service --ignore-not-found 62 | kubectl delete -f ./minikube/portauthority/portauthority --ignore-not-found 63 | kubectl delete -f ./minikube/portauthority/portauthority-local --ignore-not-found 64 | 65 | clean-minikube-postgres: 66 | @echo "Cleaning Clair postgres database" 67 | kubectl delete -f ./minikube/clair/postgres --ignore-not-found 68 | @echo "Cleaning portauthority postgres database" 69 | kubectl delete -f ./minikube/portauthority/postgres --ignore-not-found 70 | 71 | ## Builds ## 72 | build-mac: 73 | $(GOBUILD) -o $(BINARY_MAC) 74 | build-linux: 75 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -ldflags "-X main.appVersion=local-dev" -o $(BINARY_NAME) 76 | docker-build: 77 | docker build -t $(BINARY_NAME) . 78 | 79 | ## Glide ## 80 | glide: 81 | @if ! hash glide 2>/dev/null; then curl https://glide.sh/get | sh; fi 82 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | CoreOS Project 2 | Copyright 2015 CoreOS, Inc 3 | 4 | This product includes software developed at CoreOS, Inc. 5 | (http://www.coreos.com/). 6 | 7 | Copyright (c) 2018 Target Brands, Inc. 8 | -------------------------------------------------------------------------------- /NOTICE2: -------------------------------------------------------------------------------- 1 | This file contains the open source licenses for the following open source components in coreos/clair: 2 | 3 | 1. coreos/clair (commit 3ec262dd51466100b44910b60308e46b00f2a097) 4 | ------------------------------------------------------------------------------------------------------- 5 | 6 | 1. coreos/clair (commit 3ec262dd51466100b44910b60308e46b00f2a097) 7 | https://github.com/coreos/clair 8 | 9 | Apache License 10 | Version 2.0, January 2004 11 | http://www.apache.org/licenses/ 12 | 13 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 14 | 15 | 1. Definitions. 16 | 17 | "License" shall mean the terms and conditions for use, reproduction, 18 | and distribution as defined by Sections 1 through 9 of this document. 19 | 20 | "Licensor" shall mean the copyright owner or entity authorized by 21 | the copyright owner that is granting the License. 22 | 23 | "Legal Entity" shall mean the union of the acting entity and all 24 | other entities that control, are controlled by, or are under common 25 | control with that entity. For the purposes of this definition, 26 | "control" means (i) the power, direct or indirect, to cause the 27 | direction or management of such entity, whether by contract or 28 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 29 | outstanding shares, or (iii) beneficial ownership of such entity. 30 | 31 | "You" (or "Your") shall mean an individual or Legal Entity 32 | exercising permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, 35 | including but not limited to software source code, documentation 36 | source, and configuration files. 37 | 38 | "Object" form shall mean any form resulting from mechanical 39 | transformation or translation of a Source form, including but 40 | not limited to compiled object code, generated documentation, 41 | and conversions to other media types. 42 | 43 | "Work" shall mean the work of authorship, whether in Source or 44 | Object form, made available under the License, as indicated by a 45 | copyright notice that is included in or attached to the work 46 | (an example is provided in the Appendix below). 47 | 48 | "Derivative Works" shall mean any work, whether in Source or Object 49 | form, that is based on (or derived from) the Work and for which the 50 | editorial revisions, annotations, elaborations, or other modifications 51 | represent, as a whole, an original work of authorship. For the purposes 52 | of this License, Derivative Works shall not include works that remain 53 | separable from, or merely link (or bind by name) to the interfaces of, 54 | the Work and Derivative Works thereof. 55 | 56 | "Contribution" shall mean any work of authorship, including 57 | the original version of the Work and any modifications or additions 58 | to that Work or Derivative Works thereof, that is intentionally 59 | submitted to Licensor for inclusion in the Work by the copyright owner 60 | or by an individual or Legal Entity authorized to submit on behalf of 61 | the copyright owner. For the purposes of this definition, "submitted" 62 | means any form of electronic, verbal, or written communication sent 63 | to the Licensor or its representatives, including but not limited to 64 | communication on electronic mailing lists, source code control systems, 65 | and issue tracking systems that are managed by, or on behalf of, the 66 | Licensor for the purpose of discussing and improving the Work, but 67 | excluding communication that is conspicuously marked or otherwise 68 | designated in writing by the copyright owner as "Not a Contribution." 69 | 70 | "Contributor" shall mean Licensor and any individual or Legal Entity 71 | on behalf of whom a Contribution has been received by Licensor and 72 | subsequently incorporated within the Work. 73 | 74 | 2. Grant of Copyright 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 | copyright license to reproduce, prepare Derivative Works of, 78 | publicly display, publicly perform, sublicense, and distribute the 79 | Work and such Derivative Works in Source or Object form. 80 | 81 | 3. Grant of Patent License. Subject to the terms and conditions of 82 | this License, each Contributor hereby grants to You a perpetual, 83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 84 | (except as stated in this section) patent license to make, have made, 85 | use, offer to sell, sell, import, and otherwise transfer the Work, 86 | where such license applies only to those patent claims licensable 87 | by such Contributor that are necessarily infringed by their 88 | Contribution(s) alone or by combination of their Contribution(s) 89 | with the Work to which such Contribution(s) was submitted. If You 90 | institute patent litigation against any entity (including a 91 | cross-claim or counterclaim in a lawsuit) alleging that the Work 92 | or a Contribution incorporated within the Work constitutes direct 93 | or contributory patent infringement, then any patent licenses 94 | granted to You under this License for that Work shall terminate 95 | as of the date such litigation is filed. 96 | 97 | 4. Redistribution. You may reproduce and distribute copies of the 98 | Work or Derivative Works thereof in any medium, with or without 99 | modifications, and in Source or Object form, provided that You 100 | meet the following conditions: 101 | 102 | (a) You must give any other recipients of the Work or 103 | Derivative Works a copy of this License; and 104 | 105 | (b) You must cause any modified files to carry prominent notices 106 | stating that You changed the files; and 107 | 108 | (c) You must retain, in the Source form of any Derivative Works 109 | that You distribute, all copyright, patent, trademark, and 110 | attribution notices from the Source form of the Work, 111 | excluding those notices that do not pertain to any part of 112 | the Derivative Works; and 113 | 114 | (d) If the Work includes a "NOTICE" text file as part of its 115 | distribution, then any Derivative Works that You distribute must 116 | include a readable copy of the attribution notices contained 117 | within such NOTICE file, excluding those notices that do not 118 | pertain to any part of the Derivative Works, in at least one 119 | of the following places: within a NOTICE text file distributed 120 | as part of the Derivative Works; within the Source form or 121 | documentation, if provided along with the Derivative Works; or, 122 | within a display generated by the Derivative Works, if and 123 | wherever such third-party notices normally appear. The contents 124 | of the NOTICE file are for informational purposes only and 125 | do not modify the License. You may add Your own attribution 126 | notices within Derivative Works that You distribute, alongside 127 | or as an addendum to the NOTICE text from the Work, provided 128 | that such additional attribution notices cannot be construed 129 | as modifying the License. 130 | 131 | You may add Your own copyright statement to Your modifications and 132 | may provide additional or different license terms and conditions 133 | for use, reproduction, or distribution of Your modifications, or 134 | for any such Derivative Works as a whole, provided Your use, 135 | reproduction, and distribution of the Work otherwise complies with 136 | the conditions stated in this License. 137 | 138 | 5. Submission of Contributions. Unless You explicitly state otherwise, 139 | any Contribution intentionally submitted for inclusion in the Work 140 | by You to the Licensor shall be under the terms and conditions of 141 | this License, without any additional terms or conditions. 142 | Notwithstanding the above, nothing herein shall supersede or modify 143 | the terms of any separate license agreement you may have executed 144 | with Licensor regarding such Contributions. 145 | 146 | 6. Trademarks. This License does not grant permission to use the trade 147 | names, trademarks, service marks, or product names of the Licensor, 148 | except as required for reasonable and customary use in describing the 149 | origin of the Work and reproducing the content of the NOTICE file. 150 | 151 | 7. Disclaimer of Warranty. Unless required by applicable law or 152 | agreed to in writing, Licensor provides the Work (and each 153 | Contributor provides its Contributions) on an "AS IS" BASIS, 154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 155 | implied, including, without limitation, any warranties or conditions 156 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 157 | PARTICULAR PURPOSE. You are solely responsible for determining the 158 | appropriateness of using or redistributing the Work and assume any 159 | risks associated with Your exercise of permissions under this License. 160 | 161 | 8. Limitation of Liability. In no event and under no legal theory, 162 | whether in tort (including negligence), contract, or otherwise, 163 | unless required by applicable law (such as deliberate and grossly 164 | negligent acts) or agreed to in writing, shall any Contributor be 165 | liable to You for damages, including any direct, indirect, special, 166 | incidental, or consequential damages of any character arising as a 167 | result of this License or out of the use or inability to use the 168 | Work (including but not limited to damages for loss of goodwill, 169 | work stoppage, computer failure or malfunction, or any and all 170 | other commercial damages or losses), even if such Contributor 171 | has been advised of the possibility of such damages. 172 | 173 | 9. Accepting Warranty or Additional Liability. While redistributing 174 | the Work or Derivative Works thereof, You may choose to offer, 175 | and charge a fee for, acceptance of support, warranty, indemnity, 176 | or other liability obligations and/or rights consistent with this 177 | License. However, in accepting such obligations, You may act only 178 | on Your own behalf and on Your sole responsibility, not on behalf 179 | of any other Contributor, and only if You agree to indemnify, 180 | defend, and hold each Contributor harmless for any liability 181 | incurred by, or claims asserted against, such Contributor by reason 182 | of your accepting any such warranty or additional liability. 183 | 184 | END OF TERMS AND CONDITIONS 185 | 186 | APPENDIX: How to apply the Apache License to your work. 187 | 188 | To apply the Apache License to your work, attach the following 189 | boilerplate notice, with the fields enclosed by brackets "{}" 190 | replaced with your own identifying information. (Don't include 191 | the brackets!) The text should be enclosed in the appropriate 192 | comment syntax for the file format. We also recommend that a 193 | file or class name and description of purpose be included on the 194 | same "printed page" as the copyright notice for easier 195 | identification within third-party archives. 196 | 197 | Copyright {yyyy} {name of copyright owner} 198 | 199 | Licensed under the Apache License, Version 2.0 (the "License"); 200 | you may not use this file except in compliance with the License. 201 | You may obtain a copy of the License at 202 | 203 | http://www.apache.org/licenses/LICENSE-2.0 204 | 205 | Unless required by applicable law or agreed to in writing, software 206 | distributed under the License is distributed on an "AS IS" BASIS, 207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 208 | See the License for the specific language governing permissions and 209 | limitations under the License. 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/target/portauthority.svg?branch=master)](https://travis-ci.org/target/portauthority/builds) 2 | 3 | 4 | ## Introduction 5 | 6 | Port Authority is an API service that delivers component based vulnerability assessments for Docker images at time of build and in run-time environments. 7 | 8 | The Port Authority API is capable of orchestrating scans of individual public or private images as well as scanning entire private Docker registries like [Docker Hub](https://hub.docker.com), [Google Container Registry](https://cloud.google.com/container-registry/) or [Artifactory](https://jfrog.com/artifactory/). To accomplish this, Port Authority breaks each Docker image into layers and sends it to the open source static analysis tool [Clair](https://github.com/coreos/clair) in the backend to perform the scans and identify vulnerabilities. Upon completion of this workflow Port Authority maintains a manifest of the images and scan results. 9 | 10 | Port Authority also supplies developers with customizable offerings to assist with the audit and governance of their container workloads. Port Authority provides a webhook that when leveraged by a [Kubernetes](https://github.com/kubernetes/kubernetes) admission controller will allow or deny deployments based off of user-defined policies and image attributes. Port Authority then achieves run-time inspection by integrating with Kubernetes to discover running containers and inventorying those deployed images for scanning. 11 | 12 | ## Getting Started 13 | 14 | ### Setup and Start Minikube 15 | 1. Install [Minikube](https://github.com/kubernetes/minikube) 16 | 2. Start Minikube: 17 | 18 | `minikube start` 19 | 20 | **NOTE:** Supported Kubernetes versions (1.6.x - 1.9.x). Supported Clair versions v2.x.x. 21 | 22 | ### Build and Deploy to Minikube 23 | 1. Use Minikube Docker: 24 | 25 | `eval $(minikube docker-env)` 26 | 27 | 2. Deploy official Port Authority stack: 28 | 29 | `make deploy-minikube` 30 | 31 | (Optional). Local developer build stack: 32 | 33 | 1. Use Minikube Docker: 34 | 35 | `eval $(minikube docker-env)` 36 | 37 | 2. Get all Glide dependancies: 38 | 39 | `make deps` 40 | 41 | 3. Deploy official Port Authority stack: 42 | 43 | `make deploy-minikube-dev` 44 | 45 | ## Optional Configuration 46 | Different configuration adjustments can be made to the Port Authority deployment here: [minikube/portauthority/portauthority/config.yml](minikube/portauthority/portauthority/config.yml) 47 | 48 | :white_check_mark: Add Docker Credentials used by the K8s Crawler scan feature 49 | 50 | ```yml 51 | ### Environment variables defined below are mapped to credentials used by the Kubernetes Crawler API (/v1/crawler/k8s) 52 | ### A 'Scan: true' flag will invoke their usage 53 | k8scrawlcredentials: 54 | # Use "" for basic auth on registries that do not require a username and password 55 | - url: "docker.io" #basic auth is empty UN and PW 56 | username: "DOCKER_USER" 57 | password: "DOCKER_PASS" 58 | - url: "gcr.io" #basic auth is empty UN and PW 59 | username: "GCR_USER" 60 | password: "GCR_PASS" 61 | ``` 62 | 63 | :white_check_mark: Enable the [Kubernetes Admission Controller](docs/webhook-example/README.md) and change webhooks default behavior 64 | ```yml 65 | # Setting imagewebhookdefaultblock to true will set the imagewebhooks endpoint default behavior to block any images with policy violations. 66 | # If it is set to false a user can change enable the behavior by setting the portauthority-webhook deployment annotation to true 67 | imagewebhookdefaultblock: false 68 | ``` 69 | 70 | 71 | ## Docs 72 | 73 | Port Authority is an API service. See our complete [_API Documentation_](docs/README.md) for further configuration, usage, Postman collections and more. 74 | 75 | ## Contributing 76 | 77 | We always welcome new PRs! See [_Contributing_](CONTRIBUTING.md) for further instructions. 78 | 79 | ## Bugs and Feature Requests 80 | 81 | Found something that doesn't seem right or have a feature request? [Please open a new issue](issues/new/). 82 | 83 | ## Copyright and License 84 | 85 | [![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE.txt) 86 | 87 | ©2018 Target Brands, Inc. 88 | 89 | **Credit [Renee French](http://reneefrench.blogspot.com/) for original golang gopher 90 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package api 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "io/ioutil" 23 | "net" 24 | "net/http" 25 | "strconv" 26 | "time" 27 | 28 | log "github.com/sirupsen/logrus" 29 | "github.com/target/portauthority/pkg/clair/client" 30 | "github.com/target/portauthority/pkg/datastore" 31 | "github.com/target/portauthority/pkg/stopper" 32 | "github.com/tylerb/graceful" 33 | ) 34 | 35 | const timeoutResponse = `{"Error":{"Message":"Port Authority failed to respond within the configured timeout window.","Type":"Timeout"}}` 36 | 37 | // Config is the configuration for the API service 38 | type Config struct { 39 | Port int 40 | HealthPort int 41 | Timeout time.Duration 42 | ClairURL string 43 | ClairTimeout int 44 | CertFile, KeyFile, CAFile string 45 | ImageWebhookDefaultBlock bool 46 | RegAuth []map[string]string `yaml:"k8scrawlcredentials"` 47 | } 48 | 49 | // Run starts main API 50 | func Run(cfg *Config, cc clairclient.Client, backend datastore.Backend, st *stopper.Stopper) { 51 | defer st.End() 52 | 53 | // Do not run the API service if there is no config 54 | if cfg == nil { 55 | log.Info("main API service is disabled.") 56 | return 57 | } 58 | log.WithField("port", cfg.Port).Info("starting main API") 59 | 60 | tlsConfig, err := tlsClientConfig(cfg.CAFile) 61 | if err != nil { 62 | log.WithError(err).Fatal("could not initialize client cert authentication") 63 | } 64 | 65 | srv := &graceful.Server{ 66 | Timeout: 0, // Already handled by our TimeOut middleware 67 | NoSignalHandling: true, // We want to use our own Stopper 68 | Server: &http.Server{ 69 | Addr: ":" + strconv.Itoa(cfg.Port), 70 | TLSConfig: tlsConfig, 71 | Handler: http.TimeoutHandler(newAPIHandler(cfg, cc, backend), cfg.Timeout, timeoutResponse), 72 | }, 73 | } 74 | 75 | listenAndServeWithStopper(srv, st, cfg.CertFile, cfg.KeyFile) 76 | 77 | log.Info("main API stopped") 78 | } 79 | 80 | // RunHealth starts the health API 81 | func RunHealth(cfg *Config, backend datastore.Backend, st *stopper.Stopper) { 82 | defer st.End() 83 | 84 | // Do not run the API service if there is no config 85 | if cfg == nil { 86 | log.Info("health API service is disabled.") 87 | return 88 | } 89 | log.WithField("port", cfg.HealthPort).Info("starting health API") 90 | 91 | srv := &graceful.Server{ 92 | Timeout: 10 * time.Second, // Interrupt health checks when stopping 93 | NoSignalHandling: true, // We want to use our own Stopper 94 | Server: &http.Server{ 95 | Addr: ":" + strconv.Itoa(cfg.HealthPort), 96 | Handler: http.TimeoutHandler(newHealthHandler(backend), cfg.Timeout, timeoutResponse), 97 | }, 98 | } 99 | 100 | listenAndServeWithStopper(srv, st, "", "") 101 | 102 | log.Info("health API stopped") 103 | } 104 | 105 | // listenAndServeWithStopper wraps graceful.Server 106 | // ListenAndServe/ListenAndServeTLS and adds the ability to interrupt them with 107 | // the provided stopper.Stopper. 108 | func listenAndServeWithStopper(srv *graceful.Server, st *stopper.Stopper, certFile, keyFile string) { 109 | go func() { 110 | <-st.Chan() 111 | srv.Stop(0) 112 | }() 113 | 114 | var err error 115 | if certFile != "" && keyFile != "" { 116 | log.Info("API: TLS Enabled") 117 | err = srv.ListenAndServeTLS(certFile, keyFile) 118 | } else { 119 | err = srv.ListenAndServe() 120 | } 121 | 122 | if err != nil { 123 | if opErr, ok := err.(*net.OpError); !ok || (ok && opErr.Op != "accept") { 124 | log.Fatal(err) 125 | } 126 | } 127 | } 128 | 129 | // tlsClientConfig initializes a *tls.Config using the given CA. The resulting 130 | // *tls.Config is meant to be used to configure an HTTP server to do client 131 | // certificate authentication. 132 | // 133 | // If no CA is given, a nil *tls.Config is returned; no client certificate will 134 | // be required and verified. In other words, authentication will be disabled. 135 | func tlsClientConfig(caPath string) (*tls.Config, error) { 136 | if caPath == "" { 137 | return nil, nil 138 | } 139 | 140 | caCert, err := ioutil.ReadFile(caPath) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | caCertPool := x509.NewCertPool() 146 | caCertPool.AppendCertsFromPEM(caCert) 147 | 148 | tlsConfig := &tls.Config{ 149 | ClientCAs: caCertPool, 150 | ClientAuth: tls.RequireAndVerifyClientCert, 151 | } 152 | 153 | return tlsConfig, nil 154 | } 155 | -------------------------------------------------------------------------------- /api/router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package api 18 | 19 | import ( 20 | "net/http" 21 | "strings" 22 | 23 | log "github.com/sirupsen/logrus" 24 | "github.com/julienschmidt/httprouter" 25 | 26 | "github.com/target/portauthority/api/v1" 27 | "github.com/target/portauthority/pkg/clair/client" 28 | "github.com/target/portauthority/pkg/datastore" 29 | ) 30 | 31 | // router is an HTTP router that forwards requests to the appropriate sub-router 32 | // depending on the API version specified in the request URI. 33 | type router map[string]*httprouter.Router 34 | 35 | // Max API versions 36 | const apiVersionLength = len("v99") 37 | 38 | func newAPIHandler(cfg *Config, cc clairclient.Client, store datastore.Backend) http.Handler { 39 | router := make(router) 40 | router["/v1"] = v1.NewRouter(store, cc, cfg.ImageWebhookDefaultBlock, cfg.RegAuth) 41 | return router 42 | } 43 | 44 | func (rtr router) ServeHTTP(w http.ResponseWriter, r *http.Request) { 45 | urlStr := r.URL.String() 46 | var version string 47 | if len(urlStr) >= apiVersionLength { 48 | version = urlStr[:apiVersionLength] 49 | } 50 | 51 | if router, _ := rtr[version]; router != nil { 52 | // Remove the version number from the request path to let the router do its 53 | // job but do not update the RequestURI 54 | r.URL.Path = strings.Replace(r.URL.Path, version, "", 1) 55 | router.ServeHTTP(w, r) 56 | return 57 | } 58 | 59 | log.WithFields(log.Fields{"status": http.StatusNotFound, "method": r.Method, "request uri": r.RequestURI, "remote addr": r.RemoteAddr}).Info("Served HTTP request") 60 | http.NotFound(w, r) 61 | } 62 | 63 | func newHealthHandler(store datastore.Backend) http.Handler { 64 | router := httprouter.New() 65 | router.GET("/health", healthHandler(store)) 66 | return router 67 | } 68 | 69 | func healthHandler(store datastore.Backend) httprouter.Handle { 70 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 71 | header := w.Header() 72 | header.Set("Server", "portauthority") 73 | 74 | status := http.StatusInternalServerError 75 | if store.Ping() { 76 | status = http.StatusOK 77 | } 78 | 79 | w.WriteHeader(status) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api/v1/models.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package v1 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/target/portauthority/pkg/clair/client" 23 | "github.com/target/portauthority/pkg/datastore" 24 | ) 25 | 26 | // Error struct init 27 | type Error struct { 28 | Message string `json:"Message,omitempty"` 29 | } 30 | 31 | // Image struct init 32 | type Image struct { 33 | ID uint64 `json:"ID,omitempty"` 34 | TopLayer string `json:"TopLayer,omitempty"` 35 | Registry string `json:"Registry,omitempty"` 36 | Repo string `json:"Repo,omitempty"` 37 | Tag string `json:"Tag,omitempty"` 38 | Digest string `json:"Digest,omitempty"` 39 | FirstSeen time.Time `json:"FirstSeen,omitempty"` 40 | LastSeen time.Time `json:"LastSeen,omitempty"` 41 | RegistryUser string `json:"RegistryUser,omitempty"` 42 | RegistryPassword string `json:"RegistryPassword,omitempty"` 43 | Features []clairclient.Feature `json:"Features,omitempty"` 44 | Violations []Violation `json:"Violations,omitempty"` 45 | Metadata datastore.JSONMap `json:"Metadata,omitempty"` 46 | } 47 | 48 | // ImageFromDatabaseModel func init 49 | func ImageFromDatabaseModel(dbImage *datastore.Image) Image { 50 | image := Image{ 51 | ID: dbImage.ID, 52 | Registry: dbImage.Registry, 53 | Repo: dbImage.Repo, 54 | Tag: dbImage.Tag, 55 | Digest: dbImage.Digest, 56 | Metadata: dbImage.Metadata, 57 | FirstSeen: dbImage.FirstSeen, 58 | LastSeen: dbImage.LastSeen, 59 | } 60 | 61 | return image 62 | } 63 | 64 | // ImageEnvelope struct init 65 | type ImageEnvelope struct { 66 | Image *Image `json:"Image,omitempty"` 67 | Error *Error `json:"Error,omitempty"` 68 | } 69 | 70 | // ImagesEnvelope struct init 71 | type ImagesEnvelope struct { 72 | Images *[]*Image `json:"Images,omitempty"` 73 | Error *Error `json:"Error,omitempty"` 74 | } 75 | 76 | // Container struct init 77 | type Container struct { 78 | ID uint64 `json:"ID,omitempty"` 79 | Namespace string `json:"Namespace"` 80 | Cluster string `json:"Cluster"` 81 | Name string `json:"Name"` 82 | Image string `json:"Image"` 83 | ImageScanned bool `json:"ImageScanned,omitempty"` 84 | ImageID string `json:"ImageID"` 85 | ImageRegistry string `json:"ImageRegistry"` 86 | ImageRepo string `json:"ImageRepo"` 87 | ImageTag string `json:"ImageTag"` 88 | ImageDigest string `json:"ImageDigest"` 89 | ImageFeatures []clairclient.Feature `json:"Features,omitempty"` 90 | ImageViolations []Violation `json:"Violations,omitempty"` 91 | Annotations datastore.JSONMap `json:"Annotations,omitempty"` 92 | FirstSeen time.Time `json:"FirstSeen"` 93 | LastSeen time.Time `json:"LastSeen"` 94 | } 95 | 96 | // ContainerFromDatabaseModel func init 97 | func ContainerFromDatabaseModel(dbContainer *datastore.Container) Container { 98 | container := Container{ 99 | ID: dbContainer.ID, 100 | Namespace: dbContainer.Namespace, 101 | Cluster: dbContainer.Cluster, 102 | Name: dbContainer.Name, 103 | Image: dbContainer.Image, 104 | ImageID: dbContainer.ImageID, 105 | ImageRegistry: dbContainer.ImageRegistry, 106 | ImageRepo: dbContainer.ImageRepo, 107 | ImageTag: dbContainer.ImageTag, 108 | ImageDigest: dbContainer.ImageDigest, 109 | Annotations: dbContainer.Annotations, 110 | FirstSeen: dbContainer.FirstSeen, 111 | LastSeen: dbContainer.LastSeen, 112 | } 113 | 114 | return container 115 | } 116 | 117 | // ContainerEnvelope struct init 118 | type ContainerEnvelope struct { 119 | Container *Container `json:"Container,omitempty"` 120 | Error *Error `json:"Error,omitempty"` 121 | } 122 | 123 | // ContainersEnvelope struct init 124 | type ContainersEnvelope struct { 125 | Containers *[]*Container `json:"Containers,omitempty"` 126 | Error *Error `json:"Error,omitempty"` 127 | } 128 | 129 | // Policy struct init 130 | type Policy struct { 131 | ID uint64 `json:"ID,omitempty"` 132 | Name string `json:"Name,omitempty"` 133 | AllowedRiskSeverity string `json:"AllowedRiskSeverity,omitempty"` 134 | AllowedCVENames string `json:"AllowedCVENames,omitempty"` 135 | AllowNotFixed bool `json:"AllowNotFixed"` 136 | NotAllowedCveNames string `json:"NotAllowedCveNames,omitempty"` 137 | NotAllowedOSNames string `json:"NotAllowedOSNames,omitempty"` 138 | Created time.Time `json:"Created,omitempty"` 139 | Updated time.Time `json:"Updated,omitempty"` 140 | } 141 | 142 | // PolicyEnvelope struct init 143 | type PolicyEnvelope struct { 144 | Policy *Policy `json:"Policy,omitempty"` 145 | Error *Error `json:"Error,omitempty"` 146 | } 147 | 148 | // PoliciesEnvelope struct init 149 | type PoliciesEnvelope struct { 150 | Policies *[]*Policy `json:"Policies,omitempty"` 151 | Error *Error `json:"Error,omitempty"` 152 | } 153 | 154 | // PolicyFromDatabaseModel func init 155 | func PolicyFromDatabaseModel(dbPolicy *datastore.Policy) Policy { 156 | policy := Policy{ 157 | ID: dbPolicy.ID, 158 | Name: dbPolicy.Name, 159 | AllowedRiskSeverity: dbPolicy.AllowedRiskSeverity, 160 | AllowedCVENames: dbPolicy.AllowedCVENames, 161 | AllowNotFixed: dbPolicy.AllowNotFixed, 162 | NotAllowedCveNames: dbPolicy.NotAllowedCveNames, 163 | NotAllowedOSNames: dbPolicy.NotAllowedOSNames, 164 | Created: dbPolicy.Created, 165 | Updated: dbPolicy.Updated, 166 | } 167 | return policy 168 | } 169 | 170 | // Violation struct init 171 | type Violation struct { 172 | Type ViolationType 173 | FeatureName string `json:"FeatureName,omitempty"` 174 | FeatureVersion string `json:"FeatureVersion,omitempty"` 175 | Vulnerability clairclient.Vulnerability 176 | } 177 | 178 | // ViolationType string init 179 | type ViolationType string 180 | 181 | const ( 182 | // BlacklistedOsViolation const init 183 | BlacklistedOsViolation ViolationType = "BlacklistedOs" 184 | // BlacklistedCveViolation const init 185 | BlacklistedCveViolation ViolationType = "BlacklistedCve" 186 | // BasicViolation const init 187 | BasicViolation ViolationType = "Basic" 188 | ) 189 | 190 | // K8sImagePolicyEnvelope struct init 191 | type K8sImagePolicyEnvelope struct { 192 | K8sImagePolicy *K8sImagePolicy `json:"K8sImagePolicy,omitempty"` 193 | Error *Error `json:"Error,omitempty"` 194 | } 195 | 196 | // K8sImagePolicy struct init 197 | type K8sImagePolicy struct { 198 | APIVersion string `json:"apiVersion,omitempty"` 199 | Kind string `json:"kind,omitempty"` 200 | Spec *K8sImageSpec `json:"spec,omitempty"` 201 | Status *ImageReviewStatus `json:"status,omitempty"` 202 | } 203 | 204 | // K8sImageSpec struct init 205 | type K8sImageSpec struct { 206 | Containers []K8sContainers `json:"containers,omitempty"` 207 | Annotations map[string]string `json:"annotations,omitempty"` 208 | Namespace string `json:"namespace,omitempty"` 209 | } 210 | 211 | // K8sContainers struct init 212 | type K8sContainers struct { 213 | Image string `json:"image,omitempty"` 214 | } 215 | 216 | // ImageReviewStatus is the result of a port authority policy review 217 | type ImageReviewStatus struct { 218 | // Allowed indicates that all images were allowed to be run 219 | Allowed bool `json:"allowed"` 220 | // Reason should be empty unless Allowed is false in which case it 221 | // may contain a short description of what is wrong. Kubernetes 222 | // may truncate excessively long errors when displaying to the user. 223 | Reason string `json:"reason,omitempty"` 224 | } 225 | 226 | // Crawler struct init 227 | type Crawler struct { 228 | ID uint64 `json:"ID,omitempty"` 229 | Type string `json:"Type,omitempty"` 230 | Status string `json:"Status,omitempty"` 231 | Scan string `json:"Scan,omitempty"` 232 | Messages *datastore.CrawlerMessages `json:"Messages,omitempty"` 233 | Started time.Time `json:"Started,omitempty"` 234 | Finished time.Time `json:"Finished,omitempty"` 235 | } 236 | 237 | // CrawlerMessages struct init 238 | type CrawlerMessages struct { 239 | Summary string `json:"Summary,omitempty"` 240 | Error string `json:"Error,omitempty"` 241 | } 242 | 243 | // CrawlerEnvelope struct init 244 | type CrawlerEnvelope struct { 245 | Crawler *Crawler `json:"Crawler,omitempty"` 246 | Error *Error `json:"Error,omitempty"` 247 | } 248 | 249 | // CrawlerFromDatabaseModel func init 250 | func CrawlerFromDatabaseModel(dbCrawler *datastore.Crawler) Crawler { 251 | crawler := Crawler{ 252 | ID: dbCrawler.ID, 253 | Type: dbCrawler.Type, 254 | Status: dbCrawler.Status, 255 | Messages: dbCrawler.Messages, 256 | Started: dbCrawler.Started, 257 | Finished: dbCrawler.Finished, 258 | } 259 | return crawler 260 | } 261 | 262 | // RegCrawler struct init 263 | type RegCrawler struct { 264 | Crawler Crawler 265 | MaxThreads uint `json:"MaxThreads,omitempty"` 266 | Registry string `json:"Registry,omitempty"` 267 | Username string `json:"Username,omitempty"` 268 | Password string `json:"Password,omitempty"` 269 | Repos []string `json:"Repos,omitempty"` 270 | Tags []string `json:"Tags,omitempty"` 271 | } 272 | 273 | // RegCrawlerEnvelope struct init 274 | type RegCrawlerEnvelope struct { 275 | RegCrawler *RegCrawler `json:"RegCrawler,omitempty"` 276 | Error *Error `json:"Error,omitempty"` 277 | } 278 | 279 | // K8sCrawler struct init 280 | type K8sCrawler struct { 281 | Crawler Crawler 282 | Context string `json:"Context,omitempty"` 283 | KubeConfig string `json:"KubeConfig,omitempty"` 284 | Scan bool `json:"Scan,omitempty"` 285 | MaxThreads uint `json:"MaxThreads,omitempty"` 286 | } 287 | 288 | // K8sCrawlerEnvelope struct init 289 | type K8sCrawlerEnvelope struct { 290 | K8sCrawler *K8sCrawler `json:"K8sCrawler,omitempty"` 291 | Error *Error `json:"Error,omitempty"` 292 | } 293 | -------------------------------------------------------------------------------- /api/v1/router.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package v1 18 | 19 | import ( 20 | "net/http" 21 | "strconv" 22 | "time" 23 | 24 | log "github.com/sirupsen/logrus" 25 | "github.com/julienschmidt/httprouter" 26 | "github.com/prometheus/client_golang/prometheus" 27 | 28 | "github.com/target/portauthority/pkg/clair/client" 29 | "github.com/target/portauthority/pkg/datastore" 30 | ) 31 | 32 | var ( 33 | promResponseDurationMilliseconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 34 | Name: "portauthority_api_response_duration_milliseconds", 35 | Help: "The duration of time it takes to receieve and write a response to an API request", 36 | Buckets: prometheus.ExponentialBuckets(9.375, 2, 10), 37 | }, []string{"route", "code"}) 38 | ) 39 | 40 | func init() { 41 | prometheus.MustRegister(promResponseDurationMilliseconds) 42 | } 43 | 44 | type handler func(http.ResponseWriter, *http.Request, httprouter.Params, *context) (route string, status int) 45 | 46 | func httpHandler(h handler, ctx *context) httprouter.Handle { 47 | return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 48 | start := time.Now() 49 | route, status := h(w, r, p, ctx) 50 | statusStr := strconv.Itoa(status) 51 | if status == 0 { 52 | statusStr = "???" 53 | } 54 | 55 | promResponseDurationMilliseconds. 56 | WithLabelValues(route, statusStr). 57 | Observe(float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond)) 58 | 59 | log.WithFields(log.Fields{"remote addr": r.RemoteAddr, "method": r.Method, "request uri": r.RequestURI, "status": statusStr, "elapsed time": time.Since(start)}).Info("Handled HTTP request") 60 | } 61 | } 62 | 63 | type context struct { 64 | Store datastore.Backend 65 | ClairClient clairclient.Client 66 | ImageWebhookDefaultBlock bool 67 | RegAuth []map[string]string 68 | } 69 | 70 | // NewRouter creates an HTTP router for version 1 of the Port Authority API 71 | func NewRouter(store datastore.Backend, cc clairclient.Client, imageWebhookDefaultBlock bool, regAuth []map[string]string) *httprouter.Router { 72 | router := httprouter.New() 73 | ctx := &context{store, cc, imageWebhookDefaultBlock, regAuth} 74 | 75 | // Images 76 | router.GET("/images", httpHandler(listImages, ctx)) 77 | router.GET("/images/:id", httpHandler(getImage, ctx)) 78 | router.POST("/images", httpHandler(postImage, ctx)) 79 | 80 | // Policies 81 | router.GET("/policies", httpHandler(listPolicy, ctx)) 82 | router.GET("/policies/:name", httpHandler(getPolicy, ctx)) 83 | router.POST("/policies", httpHandler(postPolicy, ctx)) 84 | 85 | // Kubernetes Image Policy Webhook 86 | router.POST("/k8s-image-policy-webhook", httpHandler(postK8sImagePolicy, ctx)) 87 | 88 | // Crawlers 89 | router.GET("/crawlers/:id", httpHandler(getCrawler, ctx)) 90 | router.POST("/crawlers/:type", httpHandler(postCrawler, ctx)) 91 | 92 | // Containers place holder 93 | router.GET("/containers", httpHandler(listContainers, ctx)) 94 | router.GET("/containers/:id", httpHandler(getContainer, ctx)) 95 | 96 | // Metrics 97 | router.GET("/metrics", httpHandler(getMetrics, ctx)) 98 | 99 | return router 100 | } 101 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package cmd 18 | 19 | import ( 20 | "github.com/pkg/errors" 21 | "github.com/urfave/cli" 22 | ) 23 | 24 | // CommandFactory func init 25 | type CommandFactory func() cli.Command 26 | 27 | var commandFactories = make(map[string]CommandFactory) 28 | 29 | func init() { 30 | RegisterCommand("serve", newServeCommand) 31 | } 32 | 33 | // App func init 34 | func App(appVersion string) *cli.App { 35 | app := cli.NewApp() 36 | app.Name = "Port Authority" 37 | app.Version = appVersion 38 | app.Usage = "" 39 | app.Flags = []cli.Flag{} 40 | 41 | for _, factory := range commandFactories { 42 | app.Commands = append(app.Commands, factory()) 43 | } 44 | 45 | return app 46 | } 47 | 48 | // RegisterCommand adds a new command factory that will be used to build cli 49 | // commands and returns error if factory is nil or command has always been 50 | // registered 51 | func RegisterCommand(name string, factory CommandFactory) error { 52 | if factory == nil { 53 | return errors.Errorf("Command Factory %s does not exist", name) 54 | } 55 | 56 | _, registered := commandFactories[name] 57 | if registered { 58 | return errors.Errorf("Command factory %s already registered", name) 59 | } 60 | 61 | commandFactories[name] = factory 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package cmd 18 | 19 | import ( 20 | "errors" 21 | "io/ioutil" 22 | "os" 23 | "time" 24 | 25 | "github.com/target/portauthority/api" 26 | "github.com/target/portauthority/pkg/datastore" 27 | 28 | "gopkg.in/yaml.v2" 29 | ) 30 | 31 | // ErrDatasourceNotLoaded is returned when the datasource variable in the 32 | // configuration file is not loaded properly 33 | var ErrDatasourceNotLoaded = errors.New("could not load configuration: no database source specified") 34 | 35 | // File represents a YAML configuration file that namespaces all Port Authority 36 | // configuration under the top-level "portauthority" key 37 | type File struct { 38 | PortAuthority Config `yaml:"portauthority"` 39 | } 40 | 41 | // Config is the global configuration for an instance of Port Authority 42 | type Config struct { 43 | Database datastore.BackendConfig 44 | API *api.Config 45 | } 46 | 47 | // DefaultConfig is a configuration that can be used as a fallback value 48 | func DefaultConfig() Config { 49 | return Config{ 50 | Database: datastore.BackendConfig{ 51 | Type: "pgsql", 52 | }, 53 | API: &api.Config{ 54 | Port: 8080, 55 | HealthPort: 8081, 56 | Timeout: 900 * time.Second, 57 | }, 58 | } 59 | } 60 | 61 | // LoadConfig is a shortcut to open a file, read it, and generate a Config. 62 | // It supports relative and absolute paths. Given "", it returns DefaultConfig. 63 | func LoadConfig(path string) (config *Config, err error) { 64 | var cfgFile File 65 | cfgFile.PortAuthority = DefaultConfig() 66 | if path == "" { 67 | return &cfgFile.PortAuthority, nil 68 | } 69 | 70 | f, err := os.Open(os.ExpandEnv(path)) 71 | if err != nil { 72 | return 73 | } 74 | defer f.Close() 75 | 76 | d, err := ioutil.ReadAll(f) 77 | if err != nil { 78 | return 79 | } 80 | 81 | err = yaml.Unmarshal(d, &cfgFile) 82 | if err != nil { 83 | return 84 | } 85 | config = &cfgFile.PortAuthority 86 | 87 | return 88 | } 89 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package cmd 18 | 19 | import ( 20 | "math/rand" 21 | "os" 22 | "os/signal" 23 | "strings" 24 | "syscall" 25 | "time" 26 | 27 | log "github.com/sirupsen/logrus" 28 | "github.com/target/portauthority/api" 29 | "github.com/target/portauthority/pkg/clair/client" 30 | "github.com/target/portauthority/pkg/datastore" 31 | "github.com/target/portauthority/pkg/formatter" 32 | "github.com/target/portauthority/pkg/stopper" 33 | "github.com/urfave/cli" 34 | ) 35 | 36 | func newServeCommand() cli.Command { 37 | return cli.Command{ 38 | Name: "serve", 39 | Description: "Starts Port Authority as a daemon", 40 | Usage: "portauthority serve [OPTIONS]", 41 | Action: serve, 42 | Flags: []cli.Flag{ 43 | cli.StringFlag{ 44 | Name: "config, c", 45 | Usage: "path to configuration file", 46 | EnvVar: "PA_CONFIG", 47 | }, 48 | cli.BoolFlag{ 49 | Name: "insecure-tls, i", 50 | Usage: "Disable TLS server's certificate chain and hostname verification when talking to other services", 51 | EnvVar: "PA_INSECURE_TLS", 52 | Hidden: false, 53 | }, 54 | cli.StringFlag{ 55 | Name: "log-level, l", 56 | Usage: "Define the logging level.", 57 | EnvVar: "PA_LOG_LEVEL", 58 | Value: "info", 59 | }, 60 | }, 61 | } 62 | } 63 | 64 | func waitForSignals(signals ...os.Signal) { 65 | interrupts := make(chan os.Signal, 1) 66 | signal.Notify(interrupts, signals...) 67 | <-interrupts 68 | } 69 | 70 | func serve(ctx *cli.Context) error { 71 | 72 | // Load configuration 73 | config, err := LoadConfig(ctx.String("config")) 74 | if err != nil { 75 | log.WithError(err).Fatal("failed to load configuration") 76 | } 77 | 78 | logLevel, err := log.ParseLevel(strings.ToUpper(ctx.String("log-level"))) 79 | log.SetLevel(logLevel) 80 | log.SetOutput(os.Stdout) 81 | log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true}) 82 | 83 | rand.Seed(time.Now().UnixNano()) 84 | st := stopper.NewStopper() 85 | 86 | // Open database 87 | db, err := datastore.Open(config.Database) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | defer db.Close() 92 | 93 | // Create clair client 94 | cc := clairclient.DefaultConfig() 95 | cc.Address = config.API.ClairURL 96 | cc.HTTPClient.Timeout = time.Second * time.Duration(config.API.ClairTimeout) 97 | client, err := clairclient.NewClient(cc) 98 | if err != nil { 99 | log.Fatal(err, "error creating clair client") 100 | } 101 | 102 | // Start API 103 | st.Begin() 104 | go api.Run(config.API, *client, db, st) 105 | st.Begin() 106 | go api.RunHealth(config.API, db, st) 107 | 108 | // Wait for interruption and shutdown gracefully. 109 | waitForSignals(syscall.SIGINT, syscall.SIGTERM) 110 | log.Info("Received interruption, gracefully stopping ...") 111 | st.Stop() 112 | 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /config.api.example.yml: -------------------------------------------------------------------------------- 1 | # The values specified here are the default values that Port Authority uses if no configuration file is specified or if 2 | # the keys are not defined. 3 | portauthority: 4 | database: 5 | # Database driver 6 | type: pgsql 7 | options: 8 | # PostgreSQL Connection string 9 | # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING 10 | 11 | # This can be the same DB as the one used by Clair, or a different one 12 | source: host=192.168.99.100 port=30607 user=postgres password=password sslmode=disable statement_timeout=60000 13 | 14 | # Number of elements kept in the cache 15 | # Values unlikely to change are cached in order to save prevent needless roundtrips to the database. 16 | cachesize: 16384 17 | api: 18 | # API server port 19 | port: 6100 20 | 21 | # Health server port 22 | # This is an unencrypted endpoint useful for load balancers to check the health of the port authority server. 23 | healthport: 6101 24 | 25 | # Deadline before an API request will respond with a 503 26 | timeout: 900s 27 | 28 | # Setting imagewebhookdefaultblock to true will set the imagewebhooks endpoint default behavior to block any images with 29 | # policy violations. 30 | # If it is set to false, a user can still enable the behavior by setting the portauthority-webhook annotation to true 31 | imagewebhookdefaultblock: false 32 | 33 | # URL of the Clair server that Port Authority sends its images/layers to for scanning 34 | # If Clair is running in Minikube, change the path to the advertised service (e.g. http://clair-service:6100) 35 | clairurl: http://192.168.99.100:32355 36 | clairtimeout: 900 37 | 38 | ### Environment variables defined below are mapped to credentials used by the Kubernetes Crawler API (/v1/crawler/k8s) 39 | ### A 'Scan: true' flag will invoke their usage 40 | k8scrawlcredentials: 41 | - url: "docker.io" #basic auth is empty UN and PW 42 | username: "" 43 | password: "" 44 | - url: "gcr.io" #basic auth is empty UN and PW 45 | username: "" 46 | password: "" 47 | -------------------------------------------------------------------------------- /docs/postman/README.md: -------------------------------------------------------------------------------- 1 | ## Port Authority v1 API 2 | 3 | All API examples here are to be used with [Postman](https://www.getpostman.com/) and are directed at the default Minikube cluster ip `http://192.168.99.100` and Port Authority's exposed NodePort `31700`. Curl API examples can be found [here](/docs/README.md). 4 | 5 | ### Environment Setup (Minikube) 6 | 7 | The Postman environment variables for Minikube can be imported from the [environments folder](environments/portauthority - minikube.postman_environment.json). This defines the hostname, version, port, and Kubernetes config for the API examples to run. 8 | 9 | *NOTE: The value for env variable kube_config needs to be populated with Minikube's flattened and* **base64** *encoded config. (`kubectl config view --flatten=true | base64 | pbcopy`)* 10 | 11 | ### Collection Setup (Minikube) 12 | 13 | The Postman collection of API examples for Minikube can be imported from the [collections folder](collections/portauthority Examples.postman_collection.json). This covers all of the API functionality except creating registry crawlers. 14 | 15 | *NOTE: Public registry crawling is not supported, though external registries can be crawled using the following example.* 16 | 17 | #### Example Registry Request 18 | 19 | ``` 20 | curl -X POST \ 21 | http://192.168.99.100:31700/v1/crawlers/registry \ 22 | -H "accept: application/json" \ 23 | -H "Content-Type: application/json" \ 24 | -d '{ 25 | "RegCrawler": 26 | { 27 | "Registry": "mybinrepo", 28 | "Repos": ["path/toimage"], 29 | "Tags": ["latest"], 30 | "MaxThreads": 100, 31 | "Username": "mybinrepo_username", 32 | "Password": "mybinrepo_password" 33 | } 34 | }' 35 | ``` 36 | 37 | #### Example Response 38 | 39 | ```json 40 | { 41 | "Crawler": { 42 | "ID": 2, 43 | "Type": "registry", 44 | "Started": "2017-08-11T17:03:32.845608Z", 45 | "Finished": "0000-00-00T00:00:00.000000Z" 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/postman/collections/Port_Authority Examples.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "name": "portauthority Examples", 4 | "_postman_id": "99b7494e-a613-9517-c937-2792f310c198", 5 | "description": "Examples for using postman to test/execute Port Authority API calls", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [{ 9 | "name": "POST", 10 | "item": [{ 11 | "name": "POST k8s Crawler with Scan", 12 | "request": { 13 | "method": "POST", 14 | "header": [], 15 | "body": { 16 | "mode": "raw", 17 | "raw": "{\n\t\"K8sCrawler\": {\n\t\t\"Context\": \"minikube\",\n\t\t\"KubeConfig\": \"{{k8s_config}}\",\n\t\t\"Scan\": true,\n\t\t\"MaxThreads\": 10\n\t}\n}" 18 | }, 19 | "url": { 20 | "raw": "{{host}}:{{port}}/{{version}}/crawlers/k8s", 21 | "host": [ 22 | "{{host}}" 23 | ], 24 | "port": "{{port}}", 25 | "path": [ 26 | "{{version}}", 27 | "crawlers", 28 | "k8s" 29 | ] 30 | } 31 | }, 32 | "response": [] 33 | }, 34 | { 35 | "name": "POST k8s Crawler without Scan", 36 | "request": { 37 | "method": "POST", 38 | "header": [], 39 | "body": { 40 | "mode": "raw", 41 | "raw": "{\n\t\"K8sCrawler\": {\n\t\t\"Context\": \"minikube\",\n\t\t\"KubeConfig\": \"{{k8s_config}}\",\n\t\t\"Scan\": false\n\t}\n}" 42 | }, 43 | "url": { 44 | "raw": "{{host}}:{{port}}/{{version}}/crawlers/k8s", 45 | "host": [ 46 | "{{host}}" 47 | ], 48 | "port": "{{port}}", 49 | "path": [ 50 | "{{version}}", 51 | "crawlers", 52 | "k8s" 53 | ] 54 | } 55 | }, 56 | "response": [] 57 | }, 58 | { 59 | "name": "POST Single ImageScan - docker.io Image", 60 | "request": { 61 | "method": "POST", 62 | "header": [], 63 | "body": { 64 | "mode": "raw", 65 | "raw": "{\n\t\"Image\": {\n\t\t\"Registry\": \"https://registry-1.docker.io\",\n\t\t\"Repo\": \"library/postgres\",\n\t\t\"Tag\": \"9.1\",\n\t\t\"Username\": \"\",\n\t\t\"Password\": \"\",\n\t\t\"Metadata\": { \n\t\t\t\"data\": \"is so meta\"\n\t\t}\n\t}\n}" 66 | }, 67 | "url": { 68 | "raw": "{{host}}:{{port}}/{{version}}/images", 69 | "host": [ 70 | "{{host}}" 71 | ], 72 | "port": "{{port}}", 73 | "path": [ 74 | "{{version}}", 75 | "images" 76 | ] 77 | } 78 | }, 79 | "response": [] 80 | }, 81 | { 82 | "name": "POST K8s Image Policy Webhook", 83 | "request": { 84 | "method": "POST", 85 | "header": [], 86 | "body": { 87 | "mode": "raw", 88 | "raw": "{\n\t\"kind\": \"ImageReview\",\n\t\"apiVersion\": \"imagepolicy.k8s.io/v1alpha1\",\n\t\"metadata\": {\n\t\t\"creationTimestamp\": null\n\t},\n\t\"spec\": {\n\t\t\"containers\": [{\n\t\t\t\"image\": \"https://registry-1.docker.io/library/postgres:latest\"\n\t\t}],\n\t\t\"annotations\": {\n\t\t\t\"alpha.image-policy.k8s.io/policy\": \"default\",\n\t\t\t\"alpha.image-policy.k8s.io/portauthority-webhook-enable\": \"true\"\n\t\t},\n\t\t\"namespace\": \"default\"\n\t}\n}" 89 | }, 90 | "url": { 91 | "raw": "{{host}}:{{port}}/{{version}}/k8s-image-policy-webhook", 92 | "host": [ 93 | "{{host}}" 94 | ], 95 | "port": "{{port}}", 96 | "path": [ 97 | "{{version}}", 98 | "k8s-image-policy-webhook" 99 | ] 100 | } 101 | }, 102 | "response": [] 103 | }, 104 | { 105 | "name": "POST Image Policy", 106 | "request": { 107 | "method": "POST", 108 | "header": [], 109 | "body": { 110 | "mode": "raw", 111 | "raw": "{\n\t\"Policy\": {\n\t\t\"Name\": \"default\",\n\t\t\"AllowedRiskSeverity\": \"\",\n\t\t\"AllowedCVENames\": \"\",\n\t\t\"AllowNotFixed\": false,\n\t\t\"NotAllowedCveNames\": \"\",\n\t\t\"NotAllowedOSNames\": \"\"\n\t}\n}" 112 | }, 113 | "url": { 114 | "raw": "{{host}}:{{port}}/{{version}}/policies", 115 | "host": [ 116 | "{{host}}" 117 | ], 118 | "port": "{{port}}", 119 | "path": [ 120 | "{{version}}", 121 | "policies" 122 | ] 123 | } 124 | }, 125 | "response": [] 126 | } 127 | ] 128 | }, 129 | { 130 | "name": "GET", 131 | "item": [{ 132 | "name": "GET Crawler by ID", 133 | "request": { 134 | "method": "GET", 135 | "header": [], 136 | "body": { 137 | "mode": "raw", 138 | "raw": "{\n\t\"K8sCrawler\": {\n\t\t\"Context\": \"minikube\",\n\t\t\"KubeConfig\": \"{{k8s_config}}\"\n\t}\n}" 139 | }, 140 | "url": { 141 | "raw": "{{host}}:{{port}}/{{version}}/crawlers/1", 142 | "host": [ 143 | "{{host}}" 144 | ], 145 | "port": "{{port}}", 146 | "path": [ 147 | "{{version}}", 148 | "crawlers", 149 | "1" 150 | ] 151 | } 152 | }, 153 | "response": [] 154 | }, 155 | { 156 | "name": "GET Image by ID with features and vulnerabilites", 157 | "request": { 158 | "method": "GET", 159 | "header": [], 160 | "body": { 161 | "mode": "raw", 162 | "raw": "{\n \"Image\": {\n \"Registry\": \"https://registry-1.docker.io\",\n \"Repo\": \"library/postgres\",\n \"Tag\": \"latest\"\n }\n}" 163 | }, 164 | "url": { 165 | "raw": "{{host}}:{{port}}/{{version}}/images/1?features&vulnerabilites", 166 | "host": [ 167 | "{{host}}" 168 | ], 169 | "port": "{{port}}", 170 | "path": [ 171 | "{{version}}", 172 | "images", 173 | "1" 174 | ], 175 | "query": [{ 176 | "key": "features", 177 | "value": "", 178 | "equals": true 179 | }, 180 | { 181 | "key": "vulnerabilites", 182 | "value": "", 183 | "equals": true 184 | } 185 | ] 186 | } 187 | }, 188 | "response": [] 189 | }, 190 | { 191 | "name": "GET Image Policy Violations by Image ID and Policy Name", 192 | "request": { 193 | "method": "GET", 194 | "header": [], 195 | "body": { 196 | "mode": "raw", 197 | "raw": "{\n \"Image\": {\n \"Registry\": \"https://registry-1.docker.io\",\n \"Repo\": \"library/postgres\",\n \"Tag\": \"latest\"\n }\n}" 198 | }, 199 | "url": { 200 | "raw": "{{host}}:{{port}}/{{version}}/images/1?policy=default", 201 | "host": [ 202 | "{{host}}" 203 | ], 204 | "port": "{{port}}", 205 | "path": [ 206 | "{{version}}", 207 | "images", 208 | "1" 209 | ], 210 | "query": [{ 211 | "key": "policy", 212 | "value": "default", 213 | "equals": true 214 | }] 215 | } 216 | }, 217 | "response": [] 218 | }, 219 | { 220 | "name": "GET Vulnerabilities Policy by Policy Name", 221 | "request": { 222 | "method": "GET", 223 | "header": [], 224 | "body": { 225 | "mode": "raw", 226 | "raw": "" 227 | }, 228 | "url": { 229 | "raw": "{{host}}:{{port}}/{{version}}/policies/default", 230 | "host": [ 231 | "{{host}}" 232 | ], 233 | "port": "{{port}}", 234 | "path": [ 235 | "{{version}}", 236 | "policies", 237 | "default" 238 | ] 239 | } 240 | }, 241 | "response": [] 242 | }, 243 | { 244 | "name": "GET Container by ID with image features", 245 | "request": { 246 | "method": "GET", 247 | "header": [], 248 | "body": { 249 | "mode": "raw", 250 | "raw": "" 251 | }, 252 | "url": { 253 | "raw": "{{host}}:{{port}}/{{version}}/containers/1?features", 254 | "host": [ 255 | "{{host}}" 256 | ], 257 | "port": "{{port}}", 258 | "path": [ 259 | "{{version}}", 260 | "containers", 261 | "1" 262 | ], 263 | "query": [{ 264 | "key": "features", 265 | "value": "", 266 | "equals": true 267 | }] 268 | } 269 | }, 270 | "response": [] 271 | } 272 | ] 273 | }, 274 | { 275 | "name": "LIST", 276 | "item": [{ 277 | "name": "GET list of Images by Registry", 278 | "request": { 279 | "method": "GET", 280 | "header": [], 281 | "body": { 282 | "mode": "raw", 283 | "raw": "" 284 | }, 285 | "url": { 286 | "raw": "{{host}}:{{port}}/{{version}}/images?registry=https://registry-1.docker.io", 287 | "host": [ 288 | "{{host}}" 289 | ], 290 | "port": "{{port}}", 291 | "path": [ 292 | "{{version}}", 293 | "images" 294 | ], 295 | "query": [{ 296 | "key": "registry", 297 | "value": "https://registry-1.docker.io" 298 | }] 299 | } 300 | }, 301 | "response": [] 302 | }, 303 | { 304 | "name": "GET list of Images by last seen date with a limit", 305 | "request": { 306 | "method": "GET", 307 | "header": [], 308 | "body": { 309 | "mode": "raw", 310 | "raw": "" 311 | }, 312 | "url": { 313 | "raw": "{{host}}:{{port}}/{{version}}/images?date_start=2018-04-03&limit=2", 314 | "host": [ 315 | "{{host}}" 316 | ], 317 | "port": "{{port}}", 318 | "path": [ 319 | "{{version}}", 320 | "images" 321 | ], 322 | "query": [{ 323 | "key": "date_start", 324 | "value": "2018-04-03" 325 | }, 326 | { 327 | "key": "limit", 328 | "value": "2" 329 | } 330 | ] 331 | } 332 | }, 333 | "response": [] 334 | }, 335 | { 336 | "name": "GET list of Policies by last seen date with a limit", 337 | "request": { 338 | "method": "GET", 339 | "header": [], 340 | "body": { 341 | "mode": "raw", 342 | "raw": "" 343 | }, 344 | "url": { 345 | "raw": "{{host}}:{{port}}/{{version}}/policies", 346 | "host": [ 347 | "{{host}}" 348 | ], 349 | "port": "{{port}}", 350 | "path": [ 351 | "{{version}}", 352 | "policies" 353 | ] 354 | } 355 | }, 356 | "response": [] 357 | }, 358 | { 359 | "name": "GET list of Containers by namespace and image name", 360 | "request": { 361 | "method": "GET", 362 | "header": [], 363 | "body": { 364 | "mode": "raw", 365 | "raw": "" 366 | }, 367 | "url": { 368 | "raw": "{{host}}:{{port}}/{{version}}/containers?namespace=default&name=postgres", 369 | "host": [ 370 | "{{host}}" 371 | ], 372 | "port": "{{port}}", 373 | "path": [ 374 | "{{version}}", 375 | "containers" 376 | ], 377 | "query": [{ 378 | "key": "namespace", 379 | "value": "default" 380 | }, 381 | { 382 | "key": "name", 383 | "value": "postgres" 384 | } 385 | ] 386 | } 387 | }, 388 | "response": [] 389 | } 390 | ] 391 | } 392 | ] 393 | } 394 | -------------------------------------------------------------------------------- /docs/postman/environments/Port_Authority - minikube.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "5b8c8e36-8913-dc41-dfd2-a0fc682ba816", 3 | "name": "portauthority - minikube", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "host", 8 | "value": "192.168.99.100", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "version", 14 | "value": "v1", 15 | "type": "text" 16 | }, 17 | { 18 | "enabled": true, 19 | "key": "port", 20 | "value": "31700", 21 | "type": "text" 22 | }, 23 | { 24 | "enabled": true, 25 | "key": "k8s_config", 26 | "value":"REPLACE_WITH_BASE64_MINIKUBE_CONFIG", 27 | "type": "text" 28 | } 29 | ], 30 | "timestamp": 1514997925245, 31 | "_postman_variable_scope": "environment", 32 | "_postman_exported_at": "2018-01-03T18:09:00.464Z", 33 | "_postman_exported_using": "Postman/5.3.2" 34 | } 35 | -------------------------------------------------------------------------------- /docs/webhook-example/README.md: -------------------------------------------------------------------------------- 1 | # Configuring Minikube to leverage the Port Authority ImagePolicyWebhook 2 | 3 | The following steps have been tested running on a Mac in Minikube server version 1.7.x-1.9.x and Kubectl client version 1.7.x-1.9.x 4 | 5 | #### Prerequisites 6 | 7 | Ensure you've successfully completed the [Minikube Setup](/README.md#setup-and-start-minikube). 8 | 9 | ### Setup 10 | 11 | 1. Modify the [image-review.example.yml](image-review.example.yml) where you see *** < my-id > *** 12 | 13 | ```yml 14 | # clusters refers to the remote service. 15 | # this config assumes you're running Port Authority in Minikube. 16 | # the IPs below are the one Minikube uses as your IP routable via the cluster. 17 | clusters: 18 | - name: image-review-server 19 | cluster: 20 | insecure-skip-tls-verify: true 21 | server: http://192.168.99.100:31700/v1/k8s-image-policy-webhook #nodeport routable ip 22 | #server: http://10.0.2.2:6100/v1/k8s-image-policy-webhook ##local development routeable ip to your localhost 23 | 24 | # users refers to the API server's webhook configuration. 25 | users: 26 | - name: kube-apiserver 27 | user: 28 | client-certificate: /Users//.minikube/client.crt 29 | client-key: /Users//.minikube/client.key 30 | current-context: webhook 31 | contexts: 32 | - context: 33 | cluster: image-review-server 34 | user: kube-apiserver 35 | name: webhook 36 | ``` 37 | 38 | 2. Modify the [admission-controller.example.json](admission-controller.example.json) with the appropriate you see *** < my-id > *** path to the [image-review.example.yml](image-review.example.yml) file created in the previous step. 39 | 40 | ```json 41 | { 42 | "imagePolicy": { 43 | "kubeConfigFile": "/Users//go/src/github.com/target/portauthority/docs/webhook-example/image-review.example.yml", 44 | "allowTTL": 50, 45 | "denyTTL": 50, 46 | "retryBackoff": 500, 47 | "defaultAllow": true 48 | } 49 | } 50 | ``` 51 | 52 | 3. Start Minikube with something similar to the following command after changing ** < my-id > ** 53 | 54 | ```sh 55 | minikube start \ 56 | --extra-config=apiserver.Admission.PluginNames=ImagePolicyWebhook \ 57 | --extra-config=apiserver.Admission.ConfigFile=/Users//go/src/github.com/target/portauthority/docs/webhook-example/admission-controller.example.json 58 | ``` 59 | Note: Minikube will NOT start if can't find a valid admission-controller.example.json file. 60 | 61 | ### Create a deployment to validate that it's working 62 | 63 | 1. Add the following annotations to your deployment 64 | 65 | ``` 66 | apiVersion: extensions/v1beta1 67 | kind: Deployment 68 | metadata: 69 | labels: 70 | service: myapp-deployment 71 | name: myapp-deployment 72 | spec: 73 | template: 74 | metadata: 75 | labels: 76 | app: myapp-deployment 77 | annotations: 78 | alpha.image-policy.k8s.io/portauthority-webhook-enable: "true" 79 | alpha.image-policy.k8s.io/policy: "default" 80 | spec: 81 | containers: 82 | - name: postgres 83 | env: 84 | - name: PGUSER 85 | value: postgres 86 | - name: PGPASSWORD 87 | value: password 88 | image: postgres:9.6 89 | ``` 90 | 91 | ### Default behavior of the webhook-example 92 | 93 | The default behavior of the webhook is configurable on your Port Authority endpoint within `config.yml`: 94 | 95 | 1. Secure configuration (requires opt-out annotation): 96 | 97 | `imagewebhookdefaultblock: true` 98 | 99 | 2. Insecure configuration (requires opt-in annotation): 100 | 101 | `imagewebhookdefaultblock: false` 102 | 103 | Flow Diagram: 104 | 105 | ![image](imagepolicywebhookflow.png) 106 | -------------------------------------------------------------------------------- /docs/webhook-example/admission-controller.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "imagePolicy": { 3 | "kubeConfigFile": "/Users//go/src/github.com/target/portauthority/webhook-example/image-review.example.yml", 4 | "allowTTL": 50, 5 | "denyTTL": 50, 6 | "retryBackoff": 500, 7 | "defaultAllow": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/webhook-example/image-review.example.yml: -------------------------------------------------------------------------------- 1 | # clusters refers to the remote service. 2 | # this config assumes you're running port authority locally. 3 | # the IPs below are the one minikube uses as your IP routable via the cluster. 4 | clusters: 5 | - name: image-review-server 6 | cluster: 7 | insecure-skip-tls-verify: true 8 | server: http://192.168.99.100:31700/v1/k8s-image-policy-webhook #nodeport routable ip 9 | #server: http://10.0.2.2:6100/v1/k8s-image-policy-webhook ##local development routeable ip 10 | 11 | # users refers to the API server's webhook configuration. 12 | users: 13 | - name: kube-apiserver 14 | user: 15 | client-certificate: /Users//.minikube/client.crt 16 | client-key: /Users//.minikube/client.key 17 | 18 | 19 | current-context: webhook 20 | 21 | contexts: 22 | - context: 23 | cluster: image-review-server 24 | user: kube-apiserver 25 | name: webhook 26 | -------------------------------------------------------------------------------- /docs/webhook-example/imagepolicywebhookflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/portauthority/d1992f0d755920dc8a79d033bab1280a2fc11b4a/docs/webhook-example/imagepolicywebhookflow.png -------------------------------------------------------------------------------- /docs/webhook-example/imagepolicywebhookflow.xml: -------------------------------------------------------------------------------- 1 | 7V1bc6M4Fv41rpp9cEqAuT3GTrKztT0zXZOu7ulHGWSbCUZejHOZX78SCAxGXBxAxonykJhjcZM+vvPpHHEy0Rbb13+HcLf5DbvIn6jAfZ1odxNVVWaKSf5Qyxuz2DM1saxDz2W2o+HR+wcxI2DWg+eifaFhhLEfebui0cFBgJyoYINhiF+KzVbYL551B9eoZHh0oF+2/vDcaJNYLR0c7b8ib71Jz6wA9s0SOk/rEB8Cdr6Jqq3in+TrLUyPxdrvN9DFLzmTdj/RFiHGUfJp+7pAPu3ctNuS/R4qvs2uO0RB1GYH0072eIb+AaWXbPhk3/l+BwPyeU0/f8VhRFrdHqINDr3ojY4tWsGDT61397//JH/uA3eHPXJatj85af4Q7H6jt7SP91GIn7LuJXc+30Rbn3xU6Mk3cEfbbV/XFGI3Kx+/OBsYRje7EDtoT8Z3/rLxIvS4gw5t+EKaEVvc9YjeHCBbPlwif54NyQL7OCRfBTggu8xXnu+nJjJOro4sd0btOIge4NbzKXq/o9CFAWRmhlQFsO3c7iD+IXboe+uA2BwyBCik9xLf6OnJ6dFYZ9AbfkZh5BEA3rK9I7xLxsDxgvU3unE3y3qRNkavlUOuZEAiTyjCWxSFdMTYDlqKvZccsg1m2+RQrdnMCNnTtM6OdUQU+cBAVQEwqwSw/1p7Ylj4hz3tn9HDAiJr5Xx8WKhGCRWqORgqAId2ToCAXELJbJPQzwavcQD9+6P1ZFRzMEGvXvQXNd/obOtn7puvKPTIFdNBiHckHRS+/ZU2oBs/j/vSzdMdKvBDRvvu9t56WJAmf6MoemOwgIcIE9PxHr7geOCUEgjIEcz7W+MetIKbVYO2PI5YX9P+rMcG6X58CB2Ujj5zvDBcI9ZMn/ExFCIfRt5z8fidAKI0+KUcUoz/Hai7nB9979RJuuSWXtp6+YuqaBPKeuS0gPDf8bOi/uu4f+qpft4/tvVh5HGLitAL0d77By5Zx2vz2CXGHaHPJ/odjwhOn/Gt57oxvqtxxjQFFw4VoGmJEg6HMJHFbuqoXfL4MQEfGOxIU3CjGqkqZKJwyg7fGjvs6F8TkZE1wavVnuDzFFzZRbTCW3ptHLzRTuLijX4x3cedT4Fm7V4TEJ3A6St2qb97JM9RRMjqiKzkwBXIIpdHxC6q8Gc5wMH9LlHAK++VcuFZvo7rg8oe+MQlGgvrfv7QmaNKD0I7OLZ3abpRAJyil3UPx8FZPfg3pVpWnwOnaizh3a49lt4pqcigbL0ARnTg+lZVGYTISV0PHQGS00E9IqtRa/UBt1kRbhoo4y2bJ+YBZ/QAOEOMoEo/5wRSnaDKNqSgooIKlAWVqgkSVCoPIAl9uN7zkTrKc/++/OAd2vn4bYsKEYIib1WIrszMuVJBF78IEazz3rVXvgyb7qUfwnaxc4j7dzi6/gC8rBgtednsg5dnI+LlT0KzKodmRc1b05M3xFNJTz4ddsncKoJeQONg4D9buEb7aw2fWupcM4wrpQhD1U+mCuUYKZcjMmMnkuAFO6R4GxeraGVW0VVRrKKdL96qtM4LQy9VOwEOt9Avq52YiX6gJWGpWg3Tg0zrcpnfQm+9RmFBlrW5pJ7UFnK8vYeDAdXWSCbHKT11YVjFPA3GCGVYXQjDynxDF4adcRhWlG4zjAbdNmC+4fc/ZLohN4xt0g3J81xJNuAGmAYLwb43vcAOZRVpa6rPikfoJftg8LIPUgCOnp7UChj2T0/lVRQyHTpmfjJr+YmmQzVgFZlF68ZXvRKSygtblZR8PE0grR4dSLqE7PpwrYpbaPCiRtX3GdMwDe2SipuXAZUubVwurav74ksmBRjmzUy1dFNJfhcllArAjZL/unj8xL+yQ3YjsfKkb7FBztOELgimAdfvBz9AIVx6fryktTJSkTM9e5j2DQ5oGh6uoRfso3MiH3BLmSpY7nexM/Q9Jztxc5ZoSG79iHHhGi3RA7/aStGBi+VXXcyUoRjRADKicRa/8lZQGoKmDPoFpwwyonH2jEGvnzHQiIalFCcMHddPpnMR48Q9Fw/Qz3SienVlzpX9CqlT/X70sB9gPjGSCH4f8wkF6Jd0eKaYEL6cUHRweLrKcXiiVrilJ29YevEnevbQC2n3Ay03cYIT3AYBjlJVL1dfCGcWbQYuqaTFRCrOVNI5LslyiZ+YWHirL0QpaaOJWGTwvWcpXb3goJWUtmvZZgpuFBsU13tNjZ60dOGog6QKeUuBpAwaPVslwyTizUm5kuGayMqokMdHsgKmMuvGTr0SkF4mIDJ28Gm69uE+mbOPfXa+shzktHulv4NYJk2Xlj7T28eJznilwbzkemWTJ4gGUMytfNAncSmc1SdKhc7pf2ZdvRqg67tWJRfyJ9rvyDwcve9NK066rG1ajWPqdCO3PmGyGN4P0G9zP4Mk9RDBVRzZkCGHk3fCisUINMAh0PSF8b5fCjPFaHjJnzn+NDj8WZFy6Z8/myS55M86/vwWHiR9jps+OSXFBmNPq93TVFuy7vbLlz9+TGTNumspTpbVrEsja5rIGnZWOX0ta9iNESaq0YiS4WraWbwFN0PnkeQ7ZkWZltd8CuCsv1LS1agtUNW7ELSaqq3KTNKoFmUlj3TtaxyjrmqX0pqsavdxq9rNZuLK2tnVVTk7zfVkWbtRz/UKeDM1gWXtbDGZBpntbqupbE5q2zYvp6jsd1RGkWXtZFm7YV3+JXhZtwWWtbNlWbthaVbnzFyBekGe5a2+l3Xtxs4Rp3XtdIsTqB9snYjNi9RL9XZBWuEFxIB9QVpp9WJYY4wsVjuysF1feksWtutQ2E4wxcqkw7goNlNpo0k62BdMOnzKBeHdcg52fc7h2krbZSwnReCoGQrMLsdQCpD/7euqOIo91LWJ0VHXt8ueAVng7uMUuBOrvBUg38Qcm2MbwIfxpVNDiTtDE1TiLrtlWePuk8aIxda4E02xlyjb31ia41NHN7hLKq1Lzh3k++7XNXVoLt1/vYXusudDVrq74hVvpUp3ot2emKC+nFm0d3s2z+1dNGTWFNWX1e5Gyi6n1e4Ek4siJh4vy921JpeUR8ajqZULxuM/pabuVkOKPdN14fgrrninKLxYvZRDwzCW8l7GSleIXoawql8DkAnEUTKW2sRYIyt7l2E+BzGmqacoSG72GkrfrVaqkNJ3rrE0dGPSv3w+LX0nWj4LCkm3ckZX5lvaqWFOpSa7PUoG8C2yetP5NyKr3405BHFavsnkFEoZrH6Togj611Wfl0ItDoXyVJgwCm36X1SSQusoVBbAGz2DzngFmHtiULIZYgqN49SFjtNv2EW0xf8B -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 84393a77de3a79dfb1db138c6ae21db3ae729337f8dfd1181a4a15317e318679 2 | updated: 2018-05-29T11:18:34.609644791-05:00 3 | imports: 4 | - name: cloud.google.com/go 5 | version: 3b1ae45394a234c385be014e9a488f2bb6eef821 6 | subpackages: 7 | - compute/metadata 8 | - internal 9 | - name: github.com/beorn7/perks 10 | version: 3a771d992973f24aa725d07868b467d1ddfceafb 11 | subpackages: 12 | - quantile 13 | - name: github.com/docker/distribution 14 | version: 83389a148052d74ac602f5f1d62f86ff2f3c4aa5 15 | subpackages: 16 | - context 17 | - digestset 18 | - manifest 19 | - manifest/schema1 20 | - manifest/schema2 21 | - reference 22 | - uuid 23 | - name: github.com/docker/libtrust 24 | version: aabc10ec26b754e797f9028f4589c5b7bd90dc20 25 | - name: github.com/fatih/structs 26 | version: a720dfa8df582c51dee1b36feabb906bde1588bd 27 | - name: github.com/ghodss/yaml 28 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 29 | - name: github.com/gogo/protobuf 30 | version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 31 | subpackages: 32 | - proto 33 | - sortkeys 34 | - name: github.com/golang/glog 35 | version: 44145f04b68cf362d9c4df2182967c2275eaefed 36 | - name: github.com/golang/protobuf 37 | version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 38 | subpackages: 39 | - proto 40 | - ptypes 41 | - ptypes/any 42 | - ptypes/duration 43 | - ptypes/timestamp 44 | - name: github.com/google/gofuzz 45 | version: 44d81051d367757e1c7c6a5a86423ece9afcf63c 46 | - name: github.com/googleapis/gnostic 47 | version: 0c5108395e2debce0d731cf0287ddf7242066aba 48 | subpackages: 49 | - OpenAPIv2 50 | - compiler 51 | - extensions 52 | - name: github.com/imdario/mergo 53 | version: 6633656539c1639d9d78127b7d47c622b5d7b6dc 54 | - name: github.com/json-iterator/go 55 | version: 2ddf6d758266fcb080a4f9e054b9f292c85e6798 56 | - name: github.com/julienschmidt/httprouter 57 | version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 58 | - name: github.com/lib/pq 59 | version: d34b9ff171c21ad295489235aec8b6626023cd04 60 | subpackages: 61 | - oid 62 | - name: github.com/matttproud/golang_protobuf_extensions 63 | version: c12348ce28de40eed0136aa2b644d0ee0650e56c 64 | subpackages: 65 | - pbutil 66 | - name: github.com/modern-go/concurrent 67 | version: bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94 68 | - name: github.com/modern-go/reflect2 69 | version: 05fbef0ca5da472bbf96c9322b84a53edc03c9fd 70 | - name: github.com/opencontainers/go-digest 71 | version: 279bed98673dd5bef374d3b6e4b09e2af76183bf 72 | - name: github.com/pkg/errors 73 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 74 | - name: github.com/prometheus/client_golang 75 | version: 967789050ba94deca04a5e84cce8ad472ce313c1 76 | subpackages: 77 | - prometheus 78 | - name: github.com/prometheus/client_model 79 | version: 99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c 80 | subpackages: 81 | - go 82 | - name: github.com/prometheus/common 83 | version: d811d2e9bf898806ecfb6ef6296774b13ffc314c 84 | subpackages: 85 | - expfmt 86 | - internal/bitbucket.org/ww/goautoneg 87 | - model 88 | - name: github.com/prometheus/procfs 89 | version: 8b1c2da0d56deffdbb9e48d4414b4e674bd8083e 90 | subpackages: 91 | - internal/util 92 | - nfs 93 | - xfs 94 | - name: github.com/sirupsen/logrus 95 | version: c155da19408a8799da419ed3eeb0cb5db0ad5dbc 96 | - name: github.com/spf13/pflag 97 | version: 583c0c0531f06d5278b7d917446061adc344b5cd 98 | - name: github.com/tylerb/graceful 99 | version: 4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb 100 | - name: github.com/urfave/cli 101 | version: cfb38830724cc34fedffe9a2a29fb54fa9169cd1 102 | - name: golang.org/x/crypto 103 | version: 2d027ae1dddd4694d54f7a8b6cbe78dca8720226 104 | subpackages: 105 | - ssh/terminal 106 | - name: golang.org/x/net 107 | version: 1c05540f6879653db88113bc4a2b70aec4bd491f 108 | subpackages: 109 | - context 110 | - context/ctxhttp 111 | - http2 112 | - http2/hpack 113 | - idna 114 | - lex/httplex 115 | - name: golang.org/x/oauth2 116 | version: a6bd8cefa1811bd24b86f8902872e4e8225f74c4 117 | subpackages: 118 | - google 119 | - internal 120 | - jws 121 | - jwt 122 | - name: golang.org/x/sys 123 | version: 95c6576299259db960f6c5b9b69ea52422860fce 124 | subpackages: 125 | - unix 126 | - windows 127 | - name: golang.org/x/text 128 | version: b19bf474d317b857955b12035d2c5acb57ce8b01 129 | subpackages: 130 | - secure/bidirule 131 | - transform 132 | - unicode/bidi 133 | - unicode/norm 134 | - name: golang.org/x/time 135 | version: f51c12702a4d776e4c1fa9b0fabab841babae631 136 | subpackages: 137 | - rate 138 | - name: google.golang.org/appengine 139 | version: 962cbd1200af94a5a35ba8d512e9f91271b4d01a 140 | subpackages: 141 | - internal 142 | - internal/app_identity 143 | - internal/base 144 | - internal/datastore 145 | - internal/log 146 | - internal/modules 147 | - internal/remote_api 148 | - internal/urlfetch 149 | - urlfetch 150 | - name: gopkg.in/inf.v0 151 | version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 152 | - name: gopkg.in/yaml.v2 153 | version: 5420a8b6744d3b0345ab293f6fcba19c978f1183 154 | - name: k8s.io/api 155 | version: 53d615ae3f440f957cb9989d989d597f047262d9 156 | subpackages: 157 | - admissionregistration/v1alpha1 158 | - admissionregistration/v1beta1 159 | - apps/v1 160 | - apps/v1beta1 161 | - apps/v1beta2 162 | - authentication/v1 163 | - authentication/v1beta1 164 | - authorization/v1 165 | - authorization/v1beta1 166 | - autoscaling/v1 167 | - autoscaling/v2beta1 168 | - batch/v1 169 | - batch/v1beta1 170 | - batch/v2alpha1 171 | - certificates/v1beta1 172 | - core/v1 173 | - events/v1beta1 174 | - extensions/v1beta1 175 | - imagepolicy/v1alpha1 176 | - networking/v1 177 | - policy/v1beta1 178 | - rbac/v1 179 | - rbac/v1alpha1 180 | - rbac/v1beta1 181 | - scheduling/v1alpha1 182 | - settings/v1alpha1 183 | - storage/v1 184 | - storage/v1alpha1 185 | - storage/v1beta1 186 | - name: k8s.io/apimachinery 187 | version: 13b73596e4b63e03203e86f6d9c7bcc1b937c62f 188 | subpackages: 189 | - pkg/api/errors 190 | - pkg/api/meta 191 | - pkg/api/resource 192 | - pkg/apis/meta/v1 193 | - pkg/apis/meta/v1beta1 194 | - pkg/conversion 195 | - pkg/conversion/queryparams 196 | - pkg/fields 197 | - pkg/labels 198 | - pkg/runtime 199 | - pkg/runtime/schema 200 | - pkg/runtime/serializer 201 | - pkg/runtime/serializer/json 202 | - pkg/runtime/serializer/protobuf 203 | - pkg/runtime/serializer/recognizer 204 | - pkg/runtime/serializer/streaming 205 | - pkg/runtime/serializer/versioning 206 | - pkg/selection 207 | - pkg/types 208 | - pkg/util/clock 209 | - pkg/util/errors 210 | - pkg/util/framer 211 | - pkg/util/intstr 212 | - pkg/util/json 213 | - pkg/util/net 214 | - pkg/util/runtime 215 | - pkg/util/sets 216 | - pkg/util/validation 217 | - pkg/util/validation/field 218 | - pkg/util/wait 219 | - pkg/util/yaml 220 | - pkg/version 221 | - pkg/watch 222 | - third_party/forked/golang/reflect 223 | - name: k8s.io/client-go 224 | version: 638bb430f4aff1c30dd25aab07e1dbcfc78371e4 225 | subpackages: 226 | - api 227 | - discovery 228 | - kubernetes 229 | - kubernetes/scheme 230 | - kubernetes/typed/admissionregistration/v1alpha1 231 | - kubernetes/typed/admissionregistration/v1beta1 232 | - kubernetes/typed/apps/v1 233 | - kubernetes/typed/apps/v1beta1 234 | - kubernetes/typed/apps/v1beta2 235 | - kubernetes/typed/authentication/v1 236 | - kubernetes/typed/authentication/v1beta1 237 | - kubernetes/typed/authorization/v1 238 | - kubernetes/typed/authorization/v1beta1 239 | - kubernetes/typed/autoscaling/v1 240 | - kubernetes/typed/autoscaling/v2beta1 241 | - kubernetes/typed/batch/v1 242 | - kubernetes/typed/batch/v1beta1 243 | - kubernetes/typed/batch/v2alpha1 244 | - kubernetes/typed/certificates/v1beta1 245 | - kubernetes/typed/core/v1 246 | - kubernetes/typed/events/v1beta1 247 | - kubernetes/typed/extensions/v1beta1 248 | - kubernetes/typed/networking/v1 249 | - kubernetes/typed/policy/v1beta1 250 | - kubernetes/typed/rbac/v1 251 | - kubernetes/typed/rbac/v1alpha1 252 | - kubernetes/typed/rbac/v1beta1 253 | - kubernetes/typed/scheduling/v1alpha1 254 | - kubernetes/typed/settings/v1alpha1 255 | - kubernetes/typed/storage/v1 256 | - kubernetes/typed/storage/v1alpha1 257 | - kubernetes/typed/storage/v1beta1 258 | - pkg/apis/clientauthentication 259 | - pkg/apis/clientauthentication/v1alpha1 260 | - pkg/version 261 | - plugin/pkg/client/auth/exec 262 | - rest 263 | - rest/watch 264 | - tools/auth 265 | - tools/clientcmd 266 | - tools/clientcmd/api 267 | - tools/clientcmd/api/latest 268 | - tools/clientcmd/api/v1 269 | - tools/metrics 270 | - tools/reference 271 | - transport 272 | - util/cert 273 | - util/flowcontrol 274 | - util/homedir 275 | - util/integer 276 | testImports: [] 277 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/target/portauthority 2 | import: 3 | - package: github.com/docker/distribution 4 | version: 83389a148052d74ac602f5f1d62f86ff2f3c4aa5 5 | subpackages: 6 | - context 7 | - manifest 8 | - manifest/schema1 9 | - manifest/schema2 10 | - reference 11 | - uuid 12 | - package: github.com/opencontainers/go-digest 13 | version: ^1.0.0-rc1 14 | - package: github.com/julienschmidt/httprouter 15 | version: ^1.1.0 16 | - package: github.com/lib/pq 17 | - package: github.com/pkg/errors 18 | version: ^0.8.0 19 | - package: github.com/prometheus/client_golang 20 | version: ^0.9.0-pre1 21 | subpackages: 22 | - prometheus 23 | - package: github.com/sirupsen/logrus 24 | version: 1.0.5 25 | - package: github.com/tylerb/graceful 26 | version: ^1.2.15 27 | - package: github.com/urfave/cli 28 | version: ^1.20.0 29 | - package: golang.org/x/oauth2 30 | subpackages: 31 | - google 32 | - package: gopkg.in/yaml.v2 33 | version: ^2.2.0 34 | - package: k8s.io/apimachinery 35 | subpackages: 36 | - pkg/apis/meta/v1 37 | - package: k8s.io/client-go 38 | subpackages: 39 | - api 40 | - kubernetes 41 | - tools/clientcmd 42 | - package: github.com/fatih/structs 43 | version: ^1.0.0 44 | -------------------------------------------------------------------------------- /imgs/ahab-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/portauthority/d1992f0d755920dc8a79d033bab1280a2fc11b4a/imgs/ahab-small.png -------------------------------------------------------------------------------- /imgs/ahab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/target/portauthority/d1992f0d755920dc8a79d033bab1280a2fc11b4a/imgs/ahab.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/target/portauthority/cmd" 9 | 10 | //this registers the db driver 11 | _ "github.com/target/portauthority/pkg/datastore/pgsql" 12 | ) 13 | 14 | var appVersion string 15 | 16 | func main() { 17 | app := cmd.App(appVersion) 18 | app.Run(os.Args) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /minikube/clair/clair/config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: clair-config 5 | data: 6 | config.yaml: | 7 | # Copyright 2015 clair authors 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | 21 | # The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined. 22 | clair: 23 | database: 24 | type: pgsql 25 | options: 26 | # PostgreSQL Connection string 27 | # http://www.postgresql.org/docs/9.4/static/libpq-connect.html 28 | source: host=clair-postgres-service port=5432 dbname=postgres user=postgres password=password sslmode=disable statement_timeout=60000 29 | # Number of elements kept in the cache 30 | # Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database. 31 | cacheSize: 16384 32 | 33 | api: 34 | # API server port 35 | port: 6060 36 | 37 | # Health server port 38 | # This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server. 39 | healthport: 6061 40 | 41 | # Deadline before an API request will respond with a 503 42 | timeout: 900s 43 | 44 | # 32-bit URL-safe base64 key used to encrypt pagination tokens 45 | # If one is not provided, it will be generated. 46 | # Multiple clair instances in the same cluster need the same value. 47 | paginationKey: 48 | 49 | # Optional PKI configuration 50 | # If you want to easily generate client certificates and CAs, try the following projects: 51 | # https://github.com/coreos/etcd-ca 52 | # https://github.com/cloudflare/cfssl 53 | servername: 54 | cafile: 55 | keyfile: 56 | certfile: 57 | 58 | updater: 59 | # Frequency the database will be updated with vulnerabilities from the default data sources 60 | # The value 0 disables the updater entirely. 61 | interval: 0h 62 | 63 | notifier: 64 | # Number of attempts before the notification is marked as failed to be sent 65 | attempts: 3 66 | 67 | # Duration before a failed notification is retried 68 | renotifyInterval: 2h 69 | 70 | http: 71 | # Optional endpoint that will receive notifications via POST requests 72 | endpoint: 73 | 74 | # Optional PKI configuration 75 | # If you want to easily generate client certificates and CAs, try the following projects: 76 | # https://github.com/cloudflare/cfssl 77 | # https://github.com/coreos/etcd-ca 78 | servername: 79 | cafile: 80 | keyfile: 81 | certfile: 82 | -------------------------------------------------------------------------------- /minikube/clair/clair/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: clair-deployment 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: clair-deployment 11 | spec: 12 | containers: 13 | - name: clair 14 | image: quay.io/coreos/clair:v2.0.1 15 | imagePullPolicy: IfNotPresent 16 | ports: 17 | - containerPort: 6060 18 | - containerPort: 6061 19 | env: 20 | - name: PGUSER 21 | value: postgres 22 | - name: PGPASSWORD 23 | value: password 24 | args: 25 | - "-config" 26 | - "/etc/config/config.yaml" 27 | - "--log-level=debug" 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/config 31 | readOnly: true 32 | livenessProbe: 33 | httpGet: 34 | path: /health 35 | port: 6061 36 | initialDelaySeconds: 30 37 | periodSeconds: 4 38 | timeoutSeconds: 2 39 | failureThreshold: 2 40 | volumes: 41 | - name: config-volume 42 | configMap: 43 | name: clair-config 44 | -------------------------------------------------------------------------------- /minikube/clair/clair/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: clair-service 6 | name: clair-service 7 | spec: 8 | type: NodePort 9 | ports: 10 | - protocol: TCP 11 | port: 6060 12 | targetPort: 6060 13 | nodePort: 32355 14 | name: clair-port0 15 | - port: 6061 16 | protocol: TCP 17 | targetPort: 6061 18 | nodePort: 30976 19 | name: clair-port1 20 | selector: 21 | app: clair-deployment 22 | -------------------------------------------------------------------------------- /minikube/clair/postgres/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/revision: "1" 6 | labels: 7 | run: clair-postgres-deployment 8 | name: clair-postgres-deployment 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | run: clair-postgres-deployment 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | run: clair-postgres-deployment 19 | spec: 20 | containers: 21 | - env: 22 | - name: POSTGRES_USER 23 | value: postgres 24 | - name: POSTGRES_PASSWORD 25 | value: password 26 | image: arminc/clair-db:latest 27 | imagePullPolicy: IfNotPresent 28 | name: postgres 29 | ports: 30 | - containerPort: 5432 31 | protocol: TCP 32 | -------------------------------------------------------------------------------- /minikube/clair/postgres/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | run: clair-postgres-service 6 | name: clair-postgres-service 7 | spec: 8 | type: NodePort 9 | ports: 10 | - port: 5432 11 | protocol: TCP 12 | targetPort: 5432 13 | nodePort: 30609 14 | selector: 15 | run: clair-postgres-deployment 16 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority-local/config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: portauthority-config 5 | data: 6 | config.yml: | 7 | # The values specified here are the default values that Port Authority uses if no configuration file is specified or if the keys are not defined. 8 | portauthority: 9 | database: 10 | # Database driver 11 | type: pgsql 12 | options: 13 | # PostgreSQL Connection string 14 | # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING 15 | 16 | # This can be the same or separate DB as your clair database. 17 | source: host=portauthority-postgres-service port=5432 sslmode=disable statement_timeout=60000 18 | 19 | # Number of elements kept in the cache 20 | # Values unlikely to change are cached in order to save prevent needless roundtrips to the database. 21 | cachesize: 16384 22 | api: 23 | # API server port 24 | port: 6100 25 | 26 | # Health server port 27 | # This is an unencrypted endpoint useful for load balancers to check to healthiness of the port authority server. 28 | healthport: 6101 29 | 30 | # Deadline before an API request will respond with a 503 31 | timeout: 900s 32 | 33 | # Setting imagewebhookdefaultblock to true will set the imagewebhooks endpoint default behavior to block any images with policy violations. 34 | # If it is set to false, a user can change enable the behavior by setting the portauthority-webhook anotation to true 35 | imagewebhookdefaultblock: false 36 | 37 | # URL of the Clair server Port Authority sends it's images/layers for scanning 38 | # If running Clair is running in Minikube, change the path to the advertised service (e.g. http://clair-service:6100) 39 | clairurl: http://clair-service:6060 40 | clairtimeout: 900 41 | 42 | ### Environment variables defined below are mapped to credentials used by the Kubneretes Crawler API (/v1/crawler/k8s) 43 | ### A 'Scan: true' flag will invoke their usage 44 | k8scrawlcredentials: 45 | - url: "docker.io" #basic auth is empty UN and PW 46 | username: "" 47 | password: "" 48 | - url: "gcr.io" #basic auth is empty UN and PW 49 | username: "" 50 | password: "" 51 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority-local/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: portauthority-deployment 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: portauthority-deployment 11 | spec: 12 | containers: 13 | - name: portauthority 14 | image: portauthority:latest 15 | imagePullPolicy: IfNotPresent 16 | ports: 17 | - containerPort: 6100 18 | - containerPort: 6101 19 | env: 20 | - name: PGUSER 21 | value: postgres 22 | - name: PGPASSWORD 23 | value: password 24 | command: 25 | - "portauthority" 26 | - "serve" 27 | - "-c" 28 | - "/config/config.yml" 29 | - "-l" 30 | - "debug" 31 | volumeMounts: 32 | - name: config-volume 33 | mountPath: /config 34 | readOnly: true 35 | livenessProbe: 36 | httpGet: 37 | path: /health 38 | port: 6101 39 | initialDelaySeconds: 10 40 | periodSeconds: 8 41 | timeoutSeconds: 4 42 | failureThreshold: 4 43 | readinessProbe: 44 | httpGet: 45 | path: /health 46 | port: 6101 47 | initialDelaySeconds: 10 48 | periodSeconds: 8 49 | timeoutSeconds: 4 50 | failureThreshold: 4 51 | volumes: 52 | - name: config-volume 53 | configMap: 54 | name: portauthority-config 55 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority-local/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: portauthority-service 6 | name: portauthority-service 7 | spec: 8 | type: NodePort 9 | ports: 10 | - protocol: TCP 11 | port: 6100 12 | targetPort: 6100 13 | nodePort: 31700 14 | name: portauthority-port0 15 | - port: 6101 16 | protocol: TCP 17 | targetPort: 6101 18 | nodePort: 31701 19 | name: portauthority-port1 20 | selector: 21 | app: portauthority-deployment 22 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority/config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: portauthority-config 5 | data: 6 | config.yml: | 7 | # The values specified here are the default values that Port Authority uses if no configuration file is specified or if the keys are not defined. 8 | portauthority: 9 | database: 10 | # Database driver 11 | type: pgsql 12 | options: 13 | # PostgreSQL Connection string 14 | # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING 15 | 16 | # This can be the same or separate DB as your clair database. 17 | source: host=portauthority-postgres-service port=5432 sslmode=disable statement_timeout=60000 18 | 19 | # Number of elements kept in the cache 20 | # Values unlikely to change are cached in order to save prevent needless roundtrips to the database. 21 | cachesize: 16384 22 | api: 23 | # API server port 24 | port: 6100 25 | 26 | # Health server port 27 | # This is an unencrypted endpoint useful for load balancers to check to healthiness of the port authority server. 28 | healthport: 6101 29 | 30 | # Deadline before an API request will respond with a 503 31 | timeout: 900s 32 | 33 | # Setting imagewebhookdefaultblock to true will set the imagewebhooks endpoint default behavior to block any images with policy violations. 34 | # If it is set to false, a user can change enable the behavior by setting the portauthority-webhook anotation to true 35 | imagewebhookdefaultblock: false 36 | 37 | # URL of the Clair server Port Authority sends it's images/layers for scanning 38 | # If running Clair is running in Minikube, change the path to the advertised service (e.g. http://clair-service:6100) 39 | clairurl: http://clair-service:6060 40 | clairtimeout: 900 41 | 42 | ### Environment variables defined below are mapped to credentials used by the Kubneretes Crawler API (/v1/crawler/k8s) 43 | ### A 'Scan: true' flag will invoke their usage 44 | k8scrawlcredentials: 45 | - url: "docker.io" #basic auth is empty UN and PW 46 | username: "" 47 | password: "" 48 | - url: "gcr.io" #basic auth is empty UN and PW 49 | username: "" 50 | password: "" 51 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: portauthority-deployment 5 | spec: 6 | replicas: 1 7 | template: 8 | metadata: 9 | labels: 10 | app: portauthority-deployment 11 | spec: 12 | containers: 13 | - name: portauthority 14 | image: target/portauthority:latest 15 | imagePullPolicy: Always 16 | ports: 17 | - containerPort: 6100 18 | - containerPort: 6101 19 | env: 20 | - name: PGUSER 21 | value: postgres 22 | - name: PGPASSWORD 23 | value: password 24 | command: 25 | - "portauthority" 26 | - "serve" 27 | - "-c" 28 | - "/config/config.yml" 29 | - "-l" 30 | - "debug" 31 | volumeMounts: 32 | - name: config-volume 33 | mountPath: /config 34 | readOnly: true 35 | livenessProbe: 36 | httpGet: 37 | path: /health 38 | port: 6101 39 | initialDelaySeconds: 10 40 | periodSeconds: 8 41 | timeoutSeconds: 4 42 | failureThreshold: 4 43 | readinessProbe: 44 | httpGet: 45 | path: /health 46 | port: 6101 47 | initialDelaySeconds: 10 48 | periodSeconds: 8 49 | timeoutSeconds: 4 50 | failureThreshold: 4 51 | volumes: 52 | - name: config-volume 53 | configMap: 54 | name: portauthority-config 55 | -------------------------------------------------------------------------------- /minikube/portauthority/portauthority/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | name: portauthority-service 6 | name: portauthority-service 7 | spec: 8 | type: NodePort 9 | ports: 10 | - protocol: TCP 11 | port: 6100 12 | targetPort: 6100 13 | nodePort: 31700 14 | name: portauthority-port0 15 | - port: 6101 16 | protocol: TCP 17 | targetPort: 6101 18 | nodePort: 31701 19 | name: portauthority-port1 20 | selector: 21 | app: portauthority-deployment 22 | -------------------------------------------------------------------------------- /minikube/portauthority/postgres/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/revision: "1" 6 | labels: 7 | run: portauthority-postgres-deployment 8 | name: portauthority-postgres-deployment 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | run: portauthority-postgres-deployment 14 | template: 15 | metadata: 16 | creationTimestamp: null 17 | labels: 18 | run: portauthority-postgres-deployment 19 | spec: 20 | containers: 21 | - env: 22 | - name: POSTGRES_USER 23 | value: postgres 24 | - name: POSTGRES_PASSWORD 25 | value: password 26 | image: postgres:9.6 27 | imagePullPolicy: IfNotPresent 28 | name: postgres 29 | ports: 30 | - containerPort: 5432 31 | protocol: TCP 32 | -------------------------------------------------------------------------------- /minikube/portauthority/postgres/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | run: portauthority-postgres-service 6 | name: portauthority-postgres-service 7 | spec: 8 | type: NodePort 9 | ports: 10 | - port: 5432 11 | protocol: TCP 12 | targetPort: 5432 13 | nodePort: 30607 14 | selector: 15 | run: portauthority-postgres-deployment 16 | -------------------------------------------------------------------------------- /pkg/clair/clair.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clair 4 | 5 | import ( 6 | "crypto/md5" 7 | "encoding/hex" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/target/portauthority/pkg/clair/client" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Image struct init 18 | type Image struct { 19 | Digest string `json:"digest"` 20 | Registry string `json:"registry"` 21 | Repo string `json:"repo"` 22 | Tag string `json:"tag"` 23 | Layers []string `json:"layers"` 24 | Headers map[string]string 25 | } 26 | 27 | // Push will push an image's layers into Clair 28 | // Headers are passed to Clair and reused on requests it makes to registries 29 | func Push(client *clairclient.Client, image Image) error { 30 | parent := "" 31 | 32 | // We need to go in reverse order of the layers obtained from the manifest 33 | // The last layer in the list is the base parent layer 34 | for i := len(image.Layers) - 1; i >= 0; i-- { 35 | 36 | // Append the image id to the layer to create a unqiue value that allows us 37 | // to maintain the parent/child relationships. 38 | // As a result of doing this, we download each image at least once, but the 39 | // false postitive detection is worth it. 40 | if parent != "" { 41 | parent = strings.Join([]string{string(image.Digest), parent}, "") 42 | parent = GetMD5Hash(parent) 43 | } 44 | 45 | log.Debug("Clair Precalculated Hash: ", image.Digest, image.Layers[i]) 46 | layer := strings.Join([]string{string(image.Digest), image.Layers[i]}, "") 47 | layer = GetMD5Hash(layer) 48 | 49 | le, err := client.PostLayers(&clairclient.Layer{ 50 | Name: layer, 51 | ParentName: parent, 52 | Path: strings.Join([]string{image.Registry, "v2", image.Repo, "blobs", image.Layers[i]}, "/"), 53 | Format: "Docker", 54 | Headers: map[string]string{"Authorization": image.Headers["Authorization"]}, 55 | }) 56 | if err != nil { 57 | return errors.Wrapf(err, "error pushing layer %s to clair", image.Layers[i]) 58 | } 59 | 60 | if le.Error != nil { 61 | log.Error("Error: \n", le.Error.Message) 62 | } else { 63 | log.Debug("Name: ", le.Layer.Name) 64 | log.Debug("Parent: ", le.Layer.ParentName) 65 | log.Debug("Indexed by Version: ", le.Layer.IndexedByVersion) 66 | } 67 | 68 | // Port Authority keeps the manifest record so we can put things back 69 | // together in the right order. 70 | parent = image.Layers[i] 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // Get func init 77 | func Get(client clairclient.Client, image Image) ([]*clairclient.LayerEnvelope, error) { 78 | var layers []*clairclient.LayerEnvelope 79 | for _, layer := range image.Layers { 80 | le, err := client.GetLayers(layer, false, false) 81 | if err != nil { 82 | if clairclient.IsStatusCodeError(err) && clairclient.ErrorStatusCode(err) == 404 { 83 | return nil, errors.Wrapf(err, "error getting data for layer %s in image %s/%s:%s", layer, image.Registry, image.Repo, image.Tag) 84 | } 85 | } 86 | layers = append(layers, le) 87 | } 88 | 89 | return layers, nil 90 | } 91 | 92 | // GetMD5Hash gets the MD5 hash of a string 93 | func GetMD5Hash(text string) string { 94 | hasher := md5.New() 95 | hasher.Write([]byte(text)) 96 | return hex.EncodeToString(hasher.Sum(nil)) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/clair/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/json" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Config for Clair Client 18 | type Config struct { 19 | Address string 20 | HTTPClient *http.Client 21 | } 22 | 23 | // TLSConfig for Clair Client 24 | type TLSConfig struct { 25 | CaCert string 26 | Insecure bool 27 | } 28 | 29 | // DefaultConfig will return a client configuration with default values 30 | func DefaultConfig() *Config { 31 | var config *Config 32 | 33 | transport := &http.Transport{ 34 | TLSHandshakeTimeout: 10 * time.Second, 35 | TLSClientConfig: &tls.Config{ 36 | MinVersion: tls.VersionTLS12, 37 | }, 38 | } 39 | 40 | config = &Config{ 41 | Address: "http://127.0.0.1:6060", 42 | HTTPClient: &http.Client{ 43 | Transport: transport, 44 | Timeout: time.Second * 10, 45 | }, 46 | } 47 | 48 | return config 49 | } 50 | 51 | // ConfigureTLS will apply the provided tlsconfig to the config 52 | func (c *Config) ConfigureTLS(tc *TLSConfig) error { 53 | clientTLSConfig := c.HTTPClient.Transport.(*http.Transport).TLSClientConfig 54 | 55 | if tc.CaCert != "" { 56 | caCert, err := ioutil.ReadFile(tc.CaCert) 57 | if err != nil { 58 | return errors.Wrap(err, "failed to read cacert file") 59 | } 60 | certPool := x509.NewCertPool() 61 | certPool.AppendCertsFromPEM(caCert) 62 | clientTLSConfig.RootCAs = certPool 63 | } 64 | 65 | clientTLSConfig.InsecureSkipVerify = tc.Insecure 66 | 67 | return nil 68 | } 69 | 70 | // Client struct 71 | type Client struct { 72 | addr *url.URL 73 | config *Config 74 | } 75 | 76 | // NewClient Sets up a new Clair client 77 | func NewClient(c *Config) (*Client, error) { 78 | if nil == c { 79 | c = DefaultConfig() 80 | } 81 | 82 | u, err := url.Parse(c.Address) 83 | if err != nil { 84 | return nil, errors.Wrap(err, "parsing url failed") 85 | } 86 | 87 | if c.HTTPClient == nil { 88 | c.HTTPClient = DefaultConfig().HTTPClient 89 | } 90 | 91 | client := &Client{ 92 | addr: u, 93 | config: c, 94 | } 95 | 96 | return client, nil 97 | } 98 | 99 | // Request builds the standard request to Clair 100 | func (c *Client) Request(r *Request) (*http.Response, error) { 101 | var req *http.Request 102 | var result *http.Response 103 | var err error 104 | 105 | req, err = r.HTTPReq() 106 | if err != nil { 107 | return nil, errors.Wrap(err, "creating http request from logical request failed") 108 | } 109 | 110 | result, err = c.config.HTTPClient.Do(req) 111 | if err != nil { 112 | return nil, errors.Wrap(err, "doing http request failed") 113 | } 114 | 115 | return result, nil 116 | } 117 | 118 | // DecodeJSONBody decodes the body from a response into the provided interface 119 | func DecodeJSONBody(resp *http.Response, out interface{}) error { 120 | var dec *json.Decoder 121 | 122 | if nil == resp.Body { 123 | return errors.New("Response body is nil") 124 | } 125 | 126 | if nil == out { 127 | return errors.New("Output interface is nil") 128 | } 129 | 130 | dec = json.NewDecoder(resp.Body) 131 | 132 | return dec.Decode(out) 133 | } 134 | -------------------------------------------------------------------------------- /pkg/clair/client/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import "fmt" 6 | 7 | type statusCodeErrorInt interface { 8 | statusCode() int 9 | } 10 | 11 | type statusCodeError struct { 12 | code int 13 | error 14 | } 15 | 16 | func (e statusCodeError) statusCode() int { 17 | return e.code 18 | } 19 | 20 | func (e statusCodeError) Error() string { 21 | return fmt.Sprintf("recieved unexpected response status code %d", e.code) 22 | } 23 | 24 | func newStatusCodeError(code int) statusCodeError { 25 | return statusCodeError{ 26 | code: code, 27 | } 28 | } 29 | 30 | // IsStatusCodeError func init 31 | func IsStatusCodeError(err error) bool { 32 | _, ok := err.(statusCodeErrorInt) 33 | return ok 34 | } 35 | 36 | // ErrorStatusCode func init 37 | func ErrorStatusCode(err error) int { 38 | e, ok := err.(statusCodeError) 39 | if ok { 40 | return e.statusCode() 41 | } 42 | return -1 43 | } 44 | -------------------------------------------------------------------------------- /pkg/clair/client/fixes.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // GetFixes func init 14 | func (c *Client) GetFixes(nspace, vuln string) (*FeatureEnvelope, error) { 15 | reqURL := &url.URL{ 16 | Scheme: c.addr.Scheme, 17 | Host: c.addr.Host, 18 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s/fixes", nspace, vuln), 19 | } 20 | 21 | req := &Request{ 22 | Method: "GET", 23 | URL: reqURL, 24 | Params: make(map[string][]string), 25 | } 26 | 27 | resp, err := c.Request(req) 28 | if err != nil { 29 | return nil, errors.Wrap(err, "error performing request") 30 | } 31 | defer resp.Body.Close() 32 | 33 | if resp.StatusCode != http.StatusOK { 34 | return nil, newStatusCodeError(resp.StatusCode) 35 | } 36 | 37 | fe := &FeatureEnvelope{} 38 | err = DecodeJSONBody(resp, fe) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "error parsing response") 41 | } 42 | 43 | return fe, nil 44 | } 45 | 46 | // PutFixes func init 47 | func (c *Client) PutFixes(vuln string, feat *Feature) (*FeatureEnvelope, error) { 48 | reqURL := &url.URL{ 49 | Scheme: c.addr.Scheme, 50 | Host: c.addr.Host, 51 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s/fixes/%s", feat.NamespaceName, vuln, feat.Name), 52 | } 53 | 54 | req := &Request{ 55 | Method: "PUT", 56 | URL: reqURL, 57 | Params: make(map[string][]string), 58 | } 59 | 60 | err := req.JSONBody(&FeatureEnvelope{Feature: feat}) 61 | if err != nil { 62 | return nil, errors.Wrap(err, "error marshaling json body") 63 | } 64 | 65 | resp, err := c.Request(req) 66 | if err != nil { 67 | return nil, errors.Wrap(err, "error performing request") 68 | } 69 | defer resp.Body.Close() 70 | 71 | if resp.StatusCode != http.StatusOK { 72 | return nil, newStatusCodeError(resp.StatusCode) 73 | } 74 | 75 | fe := &FeatureEnvelope{} 76 | err = DecodeJSONBody(resp, fe) 77 | if err != nil { 78 | return nil, errors.Wrap(err, "error parsing response") 79 | } 80 | 81 | return fe, nil 82 | } 83 | 84 | // DeleteFixes func init 85 | func (c *Client) DeleteFixes(nspace, vuln, feat string) error { 86 | reqURL := &url.URL{ 87 | Scheme: c.addr.Scheme, 88 | Host: c.addr.Host, 89 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s/fixes/%s", nspace, vuln, feat), 90 | } 91 | 92 | req := &Request{ 93 | Method: "DELETE", 94 | URL: reqURL, 95 | Params: make(map[string][]string), 96 | } 97 | 98 | resp, err := c.Request(req) 99 | if err != nil { 100 | return errors.Wrap(err, "error performing request") 101 | } 102 | defer resp.Body.Close() 103 | 104 | if resp.StatusCode != http.StatusOK { 105 | return newStatusCodeError(resp.StatusCode) 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /pkg/clair/client/layers.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "fmt" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Error struct init 15 | type Error struct { 16 | Message string `json:"Message,omitempty"` 17 | } 18 | 19 | // Layer struct init 20 | type Layer struct { 21 | Name string `json:"Name,omitempty"` 22 | NamespaceName string `json:"NamespaceName,omitempty"` 23 | Path string `json:"Path,omitempty"` 24 | Headers map[string]string `json:"Headers,omitempty"` 25 | ParentName string `json:"ParentName,omitempty"` 26 | Format string `json:"Format,omitempty"` 27 | IndexedByVersion int `json:"IndexedByVersion,omitempty"` 28 | Features []Feature `json:"Features,omitempty"` 29 | } 30 | 31 | // Feature struct init 32 | type Feature struct { 33 | Name string `json:"Name,omitempty"` 34 | NamespaceName string `json:"NamespaceName,omitempty"` 35 | VersionFormat string `json:"VersionFormat,omitempty"` 36 | Version string `json:"Version,omitempty"` 37 | Vulnerabilities []Vulnerability `json:"Vulnerabilities,omitempty"` 38 | AddedBy string `json:"AddedBy,omitempty"` 39 | } 40 | 41 | // OrderedLayerName struct init 42 | type OrderedLayerName struct { 43 | Index int `json:"Index"` 44 | LayerName string `json:"LayerName"` 45 | } 46 | 47 | // LayerEnvelope struct init 48 | type LayerEnvelope struct { 49 | Layer *Layer `json:"Layer,omitempty"` 50 | Error *Error `json:"Error,omitempty"` 51 | } 52 | 53 | // FeatureEnvelope struct init 54 | type FeatureEnvelope struct { 55 | Feature *Feature `json:"Feature,omitempty"` 56 | Features *[]Feature `json:"Features,omitempty"` 57 | Error *Error `json:"Error,omitempty"` 58 | } 59 | 60 | // PostLayers func init 61 | func (c *Client) PostLayers(layer *Layer) (*LayerEnvelope, error) { 62 | 63 | reqURL := &url.URL{ 64 | Scheme: c.addr.Scheme, 65 | Host: c.addr.Host, 66 | Path: "/v1/layers", 67 | } 68 | 69 | req := &Request{ 70 | Method: "POST", 71 | URL: reqURL, 72 | Params: make(map[string][]string), 73 | } 74 | 75 | le := &LayerEnvelope{ 76 | Layer: layer, 77 | } 78 | 79 | err := req.JSONBody(le) 80 | if err != nil { 81 | return nil, errors.Wrap(err, "error marshaling json body") 82 | } 83 | 84 | resp, err := c.Request(req) 85 | if err != nil { 86 | return nil, errors.Wrap(err, "error performing request") 87 | } 88 | defer resp.Body.Close() 89 | 90 | if resp.StatusCode != http.StatusCreated { 91 | return nil, newStatusCodeError(resp.StatusCode) 92 | } 93 | 94 | respLE := &LayerEnvelope{} 95 | err = DecodeJSONBody(resp, respLE) 96 | if err != nil { 97 | return nil, errors.Wrap(err, "error parsing response") 98 | } 99 | 100 | return respLE, nil 101 | } 102 | 103 | // GetLayers func init 104 | func (c *Client) GetLayers(name string, withFeatures, withVulnerabilities bool) (*LayerEnvelope, error) { 105 | 106 | path := fmt.Sprintf("/v1/layers/%s", name) 107 | parms := url.Values{} 108 | 109 | // If vulnerabilites exits, it automatically returns features 110 | if withVulnerabilities { 111 | parms.Add("vulnerabilities", "") 112 | } else if withFeatures { 113 | parms.Add("features", "") 114 | } 115 | 116 | reqURL := &url.URL{ 117 | Scheme: c.addr.Scheme, 118 | Host: c.addr.Host, 119 | Path: path, 120 | } 121 | 122 | req := &Request{ 123 | Method: "GET", 124 | URL: reqURL, 125 | Params: parms, 126 | } 127 | 128 | resp, err := c.Request(req) 129 | if err != nil { 130 | return nil, errors.Wrap(err, "error performing request") 131 | } 132 | defer resp.Body.Close() 133 | 134 | if resp.StatusCode != http.StatusOK { 135 | return nil, newStatusCodeError(resp.StatusCode) 136 | } 137 | 138 | respLE := &LayerEnvelope{} 139 | err = DecodeJSONBody(resp, respLE) 140 | if err != nil { 141 | return nil, errors.Wrap(err, "error parsing response") 142 | } 143 | 144 | return respLE, nil 145 | } 146 | 147 | // DeleteLayers func init 148 | func (c *Client) DeleteLayers(name string) error { 149 | reqURL := &url.URL{ 150 | Scheme: c.addr.Scheme, 151 | Host: c.addr.Host, 152 | Path: fmt.Sprintf("/v1/layers/%s", name), 153 | } 154 | 155 | req := &Request{ 156 | Method: "DELETE", 157 | URL: reqURL, 158 | Params: make(map[string][]string), 159 | } 160 | 161 | resp, err := c.Request(req) 162 | if err != nil { 163 | return errors.Wrap(err, "error performing request") 164 | } 165 | defer resp.Body.Close() 166 | 167 | if resp.StatusCode != http.StatusOK { 168 | return newStatusCodeError(resp.StatusCode) 169 | } 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /pkg/clair/client/namespaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Namespace struct init 13 | type Namespace struct { 14 | Name string `json:"Name,omitempty"` 15 | VersionFormat string `json:"VersionFormat,omitempty"` 16 | } 17 | 18 | // NamespaceEnvelope struct init 19 | type NamespaceEnvelope struct { 20 | Namespaces *[]Namespace `json:"Namespaces,omitempty"` 21 | Error *Error `json:"Error,omitempty"` 22 | } 23 | 24 | // GetNamespaces func init 25 | func (c *Client) GetNamespaces() (*NamespaceEnvelope, error) { 26 | reqURL := &url.URL{ 27 | Scheme: c.addr.Scheme, 28 | Host: c.addr.Host, 29 | Path: "/v1/namespaces", 30 | } 31 | 32 | req := &Request{ 33 | Method: "GET", 34 | URL: reqURL, 35 | Params: make(map[string][]string), 36 | } 37 | 38 | resp, err := c.Request(req) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "error performing request") 41 | } 42 | defer resp.Body.Close() 43 | 44 | if resp.StatusCode != http.StatusOK { 45 | return nil, newStatusCodeError(resp.StatusCode) 46 | } 47 | 48 | ne := &NamespaceEnvelope{} 49 | err = DecodeJSONBody(resp, ne) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "error parsing response") 52 | } 53 | 54 | return ne, nil 55 | } 56 | -------------------------------------------------------------------------------- /pkg/clair/client/notifications.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "net/http" 7 | "net/url" 8 | 9 | "fmt" 10 | 11 | "strconv" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Notification struct init 17 | type Notification struct { 18 | Name string `json:"Name,omitempty"` 19 | Created string `json:"Created,omitempty"` 20 | Notified string `json:"Notified,omitempty"` 21 | Deleted string `json:"Deleted,omitempty"` 22 | Limit int `json:"Limit,omitempty"` 23 | Page string `json:"Page,omitempty"` 24 | NextPage string `json:"NextPage,omitempty"` 25 | Old *VulnerabilityWithLayers `json:"Old,omitempty"` 26 | New *VulnerabilityWithLayers `json:"New,omitempty"` 27 | } 28 | 29 | // NotificationEnvelope struct init 30 | type NotificationEnvelope struct { 31 | Notification *Notification `json:"Notification,omitempty"` 32 | Error *Error `json:"Error,omitempty"` 33 | } 34 | 35 | // GetNotifications func init 36 | func (c *Client) GetNotifications(name, page string, limit int) (*NotificationEnvelope, error) { 37 | 38 | params := make(map[string][]string) 39 | if page != "" { 40 | params["page"] = []string{page} 41 | } 42 | 43 | if limit != 0 { 44 | params["limit"] = []string{strconv.Itoa(limit)} 45 | } 46 | reqURL := &url.URL{ 47 | Scheme: c.addr.Scheme, 48 | Host: c.addr.Host, 49 | Path: fmt.Sprintf("/v1/notifications/%s", name), 50 | } 51 | 52 | req := &Request{ 53 | Method: "GET", 54 | URL: reqURL, 55 | Params: make(map[string][]string), 56 | } 57 | 58 | resp, err := c.Request(req) 59 | if err != nil { 60 | return nil, errors.Wrap(err, "error performing request") 61 | } 62 | defer resp.Body.Close() 63 | 64 | if resp.StatusCode != http.StatusOK { 65 | return nil, newStatusCodeError(resp.StatusCode) 66 | } 67 | 68 | ne := &NotificationEnvelope{} 69 | err = DecodeJSONBody(resp, ne) 70 | if err != nil { 71 | return nil, errors.Wrap(err, "error parsing response") 72 | } 73 | 74 | return ne, nil 75 | } 76 | 77 | // DeleteNotifications func init 78 | func (c *Client) DeleteNotifications(name string) error { 79 | reqURL := &url.URL{ 80 | Scheme: c.addr.Scheme, 81 | Host: c.addr.Host, 82 | Path: fmt.Sprintf("/v1/notifications/%s", name), 83 | } 84 | 85 | req := &Request{ 86 | Method: "DELETE", 87 | URL: reqURL, 88 | Params: make(map[string][]string), 89 | } 90 | 91 | resp, err := c.Request(req) 92 | if err != nil { 93 | return errors.Wrap(err, "error performing request") 94 | } 95 | defer resp.Body.Close() 96 | 97 | if resp.StatusCode != http.StatusOK { 98 | return newStatusCodeError(resp.StatusCode) 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/clair/client/request.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Request struct init 16 | type Request struct { 17 | Method string 18 | URL *url.URL 19 | Params url.Values 20 | Body io.Reader 21 | BodySize int64 22 | } 23 | 24 | // JSONBody sets the request's body to the json encoded value 25 | func (r *Request) JSONBody(val interface{}) error { 26 | var buff *bytes.Buffer 27 | var enc *json.Encoder 28 | var err error 29 | 30 | buff = bytes.NewBuffer(nil) 31 | enc = json.NewEncoder(buff) 32 | 33 | err = enc.Encode(val) 34 | if err != nil { 35 | return errors.Wrap(err, "json encoding failed") 36 | } 37 | 38 | r.Body = buff 39 | r.BodySize = int64(buff.Len()) 40 | return nil 41 | } 42 | 43 | // RawBody func init 44 | func (r *Request) RawBody(raw []byte) error { 45 | buff := bytes.NewBuffer(raw) 46 | 47 | r.Body = buff 48 | r.BodySize = int64(buff.Len()) 49 | 50 | return nil 51 | } 52 | 53 | // HTTPReq func init 54 | func (r *Request) HTTPReq() (*http.Request, error) { 55 | var req *http.Request 56 | r.URL.RawQuery = r.Params.Encode() 57 | 58 | req, err := http.NewRequest(r.Method, r.URL.String(), r.Body) 59 | if err != nil { 60 | return nil, errors.Wrap(err, "creating http request failed") 61 | } 62 | return req, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/clair/client/vulnerabilities.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package clairclient 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Vulnerability struct init 14 | type Vulnerability struct { 15 | Name string `json:"Name,omitempty"` 16 | NamespaceName string `json:"NamespaceName,omitempty"` 17 | Description string `json:"Description,omitempty"` 18 | Link string `json:"Link,omitempty"` 19 | Severity string `json:"Severity,omitempty"` 20 | Metadata map[string]interface{} `json:"Metadata,omitempty"` 21 | FixedBy string `json:"FixedBy,omitempty"` 22 | FixedIn []Feature `json:"FixedIn,omitempty"` 23 | } 24 | 25 | // VulnerabilityWithLayers struct init 26 | type VulnerabilityWithLayers struct { 27 | Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` 28 | 29 | // This field is guaranteed to be in order only for pagination. 30 | // Indices from different notifications may not be comparable. 31 | OrderedLayersIntroducingVulnerability []OrderedLayerName `json:"OrderedLayersIntroducingVulnerability,omitempty"` 32 | 33 | // This field is deprecated. 34 | LayersIntroducingVulnerability []string `json:"LayersIntroducingVulnerability,omitempty"` 35 | } 36 | 37 | // VulnerabilityEnvelope struct init 38 | type VulnerabilityEnvelope struct { 39 | Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` 40 | Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"` 41 | NextPage string `json:"NextPage,omitempty"` 42 | Error *Error `json:"Error,omitempty"` 43 | } 44 | 45 | // GetVulnerabilities init 46 | func (c *Client) GetVulnerabilities(nspace string) (*VulnerabilityEnvelope, error) { 47 | reqURL := &url.URL{ 48 | Scheme: c.addr.Scheme, 49 | Host: c.addr.Host, 50 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities", nspace), 51 | } 52 | 53 | req := &Request{ 54 | Method: "GET", 55 | URL: reqURL, 56 | Params: make(map[string][]string), 57 | } 58 | 59 | resp, err := c.Request(req) 60 | if err != nil { 61 | return nil, errors.Wrap(err, "error performing request") 62 | } 63 | defer resp.Body.Close() 64 | 65 | if resp.StatusCode != http.StatusOK { 66 | return nil, newStatusCodeError(resp.StatusCode) 67 | } 68 | 69 | ve := &VulnerabilityEnvelope{} 70 | err = DecodeJSONBody(resp, ve) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "error parsing response") 73 | } 74 | 75 | return ve, nil 76 | } 77 | 78 | // PostVulnerabilities init 79 | func (c *Client) PostVulnerabilities(vuln *Vulnerability) (*VulnerabilityEnvelope, error) { 80 | reqURL := &url.URL{ 81 | Scheme: c.addr.Scheme, 82 | Host: c.addr.Host, 83 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities", vuln.NamespaceName), 84 | } 85 | 86 | req := &Request{ 87 | Method: "POST", 88 | URL: reqURL, 89 | Params: make(map[string][]string), 90 | } 91 | 92 | err := req.JSONBody(&VulnerabilityEnvelope{Vulnerability: vuln}) 93 | if err != nil { 94 | return nil, errors.Wrap(err, "error marshaling json body") 95 | } 96 | 97 | resp, err := c.Request(req) 98 | if err != nil { 99 | return nil, errors.Wrap(err, "error performing request") 100 | } 101 | defer resp.Body.Close() 102 | 103 | if resp.StatusCode != http.StatusCreated { 104 | return nil, newStatusCodeError(resp.StatusCode) 105 | } 106 | 107 | respVE := &VulnerabilityEnvelope{} 108 | err = DecodeJSONBody(resp, respVE) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "error parsing response") 111 | } 112 | 113 | return respVE, nil 114 | } 115 | 116 | // GetVulnerability init 117 | func (c *Client) GetVulnerability(nspace, vuln string) (*VulnerabilityEnvelope, error) { 118 | reqURL := &url.URL{ 119 | Scheme: c.addr.Scheme, 120 | Host: c.addr.Host, 121 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s", nspace, vuln), 122 | } 123 | 124 | req := &Request{ 125 | Method: "GET", 126 | URL: reqURL, 127 | Params: make(map[string][]string), 128 | } 129 | 130 | resp, err := c.Request(req) 131 | if err != nil { 132 | return nil, errors.Wrap(err, "error performing request") 133 | } 134 | defer resp.Body.Close() 135 | 136 | if resp.StatusCode != http.StatusOK { 137 | return nil, newStatusCodeError(resp.StatusCode) 138 | } 139 | 140 | ve := &VulnerabilityEnvelope{} 141 | err = DecodeJSONBody(resp, ve) 142 | if err != nil { 143 | return nil, errors.Wrap(err, "error parsing response") 144 | } 145 | 146 | return ve, nil 147 | } 148 | 149 | // PutVulnerbaility init 150 | func (c *Client) PutVulnerbaility(vuln *Vulnerability) (*VulnerabilityEnvelope, error) { 151 | reqURL := &url.URL{ 152 | Scheme: c.addr.Scheme, 153 | Host: c.addr.Host, 154 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s", vuln.NamespaceName, vuln.Name), 155 | } 156 | 157 | req := &Request{ 158 | Method: "PUT", 159 | URL: reqURL, 160 | Params: make(map[string][]string), 161 | } 162 | 163 | err := req.JSONBody(vuln) 164 | if err != nil { 165 | return nil, errors.Wrap(err, "error marshaling json bodyy") 166 | } 167 | 168 | resp, err := c.Request(req) 169 | if err != nil { 170 | return nil, errors.Wrap(err, "error performing request") 171 | } 172 | defer resp.Body.Close() 173 | 174 | if resp.StatusCode != http.StatusOK { 175 | return nil, newStatusCodeError(resp.StatusCode) 176 | } 177 | 178 | ve := &VulnerabilityEnvelope{} 179 | err = DecodeJSONBody(resp, ve) 180 | if err != nil { 181 | return nil, errors.Wrap(err, "error parsing response") 182 | } 183 | 184 | return ve, nil 185 | } 186 | 187 | // DeleteVulnerbaility func init 188 | func (c *Client) DeleteVulnerbaility(nspace, vuln string) error { 189 | reqURL := &url.URL{ 190 | Scheme: c.addr.Scheme, 191 | Host: c.addr.Host, 192 | Path: fmt.Sprintf("/v1/namespaces/%s/vulnerabilities/%s", nspace, vuln), 193 | } 194 | 195 | req := &Request{ 196 | Method: "DELETE", 197 | URL: reqURL, 198 | Params: make(map[string][]string), 199 | } 200 | 201 | resp, err := c.Request(req) 202 | if err != nil { 203 | return errors.Wrap(err, "error performing request") 204 | } 205 | defer resp.Body.Close() 206 | 207 | if resp.StatusCode != http.StatusOK { 208 | return newStatusCodeError(resp.StatusCode) 209 | } 210 | 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /pkg/commonerr/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package commonerr defines reusable error types common throughout the Port 16 | // Authority codebase. 17 | package commonerr 18 | 19 | import "errors" 20 | 21 | var ( 22 | // ErrNotFound occurs when a resource could not be found 23 | ErrNotFound = errors.New("the resource cannot be found") 24 | ) 25 | 26 | // ErrBadRequest occurs when a method has been passed an inappropriate argument 27 | type ErrBadRequest struct { 28 | s string 29 | } 30 | 31 | // NewBadRequestError instantiates a ErrBadRequest with the specified message 32 | func NewBadRequestError(message string) error { 33 | return &ErrBadRequest{s: message} 34 | } 35 | 36 | func (e *ErrBadRequest) Error() string { 37 | return e.s 38 | } 39 | -------------------------------------------------------------------------------- /pkg/crawler/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package crawler 4 | 5 | import ( 6 | "crypto/md5" 7 | "encoding/hex" 8 | "fmt" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | log "github.com/sirupsen/logrus" 15 | 16 | "github.com/pkg/errors" 17 | 18 | "github.com/target/portauthority/pkg/clair" 19 | "github.com/target/portauthority/pkg/clair/client" 20 | "github.com/target/portauthority/pkg/datastore" 21 | "github.com/target/portauthority/pkg/docker" 22 | ) 23 | 24 | // RegCrawler configuration struct 25 | type RegCrawler struct { 26 | CrawlerID int64 27 | MaxThreads uint 28 | Username string 29 | Password string 30 | RegistryURL string 31 | Token *docker.Token 32 | Repos map[string]interface{} `json:"repos"` 33 | Tags map[string]interface{} `json:"tags"` 34 | } 35 | 36 | // Registry crawler gets a list of images including their manifests and layers 37 | // from a defined Docker V2 registry. 38 | // Then a semaphore channel is opened and the images are fed to the Clair 39 | // instances for scanning. 40 | func Registry(backend datastore.Backend, cc clairclient.Client, regCrawler *RegCrawler) { 41 | start := time.Now() 42 | // Log the start of the crawl in the database 43 | err := backend.UpdateCrawler(regCrawler.CrawlerID, &datastore.Crawler{ 44 | Status: "started", 45 | }) 46 | 47 | if err != nil { 48 | log.Error(err, "could not update crawler in db") 49 | return 50 | } 51 | 52 | // Open a channel for collecting Docker images 53 | dockerImages := make(chan *docker.ImageEnvelope, 50) 54 | 55 | var totalDockerImages uint64 56 | var failedScan uint64 57 | 58 | go docker.Crawl(docker.CrawlConfig{ 59 | URL: regCrawler.RegistryURL, 60 | Username: regCrawler.Username, 61 | Password: regCrawler.Password, 62 | Repos: regCrawler.Repos, 63 | Tags: regCrawler.Tags, 64 | }, dockerImages) 65 | 66 | // A blocking channel to keep concurrency under control 67 | sem := make(chan interface{}, regCrawler.MaxThreads) 68 | defer close(sem) 69 | 70 | wg := &sync.WaitGroup{} 71 | var regError error 72 | 73 | for image := range dockerImages { 74 | sem <- true // This will block if the semaphore is full 75 | wg.Add(1) 76 | 77 | if image.Error != nil { 78 | regError = image.Error 79 | wg.Done() 80 | break 81 | } 82 | 83 | go func(image *docker.ImageEnvelope) { 84 | defer func() { <-sem }() // Release hold on one of the semaphore items 85 | _, err = ScanImage(backend, cc, regCrawler.Token, &image.Image) 86 | 87 | atomic.AddUint64(&totalDockerImages, 1) 88 | if err != nil { 89 | log.Error(fmt.Sprintf("Crawl Scan Error for Image %s/%s:%s: %s", image.Registry, image.Repo, image.Tag, err.Error())) 90 | atomic.AddUint64(&failedScan, 1) 91 | } 92 | // Tell the wait group that this scan is done 93 | wg.Done() 94 | }(image) 95 | } 96 | 97 | // Wait for all the goroutines to be done 98 | wg.Wait() 99 | ti := atomic.LoadUint64(&totalDockerImages) 100 | fs := atomic.LoadUint64(&failedScan) 101 | elapsed := time.Since(start) 102 | if regError != nil { 103 | err = backend.UpdateCrawler(regCrawler.CrawlerID, &datastore.Crawler{ 104 | Status: "finished", 105 | Messages: &datastore.CrawlerMessages{ 106 | Error: fmt.Sprintf("** Crawl of %s produced error: %s **", regCrawler.RegistryURL, regError.Error())}, 107 | Finished: time.Now(), 108 | }) 109 | if err != nil { 110 | log.Error(err, "could not update crawler in db") 111 | return 112 | } 113 | log.Error(fmt.Sprintf("** Crawl of %s produced error: %s **", regCrawler.RegistryURL, regError)) 114 | } else { 115 | err = backend.UpdateCrawler(regCrawler.CrawlerID, &datastore.Crawler{ 116 | Status: "finished", 117 | Messages: &datastore.CrawlerMessages{ 118 | Summary: fmt.Sprintf("** %d images in %s processed in %s with %d scan failures **", ti, regCrawler.RegistryURL, elapsed, fs)}, 119 | Finished: time.Now(), 120 | }) 121 | if err != nil { 122 | log.Error(err, "could not update crawler in db") 123 | return 124 | } 125 | log.Info(fmt.Sprintf("Registry crawl #%d in %s of %d images completed in %s with %d scan failures **", regCrawler.CrawlerID, regCrawler.RegistryURL, ti, elapsed, fs)) 126 | 127 | } 128 | 129 | } 130 | 131 | // ScanImage sends a single image to Clair 132 | func ScanImage(db datastore.Backend, cc clairclient.Client, token *docker.Token, image *docker.Image) (*datastore.Image, error) { 133 | // Make another call to the DB to get the image ID 134 | dbImage, err := db.GetImage(image.Registry, image.Repo, image.Tag, image.Digest) 135 | if err != nil { 136 | return nil, errors.Wrap(err, "error looking up image in database") 137 | } 138 | 139 | // Get the TopLayer and store in the PA DB 140 | topLayerHash := "" 141 | if len(image.Layers) > 0 { 142 | topLayer := strings.Join([]string{image.Digest, string(image.Layers[0])}, "") 143 | topLayerHash = GetMD5Hash(topLayer) 144 | } 145 | 146 | if dbImage == nil { 147 | dbImage = &datastore.Image{ 148 | TopLayer: topLayerHash, 149 | Registry: image.Registry, 150 | Repo: image.Repo, 151 | Tag: image.Tag, 152 | Digest: image.Digest, 153 | ManifestV2: image.ManifestV2, 154 | ManifestV1: image.ManifestV1, 155 | Metadata: image.Metadata, 156 | FirstSeen: time.Now(), 157 | LastSeen: time.Now(), 158 | } 159 | } else { 160 | dbImage.LastSeen = time.Now() 161 | } 162 | 163 | err = db.UpsertImage(dbImage) 164 | if err != nil { 165 | log.Error(fmt.Sprintf("error updating image %s/%s:%s: %+v", image.Registry, image.Repo, image.Tag, err)) 166 | return nil, errors.Wrap(err, "error updating image") 167 | } 168 | 169 | log.Debug("updated image ", image.Digest) 170 | 171 | dbImage, err = db.GetImage(image.Registry, image.Repo, image.Tag, image.Digest) 172 | if err != nil { 173 | return nil, errors.Wrap(err, "error getting id after image insert into the database") 174 | } 175 | 176 | bearerToken := "" 177 | if token.Token != "" { 178 | bearerToken = strings.Join([]string{"Bearer", token.Token}, " ") 179 | } 180 | 181 | err = clair.Push(&cc, clair.Image{ 182 | Digest: image.Digest, 183 | Registry: image.Registry, 184 | Repo: image.Repo, 185 | Layers: image.Layers, 186 | Headers: map[string]string{"Authorization": bearerToken}, 187 | }) 188 | 189 | if err != nil { 190 | return nil, errors.Wrap(err, "Error while pushing image to Clair") 191 | } 192 | 193 | log.Debug("Clair finished scanning layers:", image.Digest) 194 | 195 | return dbImage, err 196 | } 197 | 198 | // GetMD5Hash returns a MD5 hash of a string 199 | func GetMD5Hash(text string) string { 200 | hasher := md5.New() 201 | hasher.Write([]byte(text)) 202 | return hex.EncodeToString(hasher.Sum(nil)) 203 | } 204 | -------------------------------------------------------------------------------- /pkg/datastore/datastore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package datastore 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | // BackendFactory defines the function that should open a database connection 26 | type BackendFactory func(config BackendConfig) (Backend, error) 27 | 28 | var backendFactories = make(map[string]BackendFactory) 29 | 30 | // Register adds a new datastore factory 31 | func Register(name string, factory BackendFactory) { 32 | if factory == nil { 33 | panic(fmt.Sprintf("Command Factory %s does not exist", name)) 34 | } 35 | 36 | _, registered := backendFactories[name] 37 | if registered { 38 | panic(fmt.Sprintf("Command factory %s already registered", name)) 39 | } 40 | 41 | backendFactories[name] = factory 42 | } 43 | 44 | // Open opens a database connection 45 | func Open(conf BackendConfig) (Backend, error) { 46 | factory, ok := backendFactories[conf.Type] 47 | if !ok { 48 | return nil, errors.Errorf("no datastore backend for type %s", conf.Type) 49 | } 50 | 51 | return factory(conf) 52 | } 53 | 54 | // Backend defines the functionality of a datastore 55 | type Backend interface { 56 | GetImage(registry, repo, tag, digest string) (*Image, error) 57 | GetAllImages(registry, repo, tag, digest, dateStart, dateEnd, limit string) (*[]*Image, error) 58 | GetImageByID(id int) (*Image, error) 59 | GetImageByRrt(registry, repo, tag string) (*Image, error) 60 | GetImageByDigest(digest string) (*Image, error) 61 | UpsertImage(*Image) error 62 | 63 | DeleteImage(registry, repo, tag, digest string) (bool, error) 64 | GetContainer(namespace, cluster, name, image, imageID string) (*Container, error) 65 | GetContainerByID(id int) (*Container, error) 66 | 67 | GetAllContainers(namespace, cluster, name, image, imageID, dateStart, dateEnd, limit string) (*[]*Container, error) 68 | UpsertContainer(*Container) error 69 | 70 | GetPolicy(name string) (*Policy, error) 71 | GetAllPolicies(name string) (*[]*Policy, error) 72 | UpsertPolicy(*Policy) error 73 | 74 | GetCrawler(id int) (*Crawler, error) 75 | InsertCrawler(*Crawler) (int64, error) 76 | UpdateCrawler(int64, *Crawler) error 77 | 78 | // Ping returns the health status of the database 79 | Ping() bool 80 | 81 | // Close closes the database and frees any allocated resource 82 | Close() 83 | } 84 | 85 | // BackendConfig defines the type of datastore to create and its parameters 86 | type BackendConfig struct { 87 | Type string 88 | Options map[string]interface{} 89 | } 90 | -------------------------------------------------------------------------------- /pkg/datastore/model.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package datastore 18 | 19 | import ( 20 | "database/sql/driver" 21 | "encoding/json" 22 | "errors" 23 | "time" 24 | ) 25 | 26 | // Model enforces the existence of id for model structs 27 | type Model struct { 28 | ID uint64 `db:"id"` 29 | } 30 | 31 | // Image DB Struct 32 | type Image struct { 33 | Model 34 | TopLayer string `db:"top_layer"` 35 | Registry string `db:"registry"` 36 | Repo string `db:"repo"` 37 | Tag string `db:"tag"` 38 | Digest string `db:"digest"` 39 | ManifestV2 JSONMap `db:"manifest_v2"` 40 | ManifestV1 JSONMap `db:"manifest_v1"` 41 | Metadata JSONMap `db:"metadata"` 42 | FirstSeen time.Time `db:"first_seen"` 43 | LastSeen time.Time `db:"last_seen"` 44 | } 45 | 46 | // Container DB struct 47 | type Container struct { 48 | Model 49 | Namespace string `db:"namespace"` 50 | Cluster string `db:"cluster"` 51 | Name string `db:"name"` 52 | Image string `db:"image"` 53 | ImageID string `db:"image_id"` 54 | ImageRegistry string `db:"image_registry"` 55 | ImageRepo string `db:"image_repo"` 56 | ImageTag string `db:"image_tag"` 57 | ImageDigest string `db:"image_digest"` 58 | Annotations JSONMap `db:"annotations"` 59 | FirstSeen time.Time `db:"first_seen"` 60 | LastSeen time.Time `db:"last_seen"` 61 | } 62 | 63 | // Policy DB struct 64 | type Policy struct { 65 | Model 66 | Name string `db:"name"` 67 | AllowedRiskSeverity string `db:"allowed_risk_severity"` 68 | AllowedCVENames string `db:"allowed_cve_names"` 69 | AllowNotFixed bool `db:"allow_not_fixed"` 70 | NotAllowedCveNames string `db:"not_allowed_cve_names"` 71 | NotAllowedOSNames string `db:"not_allowed_os_names"` 72 | Created time.Time `db:"created"` 73 | Updated time.Time `db:"updated"` 74 | } 75 | 76 | // Crawler DB struct 77 | type Crawler struct { 78 | Model 79 | Type string `db:"type"` 80 | Status string `db:"status"` 81 | Messages *CrawlerMessages `db:"messages"` 82 | Started time.Time `db:"started"` 83 | Finished time.Time `db:"finished"` 84 | } 85 | 86 | // CrawlerMessages Field struct will contain basic information in JSON db format. 87 | // Detailed information about the scan will be still written to standard out. 88 | type CrawlerMessages struct { 89 | Summary string `json:"summary,omitempty"` 90 | Error string `json:"error,omitempty"` 91 | } 92 | 93 | // JSONMap is a type for jsonb columns 94 | type JSONMap map[string]interface{} 95 | 96 | // Value returns marshalled json from JSONMap 97 | func (m JSONMap) Value() (driver.Value, error) { 98 | j, err := json.Marshal(m) 99 | return j, err 100 | } 101 | 102 | // Scan transforms raw jsonb data to JSONMap type 103 | func (m *JSONMap) Scan(src interface{}) error { 104 | if src == nil { 105 | return nil 106 | } 107 | 108 | source, ok := src.([]byte) 109 | if !ok { 110 | return errors.New("type assertion .([]byte) failed") 111 | } 112 | 113 | var i interface{} 114 | err := json.Unmarshal(source, &i) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | *m, ok = i.(map[string]interface{}) 120 | if !ok { 121 | return errors.New("type assertion .(map[string]interface{}) failed") 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/datastore/pgsql/container.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package pgsql 4 | 5 | import ( 6 | "database/sql" 7 | "encoding/json" 8 | "regexp" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/target/portauthority/pkg/commonerr" 12 | "github.com/target/portauthority/pkg/datastore" 13 | ) 14 | 15 | func (p *pgsql) GetContainerByID(id int) (*datastore.Container, error) { 16 | var container datastore.Container 17 | 18 | err := p.QueryRow(`SELECT id, 19 | namespace, 20 | cluster, 21 | name, 22 | image, 23 | image_id, 24 | image_registry, 25 | image_repo, 26 | image_tag, 27 | image_digest, 28 | annotations, 29 | first_seen, 30 | last_seen 31 | FROM container_pa WHERE id=$1`, 32 | id).Scan(&container.ID, 33 | &container.Namespace, 34 | &container.Cluster, 35 | &container.Name, 36 | &container.Image, 37 | &container.ImageID, 38 | &container.ImageRegistry, 39 | &container.ImageRepo, 40 | &container.ImageTag, 41 | &container.ImageDigest, 42 | &container.Annotations, 43 | &container.FirstSeen, 44 | &container.LastSeen) 45 | 46 | if err != nil { 47 | switch err { 48 | case sql.ErrNoRows: 49 | return nil, commonerr.ErrNotFound 50 | default: 51 | return nil, errors.Wrap(err, "error querying for container") 52 | } 53 | } 54 | return &container, nil 55 | } 56 | 57 | func (p *pgsql) GetContainer(namespace, cluster, name, image, imageID string) (*datastore.Container, error) { 58 | var container datastore.Container 59 | 60 | err := p.QueryRow(`SELECT id, 61 | namespace, 62 | cluster, 63 | name, 64 | image, 65 | image_id, 66 | image_registry, 67 | image_repo, 68 | image_tag, 69 | image_digest, 70 | annotations, 71 | first_seen, 72 | last_seen 73 | FROM container_pa WHERE namespace=$1 AND cluster=$2 AND name=$3 AND image=$4 AND image_id=$5`, 74 | namespace, cluster, name, image, imageID).Scan(&container.ID, 75 | &container.Namespace, 76 | &container.Cluster, 77 | &container.Name, 78 | &container.Image, 79 | &container.ImageID, 80 | &container.ImageRegistry, 81 | &container.ImageRepo, 82 | &container.ImageTag, 83 | &container.ImageDigest, 84 | &container.Annotations, 85 | &container.FirstSeen, 86 | &container.LastSeen) 87 | 88 | if err != nil { 89 | switch err { 90 | case sql.ErrNoRows: 91 | return nil, nil 92 | default: 93 | return nil, errors.Wrap(err, "error querying for container") 94 | } 95 | } 96 | return &container, nil 97 | } 98 | 99 | func (p *pgsql) GetAllContainers(namespace, cluster, name, image, imageID, dateStart, dateEnd, limit string) (*[]*datastore.Container, error) { 100 | var containers []*datastore.Container 101 | query := `SELECT id, 102 | namespace, 103 | cluster, 104 | name, 105 | image, 106 | image_id, 107 | image_registry, 108 | image_repo, 109 | image_tag, 110 | image_digest, 111 | annotations, 112 | first_seen, 113 | last_seen 114 | FROM container_pa 115 | WHERE namespace LIKE '%' || $1 || '%' 116 | AND cluster LIKE '%' || $2 || '%' 117 | AND name LIKE '%' || $3 || '%' 118 | AND image LIKE '%' || $4 || '%' 119 | AND image_id LIKE '%' || $5 || '%'` 120 | 121 | // Regex conditionals should prevent possible SQL injection from unescaped 122 | // concatenation to query string. 123 | if isDate, _ := regexp.MatchString(`^[\d]{4}-[\d]{2}-[\d]{2}$`, dateStart); isDate { 124 | query += " AND last_seen>='" + dateStart + "'::date" 125 | } 126 | 127 | if isDate, _ := regexp.MatchString(`^[\d]{4}-[\d]{2}-[\d]{2}$`, dateEnd); isDate { 128 | query += " AND (last_seen<'" + dateEnd + "'::date + '1 day'::interval)" 129 | } 130 | 131 | if isInt, _ := regexp.MatchString(`^[\d]{1,}$`, limit); isInt { 132 | query += " LIMIT " + limit 133 | } 134 | 135 | rows, err := p.Query(query, namespace, cluster, name, image, imageID) 136 | 137 | if err != nil { 138 | switch err { 139 | case sql.ErrNoRows: 140 | return nil, nil 141 | default: 142 | return nil, errors.Wrap(err, "error querying for containers") 143 | } 144 | } 145 | defer rows.Close() 146 | for rows.Next() { 147 | var container datastore.Container 148 | err = rows.Scan(&container.ID, &container.Namespace, &container.Cluster, &container.Name, &container.Image, &container.ImageID, &container.ImageRegistry, &container.ImageRepo, &container.ImageTag, &container.ImageDigest, &container.Annotations, &container.FirstSeen, &container.LastSeen) 149 | if err != nil { 150 | return nil, errors.Wrap(err, "error scanning containers") 151 | } 152 | containers = append(containers, &container) 153 | } 154 | // Get any errors encountered during iteration 155 | err = rows.Err() 156 | if err != nil { 157 | return nil, errors.Wrap(err, "error scanning containers") 158 | } 159 | return &containers, nil 160 | } 161 | 162 | func (p *pgsql) UpsertContainer(container *datastore.Container) error { 163 | annotationsJSON, _ := json.Marshal(container.Annotations) 164 | 165 | // Upserting container in db 166 | _, err := p.Exec(` 167 | INSERT INTO container_pa AS c ( 168 | namespace, 169 | cluster, 170 | name, 171 | image, 172 | image_id, 173 | image_registry, 174 | image_repo, 175 | image_tag, 176 | image_digest, 177 | annotations, 178 | first_seen, 179 | last_seen) 180 | VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) 181 | ON CONFLICT (namespace, cluster, name, image, image_id) 182 | DO UPDATE SET last_seen = $12, annotations = $10 WHERE c.namespace = $1 AND c.cluster = $2 AND c.name = $3 AND c.image = $4 AND c.image_id = $5`, 183 | container.Namespace, 184 | container.Cluster, 185 | container.Name, 186 | container.Image, 187 | container.ImageID, 188 | container.ImageRegistry, 189 | container.ImageRepo, 190 | container.ImageTag, 191 | container.ImageDigest, 192 | string(annotationsJSON), 193 | container.FirstSeen, 194 | container.LastSeen) 195 | 196 | if err != nil { 197 | return errors.Wrap(err, "error upserting container") 198 | } 199 | 200 | return nil 201 | } 202 | -------------------------------------------------------------------------------- /pkg/datastore/pgsql/crawler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package pgsql 4 | 5 | import ( 6 | "database/sql" 7 | "encoding/json" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/target/portauthority/pkg/commonerr" 11 | "github.com/target/portauthority/pkg/datastore" 12 | ) 13 | 14 | func (p *pgsql) GetCrawler(id int) (*datastore.Crawler, error) { 15 | var crawler datastore.Crawler 16 | 17 | var messages string 18 | 19 | err := p.QueryRow(`SELECT id, 20 | type, 21 | status, 22 | messages, 23 | started, 24 | finished 25 | FROM crawler_pa WHERE id=$1`, 26 | id).Scan(&crawler.ID, 27 | &crawler.Type, 28 | &crawler.Status, 29 | &messages, 30 | &crawler.Started, 31 | &crawler.Finished) 32 | 33 | if err != nil { 34 | switch err { 35 | case sql.ErrNoRows: 36 | return nil, commonerr.ErrNotFound 37 | default: 38 | return nil, errors.Wrap(err, "error querying for crawler") 39 | } 40 | } 41 | 42 | crawlerMessage := &datastore.CrawlerMessages{} 43 | if messages != "" { 44 | err = json.Unmarshal([]byte(messages), &crawlerMessage) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "improper crawler message formating") 47 | } 48 | } 49 | crawler.Messages = crawlerMessage 50 | 51 | return &crawler, nil 52 | } 53 | 54 | func (p *pgsql) InsertCrawler(crawler *datastore.Crawler) (int64, error) { 55 | 56 | // Upserting container in db 57 | var id int64 58 | err := p.QueryRow(` 59 | INSERT INTO crawler_pa as c ( 60 | type, 61 | status, 62 | started) 63 | VALUES($1, $2, $3) 64 | RETURNING id`, 65 | crawler.Type, 66 | crawler.Status, 67 | crawler.Started).Scan(&id) 68 | 69 | if err != nil { 70 | return -1, errors.Wrap(err, "error inserting crawler") 71 | } 72 | 73 | return id, nil 74 | } 75 | 76 | func (p *pgsql) UpdateCrawler(id int64, crawler *datastore.Crawler) error { 77 | 78 | // Updating container in db 79 | messages, err := json.Marshal(crawler.Messages) 80 | if err != nil { 81 | return errors.Wrap(err, "error marshaling crawler messages") 82 | } 83 | 84 | _, err = p.Exec(` 85 | UPDATE crawler_pa SET status = $2, messages = $3, finished = $4 WHERE id = $1`, 86 | id, 87 | crawler.Status, 88 | string(messages), 89 | crawler.Finished) 90 | 91 | if err != nil { 92 | return errors.Wrap(err, "error updating crawler") 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/datastore/pgsql/image.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | // Most of the code and structure on how the features and vulnerabilites are 4 | // found and returned are ported from the Clair implementation. 5 | // The biggest difference is that we base this on an image instead of layers. 6 | 7 | package pgsql 8 | 9 | import ( 10 | "database/sql" 11 | "encoding/json" 12 | "regexp" 13 | 14 | "github.com/target/portauthority/pkg/commonerr" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/target/portauthority/pkg/datastore" 18 | ) 19 | 20 | // GetAllImages returns all images from the psql table 'images' 21 | func (p *pgsql) GetAllImages(registry, repo, tag, digest, dateStart, dateEnd, limit string) (*[]*datastore.Image, error) { 22 | var images []*datastore.Image 23 | query := "SELECT id, top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen FROM image_pa WHERE registry LIKE '%' || $1 || '%' AND repo LIKE '%' || $2 || '%' AND tag LIKE '%' || $3 || '%' AND digest LIKE '%' || $4 || '%'" 24 | 25 | // Regex conditionals should prevent possible SQL injection from unescaped 26 | // concatenation to query string. 27 | if isDate, _ := regexp.MatchString(`^[\d]{4}-[\d]{2}-[\d]{2}$`, dateStart); isDate { 28 | query += " AND last_seen>='" + dateStart + "'::date" 29 | } 30 | 31 | if isDate, _ := regexp.MatchString(`^[\d]{4}-[\d]{2}-[\d]{2}$`, dateEnd); isDate { 32 | query += " AND (last_seen<'" + dateEnd + "'::date + '1 day'::interval)" 33 | } 34 | 35 | if isInt, _ := regexp.MatchString(`^[\d]{1,}$`, limit); isInt { 36 | query += " LIMIT " + limit 37 | } 38 | 39 | rows, err := p.Query(query, registry, repo, tag, digest) 40 | if err != nil { 41 | switch err { 42 | case sql.ErrNoRows: 43 | return nil, nil 44 | default: 45 | return nil, errors.Wrap(err, "error querying for images") 46 | } 47 | } 48 | defer rows.Close() 49 | for rows.Next() { 50 | var image datastore.Image 51 | err = rows.Scan(&image.ID, &image.TopLayer, &image.Registry, &image.Repo, &image.Tag, &image.Digest, &image.ManifestV2, &image.ManifestV1, &image.Metadata, &image.FirstSeen, &image.LastSeen) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "error scanning images") 54 | } 55 | images = append(images, &image) 56 | } 57 | // Get any error encountered during iteration 58 | err = rows.Err() 59 | if err != nil { 60 | return nil, errors.Wrap(err, "error scanning images") 61 | } 62 | return &images, nil 63 | } 64 | 65 | // TODO: Improve this func 66 | func (p *pgsql) GetImage(registry, repo, tag, digest string) (*datastore.Image, error) { 67 | var image datastore.Image 68 | err := p.QueryRow("SELECT id, top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen FROM image_pa WHERE registry=$1 AND repo=$2 AND tag=$3 AND digest=$4", registry, repo, tag, digest).Scan(&image.ID, &image.TopLayer, &image.Registry, &image.Repo, &image.Tag, &image.Digest, &image.ManifestV2, &image.ManifestV1, &image.Metadata, &image.FirstSeen, &image.LastSeen) 69 | if err != nil { 70 | switch err { 71 | case sql.ErrNoRows: 72 | return nil, nil 73 | default: 74 | return nil, errors.Wrap(err, "error querying for image") 75 | } 76 | } 77 | return &image, nil 78 | } 79 | 80 | // GetImageRrt retures the last image seen with a matching registry repo tag. 81 | // The preferred way to obtain the correct image from the database is via the 82 | // GetImage function which requires the SHA. 83 | // This was put in place for K8s ImagePolicyWebhook that may only have this 84 | // information available. 85 | // TODO: WHERE registry LIKE '%' || exists because if the registry has http, 86 | // then it needs to be stripped from the addition of the image. This is a 87 | // workaround. 88 | func (p *pgsql) GetImageByRrt(registry, repo, tag string) (*datastore.Image, error) { 89 | var image datastore.Image 90 | err := p.QueryRow("SELECT id, top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen FROM image_pa WHERE registry LIKE '%' || $1 AND repo=$2 AND tag=$3 order by last_seen desc limit 1", registry, repo, tag).Scan(&image.ID, &image.TopLayer, &image.Registry, &image.Repo, &image.Tag, &image.Digest, &image.ManifestV2, &image.ManifestV1, &image.Metadata, &image.FirstSeen, &image.LastSeen) 91 | if err != nil { 92 | switch err { 93 | case sql.ErrNoRows: 94 | return nil, commonerr.ErrNotFound 95 | default: 96 | return nil, errors.Wrap(err, "error querying for image") 97 | } 98 | } 99 | return &image, nil 100 | } 101 | 102 | func (p *pgsql) GetImageByDigest(digest string) (*datastore.Image, error) { 103 | var image datastore.Image 104 | err := p.QueryRow("SELECT id, top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen FROM image_pa WHERE digest=$1 order by last_seen desc limit 1", digest).Scan(&image.ID, &image.TopLayer, &image.Registry, &image.Repo, &image.Tag, &image.Digest, &image.ManifestV2, &image.ManifestV1, &image.Metadata, &image.FirstSeen, &image.LastSeen) 105 | if err != nil { 106 | switch err { 107 | case sql.ErrNoRows: 108 | return nil, commonerr.ErrNotFound 109 | default: 110 | return nil, errors.Wrap(err, "error querying for image") 111 | } 112 | } 113 | return &image, nil 114 | } 115 | 116 | func (p *pgsql) GetImageByID(id int) (*datastore.Image, error) { 117 | var image datastore.Image 118 | err := p.QueryRow("SELECT id, top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen FROM image_pa WHERE id=$1", id).Scan(&image.ID, &image.TopLayer, &image.Registry, &image.Repo, &image.Tag, &image.Digest, &image.ManifestV2, &image.ManifestV1, &image.Metadata, &image.FirstSeen, &image.LastSeen) 119 | if err != nil { 120 | switch err { 121 | case sql.ErrNoRows: 122 | return nil, errors.WithMessage(commonerr.ErrNotFound, "Image not found") 123 | default: 124 | return nil, errors.Wrap(err, "error querying for image") 125 | } 126 | } 127 | 128 | return &image, nil 129 | } 130 | 131 | func (p *pgsql) UpsertImage(image *datastore.Image) error { 132 | safeManifestV1 := marshalOrReturnEmptyJSON(image.ManifestV1) 133 | safeManifestV2 := marshalOrReturnEmptyJSON(image.ManifestV2) 134 | safeMetadata := marshalOrReturnEmptyJSON(image.Metadata) 135 | 136 | _, err := p.Exec("INSERT INTO image_pa as i (top_layer, registry, repo, tag, digest, manifest_v2, manifest_v1, metadata, first_seen, last_seen) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (registry, repo, tag, digest) DO UPDATE SET last_seen = $10 WHERE i.registry = $2 AND i.repo = $3 AND i.tag = $4 AND i.digest = $5", 137 | image.TopLayer, 138 | image.Registry, 139 | image.Repo, 140 | image.Tag, 141 | image.Digest, 142 | safeManifestV2, 143 | safeManifestV1, 144 | safeMetadata, 145 | image.FirstSeen, 146 | image.LastSeen, 147 | ) 148 | if err != nil { 149 | return errors.Wrap(err, "error upserting image") 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func (p *pgsql) DeleteImage(registry, repo, tag, digest string) (bool, error) { 156 | found := false 157 | res, err := p.Exec("DELETE FROM image_pa WHERE registry=$1 AND repo=$2 AND tag=$3 AND digest=$4", registry, repo, tag, digest) 158 | // Should delete layers in layer table now, too 159 | if err != nil { 160 | return false, errors.Wrap(err, "error deleting image") 161 | } 162 | 163 | affected, err := res.RowsAffected() 164 | if err != nil { 165 | return false, errors.Wrap(err, "error checking rows affected") 166 | } 167 | 168 | if affected > 0 { 169 | // TODO: Should we have some checking to make sure this is 170 | // never more than 1 with dry run? It's probably a bug, anyway, if there are 171 | // multiple images found with these search params. 172 | found = true 173 | } 174 | 175 | return found, nil 176 | } 177 | 178 | func marshalOrReturnEmptyJSON(JSONMap map[string]interface{}) []byte { 179 | safeJSON := []byte(`{}`) 180 | var err error 181 | if len(JSONMap) > 0 { 182 | safeJSON, err = json.Marshal(JSONMap) 183 | if err != nil { 184 | safeJSON = []byte(`{}`) 185 | } 186 | } 187 | return safeJSON 188 | } 189 | -------------------------------------------------------------------------------- /pkg/datastore/pgsql/pgsql.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright (c) 2018 Target Brands, Inc. 16 | 17 | package pgsql 18 | 19 | import ( 20 | "database/sql" 21 | "log" 22 | 23 | "github.com/pkg/errors" 24 | 25 | "github.com/target/portauthority/pkg/datastore" 26 | 27 | // Import Postgres driver for use through database/sql 28 | _ "github.com/lib/pq" 29 | ) 30 | 31 | func init() { 32 | datastore.Register("pgsql", openDatabase) 33 | } 34 | 35 | // Config parameterizes a pgsql datastore backend 36 | type Config struct { 37 | Source string 38 | } 39 | 40 | type pgsql struct { 41 | *sql.DB 42 | } 43 | 44 | // Ping verifies that the database is accessible 45 | func (p *pgsql) Ping() bool { 46 | return p.DB.Ping() == nil 47 | } 48 | 49 | func openDatabase(backendConfig datastore.BackendConfig) (datastore.Backend, error) { 50 | var pg pgsql 51 | var err error 52 | 53 | config := &Config{ 54 | Source: "host=localhost port=5432 user=postgres sslmode=disable statement_timeout=60000", 55 | } 56 | 57 | src, exists := backendConfig.Options["source"] 58 | if exists { 59 | str, ok := src.(string) 60 | if ok { 61 | config.Source = str 62 | } 63 | } 64 | 65 | pg.DB, err = sql.Open("postgres", config.Source) 66 | if err != nil { 67 | pg.Close() 68 | return nil, errors.Wrap(err, "error opening database connection") 69 | } 70 | 71 | // Verify database state 72 | if err = pg.DB.Ping(); err != nil { 73 | pg.Close() 74 | return nil, errors.Wrap(err, "error communicating with database") 75 | } 76 | 77 | pg.initDatabase() 78 | 79 | return &pg, nil 80 | } 81 | 82 | func (p *pgsql) initDatabase() error { 83 | 84 | // Create image table if it doesn't exist 85 | initSQL := ` 86 | CREATE TABLE IF NOT EXISTS image_pa( 87 | id SERIAL PRIMARY KEY, 88 | top_layer VARCHAR, 89 | registry VARCHAR, 90 | repo VARCHAR, 91 | tag VARCHAR, 92 | digest VARCHAR, 93 | manifest_v2 JSONB, 94 | manifest_v1 JSONB, 95 | metadata JSONB, 96 | first_seen TIMESTAMPTZ, 97 | last_seen TIMESTAMPTZ, 98 | unique (registry, repo, tag, digest) 99 | ); 100 | 101 | CREATE INDEX IF NOT EXISTS idx_image_id ON image_pa (id); 102 | CREATE INDEX IF NOT EXISTS idx_image_top_layer ON image_pa (top_layer); 103 | CREATE INDEX IF NOT EXISTS idxcreated ON image_pa (((manifest_v1->'history'->0->>'v1Compatibility')::JSON ->>'created')); 104 | 105 | CREATE TABLE IF NOT EXISTS container_pa( 106 | id SERIAL PRIMARY KEY, 107 | namespace VARCHAR, 108 | cluster VARCHAR, 109 | name VARCHAR, 110 | image VARCHAR, 111 | image_id VARCHAR, 112 | image_registry VARCHAR, 113 | image_repo VARCHAR, 114 | image_tag VARCHAR, 115 | image_digest VARCHAR, 116 | annotations JSONB, 117 | first_seen TIMESTAMPTZ, 118 | last_seen TIMESTAMPTZ, 119 | unique (namespace, cluster, name, image, image_id) 120 | ); 121 | 122 | CREATE INDEX IF NOT EXISTS idx_image_registry ON container_pa (image_registry); 123 | CREATE INDEX IF NOT EXISTS idx_layer_image_repo ON container_pa (image_repo); 124 | CREATE INDEX IF NOT EXISTS idx_layer_image_tag ON container_pa (image_tag); 125 | CREATE INDEX IF NOT EXISTS idx_layer_image_digest ON container_pa (image_digest); 126 | 127 | CREATE TABLE IF NOT EXISTS policy_pa( 128 | id SERIAL PRIMARY KEY, 129 | name VARCHAR NOT NULL, 130 | allowed_risk_severity VARCHAR[] DEFAULT '{}', 131 | allowed_cve_names VARCHAR[] DEFAULT '{}', 132 | allow_not_fixed BOOLEAN, 133 | not_allowed_cve_names VARCHAR[] DEFAULT '{}', 134 | not_allowed_os_names VARCHAR[] DEFAULT '{}', 135 | created TIMESTAMPTZ, 136 | updated TIMESTAMPTZ, 137 | unique (name) 138 | ); 139 | 140 | CREATE INDEX IF NOT EXISTS idx_policy_id ON policy_pa (id); 141 | 142 | INSERT INTO policy_pa as p (name,allow_not_fixed,created,updated) VALUES('default','false',now(),now()) ON CONFLICT (name) DO NOTHING; 143 | 144 | CREATE TABLE IF NOT EXISTS crawler_pa( 145 | id SERIAL PRIMARY KEY, 146 | type VARCHAR NOT NULL, 147 | status VARCHAR NOT NULL, 148 | messages VARCHAR, 149 | started TIMESTAMPTZ, 150 | finished TIMESTAMPTZ, 151 | unique (id) 152 | ); 153 | 154 | CREATE INDEX IF NOT EXISTS idx_crawler_id ON crawler_pa (id); 155 | ` 156 | res, err := p.Exec(initSQL) 157 | if err != nil { 158 | return errors.Wrap(err, "error initializing port authority database tables") 159 | } 160 | 161 | rowCnt, err := res.RowsAffected() 162 | if err != nil { 163 | return errors.Wrap(err, "error resolving number of rows affected by init sql") 164 | } 165 | if rowCnt > 0 { 166 | log.Printf("Port Authority DB Initialized") 167 | } 168 | 169 | return nil 170 | } 171 | 172 | // Close closes the database 173 | func (p *pgsql) Close() { 174 | if p.DB != nil { 175 | p.DB.Close() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /pkg/datastore/pgsql/policy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | // Most of the code and structure on how the features and vulnerabilites are 4 | // found and returned are ported from the Clair implementation. 5 | // The biggest difference is that we base this on an image instead of layers. 6 | 7 | package pgsql 8 | 9 | import ( 10 | "database/sql" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/target/portauthority/pkg/datastore" 14 | ) 15 | 16 | // GetPolicy will return a single database policy 17 | func (p *pgsql) GetPolicy(name string) (*datastore.Policy, error) { 18 | var policy datastore.Policy 19 | err := p.QueryRow("SELECT id, name, array_to_json(allowed_risk_severity), array_to_json(allowed_cve_names), allow_not_fixed, array_to_json(not_allowed_cve_names), array_to_json(not_allowed_os_names), created, updated FROM policy_pa WHERE name=$1", name).Scan(&policy.ID, &policy.Name, &policy.AllowedRiskSeverity, &policy.AllowedCVENames, &policy.AllowNotFixed, &policy.NotAllowedCveNames, &policy.NotAllowedOSNames, &policy.Created, &policy.Updated) 20 | if err != nil { 21 | switch err { 22 | case sql.ErrNoRows: 23 | return nil, nil 24 | default: 25 | return nil, errors.Wrap(err, "error querying for policy") 26 | } 27 | } 28 | return &policy, nil 29 | } 30 | 31 | // GetAllPolicies returns an an array of Policies based on the input parameters 32 | func (p *pgsql) GetAllPolicies(name string) (*[]*datastore.Policy, error) { 33 | var policies []*datastore.Policy 34 | rows, err := p.Query("SELECT id, name, array_to_json(allowed_risk_severity), array_to_json(allowed_cve_names), allow_not_fixed, array_to_json(not_allowed_cve_names), array_to_json(not_allowed_os_names), created, updated FROM policy_pa WHERE name LIKE '%' || $1 || '%'", name) 35 | if err != nil { 36 | switch err { 37 | case sql.ErrNoRows: 38 | return nil, nil 39 | default: 40 | return nil, errors.Wrap(err, "error querying for policies") 41 | } 42 | } 43 | defer rows.Close() 44 | for rows.Next() { 45 | var policy datastore.Policy 46 | err = rows.Scan(&policy.ID, &policy.Name, &policy.AllowedRiskSeverity, &policy.AllowedCVENames, &policy.AllowNotFixed, &policy.NotAllowedCveNames, &policy.NotAllowedOSNames, &policy.Created, &policy.Updated) 47 | if err != nil { 48 | return nil, errors.Wrap(err, "error scanning policies") 49 | } 50 | policies = append(policies, &policy) 51 | } 52 | // Get any error encountered during iteration 53 | err = rows.Err() 54 | if err != nil { 55 | return nil, errors.Wrap(err, "error scanning policies") 56 | } 57 | return &policies, nil 58 | } 59 | 60 | func (p *pgsql) UpsertPolicy(policy *datastore.Policy) error { 61 | 62 | _, err := p.Exec("INSERT INTO policy_pa as p (name, allowed_risk_severity, allowed_cve_names, allow_not_fixed, not_allowed_cve_names, not_allowed_os_names, created, updated) VALUES($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (name) DO UPDATE SET allowed_risk_severity=$2, allowed_cve_names=$3, allow_not_fixed=$4, not_allowed_cve_names=$5, not_allowed_os_names=$6, updated=$8 WHERE p.name = $1", 63 | policy.Name, 64 | policy.AllowedRiskSeverity, 65 | policy.AllowedCVENames, 66 | policy.AllowNotFixed, 67 | policy.NotAllowedCveNames, 68 | policy.NotAllowedOSNames, 69 | policy.Created, 70 | policy.Updated, 71 | ) 72 | if err != nil { 73 | return errors.Wrap(err, "error upserting policy") 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (p *pgsql) DeletePolicy(name string) (bool, error) { 80 | found := false 81 | res, err := p.Exec("DELETE FROM policy_pa WHERE name=$1", name) 82 | 83 | if err != nil { 84 | return false, errors.Wrap(err, "error deleting policy") 85 | } 86 | 87 | affected, err := res.RowsAffected() 88 | if err != nil { 89 | return false, errors.Wrap(err, "error checking rows affected") 90 | } 91 | 92 | if affected > 0 { 93 | // TODO: Should we have some checking to make sure this is 94 | // never more than 1 with dry run? It's probably a bug, anyway, if there are 95 | // multiple images found with these search params. 96 | found = true 97 | } 98 | 99 | return found, nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/docker/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package docker 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/target/portauthority/pkg/docker/registry" 14 | "golang.org/x/oauth2/google" 15 | ) 16 | 17 | // AuthConfig struct init 18 | type AuthConfig struct { 19 | 20 | // RegistryURL, Username, Password are required for obtaining a token. 21 | // Repo and Tag are required for docker.io as as tokens must have a directed 22 | // scope. 23 | RegistryURL string 24 | Repo string 25 | Tag string 26 | Username string 27 | Password string 28 | } 29 | 30 | // Token struct init 31 | type Token struct { 32 | Token string `json:"token"` 33 | } 34 | 35 | // AuthRegistry will attempt to authenticate to a register with the provided 36 | // credentials, returning the resulting token. 37 | func AuthRegistry(authConfig *AuthConfig) (*Token, error) { 38 | 39 | token := &Token{} 40 | 41 | // Need special handling for obtaining the GCR token at this point in time. 42 | // TODO: At some point, the token should be obtained from the Docker client 43 | // once a valid GCR token can be extracted. 44 | if strings.Contains(authConfig.RegistryURL, "gcr.io") && authConfig.Password != "" { // gcr\.io$ 45 | jwtConfig, err := google.JWTConfigFromJSON([]byte(authConfig.Password), "https://www.googleapis.com/auth/devstorage.read_only") 46 | if err != nil { 47 | return token, errors.Wrap(err, "error getting gcr token") 48 | } 49 | 50 | gcrtoken, err := jwtConfig.TokenSource(context.Background()).Token() 51 | if err != nil { 52 | return token, errors.Wrap(err, "getting gcr token error") 53 | } 54 | 55 | token = &Token{Token: gcrtoken.AccessToken} 56 | return token, nil 57 | } 58 | 59 | // Format appropriate URL 60 | url := fmt.Sprintf("%s/v2/", authConfig.RegistryURL) 61 | 62 | if authConfig.Repo != "" && authConfig.Tag != "" { 63 | url = fmt.Sprintf("%s/v2/%s/manifests/%s", authConfig.RegistryURL, authConfig.Repo, authConfig.Tag) 64 | } 65 | 66 | resp, err := http.Get(url) 67 | if resp == nil { 68 | return token, errors.Wrap(err, "no response") 69 | } 70 | if err != nil { 71 | return token, errors.Wrap(err, "error making initial request to registry for auth") 72 | } 73 | defer resp.Body.Close() 74 | 75 | if resp.StatusCode == http.StatusUnauthorized { 76 | 77 | authService := registry.ParseOauthHeader(resp) 78 | 79 | authReq, err := authService.Request(authConfig.Username, authConfig.Password) 80 | if err != nil { 81 | return token, errors.Wrap(err, "error building auth request") 82 | } 83 | 84 | tokenResp, err := http.DefaultClient.Do(authReq) 85 | if err != nil { 86 | return token, errors.Wrap(err, "error performing token request") 87 | } 88 | defer tokenResp.Body.Close() 89 | 90 | var t Token 91 | decoder := json.NewDecoder(tokenResp.Body) 92 | err = decoder.Decode(&t) 93 | if err != nil { 94 | return token, errors.Wrap(err, "error decoding token response") 95 | } 96 | 97 | token = &Token{Token: t.Token} 98 | 99 | return token, nil 100 | } 101 | 102 | return token, nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/docker/docker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Target Brands, Inc. 2 | 3 | package docker 4 | 5 | import ( 6 | "fmt" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/fatih/structs" 11 | "github.com/pkg/errors" 12 | "github.com/target/portauthority/pkg/datastore" 13 | "github.com/target/portauthority/pkg/docker/registry" 14 | ) 15 | 16 | // ImageEnvelope struct init 17 | type ImageEnvelope struct { 18 | Image 19 | Error error `json:"error"` 20 | } 21 | 22 | // Image struct init 23 | type Image struct { 24 | Registry string `json:"registry"` 25 | Repo string `json:"repo"` 26 | Tag string `json:"tag"` 27 | Digest string `json:"digest"` 28 | Layers []string `json:"layers"` 29 | ManifestV2 datastore.JSONMap `json:"manifest"` 30 | ManifestV1 datastore.JSONMap `json:"manifestv1"` 31 | Metadata datastore.JSONMap `json:"metadata"` 32 | } 33 | 34 | // CrawlConfig struct init 35 | type CrawlConfig struct { 36 | URL string `json:"url"` 37 | Username string `json:"username"` 38 | Password string `json:"password"` 39 | Repos map[string]interface{} `json:"repos"` 40 | Tags map[string]interface{} `json:"tags"` 41 | } 42 | 43 | const emptyLayer = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" 44 | 45 | // Crawl will search a registry for Docker images. 46 | // Search can be filtered in the crawl Config. 47 | // Will return results on images chan; will close images before returning. 48 | // Returns error when it cannot proceed. 49 | // Errors encountered getting image manifests are communicated in the 50 | // ImageEnvelope. 51 | func Crawl(conf CrawlConfig, images chan *ImageEnvelope) { 52 | // Open connection with the registry 53 | hub, err := registry.New(conf.URL, conf.Username, conf.Password) 54 | if err != nil { 55 | images <- &ImageEnvelope{Error: errors.Wrapf(err, "error connecting to registry: %s", conf.URL)} 56 | close(images) 57 | return 58 | } 59 | 60 | // List all the repos in the registry 61 | repos, err := hub.Repositories() 62 | if err != nil { 63 | images <- &ImageEnvelope{Error: errors.Wrapf(err, "error listing repositories for %s", conf.URL)} 64 | close(images) 65 | return 66 | } 67 | 68 | log.Debug("%v", len(conf.Repos)) 69 | log.Debug("%v", conf.Repos) 70 | 71 | for _, repo := range repos { 72 | 73 | if _, ok := conf.Repos[repo]; ok || len(conf.Repos) == 0 { // Proceed if map empty or if this repo is in map 74 | // List all the tags in the repository 75 | tags, err := hub.Tags(repo) 76 | if err != nil { 77 | images <- &ImageEnvelope{Error: errors.Wrapf(err, "error listing tags for %s/%s", conf.URL, repo)} 78 | close(images) 79 | return 80 | } 81 | 82 | for _, tag := range tags { 83 | if _, ok := conf.Tags[tag]; ok || len(conf.Tags) == 0 { // Proceed if map empty or if this tag is in map 84 | 85 | image, err := GetImage(hub, repo, tag) 86 | if err != nil { 87 | log.Error(err, "error getting image: %s/%s:%s", hub.URL, repo, tag) 88 | continue 89 | } 90 | 91 | images <- &ImageEnvelope{ 92 | Image: Image{ 93 | Registry: image.Registry, 94 | Repo: image.Repo, 95 | Tag: image.Tag, 96 | Digest: image.Digest, 97 | Layers: image.Layers, 98 | ManifestV2: image.ManifestV2, 99 | ManifestV1: image.ManifestV1, 100 | }, 101 | } 102 | } 103 | } 104 | } 105 | } 106 | close(images) 107 | } 108 | 109 | // GetRegistry returns a registry object that can be used later 110 | func GetRegistry(registryURL string, username string, password string) (*registry.Registry, error) { 111 | hub, err := registry.New(registryURL, username, password) 112 | if err != nil { 113 | return hub, errors.Wrap(err, "error initializing registry") 114 | } 115 | return hub, nil 116 | } 117 | 118 | // GetImage returns a Docker Image containing V1 and V2 manifests, its layers, 119 | // and location information. 120 | func GetImage(hub *registry.Registry, repo string, tag string) (*Image, error) { 121 | // Default maniftest will be a v2 122 | digest, err := hub.ManifestDigestV2(repo, tag) 123 | if err != nil { 124 | log.Debug(fmt.Sprintf("Error getting v2 content digest: %s", err)) 125 | // Attempt to obtain v1 if v2 is unavailable 126 | digest, err = hub.ManifestDigest(repo, tag) 127 | if err != nil { 128 | return nil, fmt.Errorf("Unable to obtain either v1 or v2 digest: %s", err) 129 | } 130 | } 131 | 132 | // Both V1 and V2 manifests contain useful data we want to store 133 | var layers []string 134 | var manifestV2Map map[string]interface{} 135 | manifest, err := hub.ManifestV2(repo, tag) 136 | if err != nil { 137 | log.Debug(fmt.Sprintf("Error getting v2 manifest: %s for Image %s/%s:%s", err, hub.URL, repo, tag)) 138 | } else { 139 | manifestV2Map = structs.Map(manifest.Manifest) 140 | 141 | // Will use v2 manifest to build layers if its availble. 142 | // V1 and V2 layer order is reversed. 143 | for i := len(manifest.Layers) - 1; i >= 0; i-- { 144 | if string(manifest.Layers[i].Digest) != emptyLayer { 145 | layers = append(layers, string(manifest.Layers[i].Digest)) 146 | } 147 | } 148 | } 149 | 150 | var manifestV1Map map[string]interface{} 151 | manifestV1, err := hub.Manifest(repo, tag) 152 | if err != nil { 153 | log.Debug(fmt.Sprintf("Error getting v1 manifest: %s for Image %s/%s:%s", err, hub.URL, repo, tag)) 154 | } else { 155 | manifestV1Map = structs.Map(manifestV1.Manifest) 156 | 157 | // If layers from V1 aren't available attempt to use the V1. 158 | // V1 and V2 layer order is reversed. 159 | if len(layers) == 0 { 160 | for i := 0; i <= len(manifestV1.FSLayers)-1; i++ { 161 | if string(manifestV1.FSLayers[i].BlobSum) != emptyLayer { 162 | layers = append(layers, string(manifestV1.FSLayers[i].BlobSum)) 163 | } 164 | } 165 | } 166 | } 167 | 168 | if err != nil && manifest == nil && manifestV1 == nil { 169 | return nil, fmt.Errorf("Docker V1 or V2 could be obtained: %s", err) 170 | } 171 | 172 | if len(layers) == 0 { 173 | return nil, fmt.Errorf("Image manifest contaied no layers: %s/%s:%s", hub.URL, repo, tag) 174 | } 175 | 176 | image := &Image{ 177 | Registry: hub.URL, 178 | Repo: repo, 179 | Tag: tag, 180 | Digest: string(digest), 181 | ManifestV1: manifestV1Map, 182 | ManifestV2: manifestV2Map, 183 | Layers: layers, 184 | } 185 | return image, nil 186 | } 187 | -------------------------------------------------------------------------------- /pkg/docker/registry/authchallenge.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // Octet types from RFC 2616 11 | type octetType byte 12 | 13 | // AuthorizationChallenge carries information from a WWW-Authenticate response 14 | // header. 15 | type AuthorizationChallenge struct { 16 | Scheme string 17 | Parameters map[string]string 18 | } 19 | 20 | var octetTypes [256]octetType 21 | 22 | const ( 23 | isToken octetType = 1 << iota 24 | isSpace 25 | ) 26 | 27 | func init() { 28 | // OCTET = 29 | // CHAR = 30 | // CTL = 31 | // CR = 32 | // LF = 33 | // SP = 34 | // HT = 35 | // <"> = 36 | // CRLF = CR LF 37 | // LWS = [CRLF] 1*( SP | HT ) 38 | // TEXT = 39 | // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> 40 | // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT 41 | // token = 1* 42 | // qdtext = > 43 | 44 | for c := 0; c < 256; c++ { 45 | var t octetType 46 | isCtl := c <= 31 || c == 127 47 | isChar := 0 <= c && c <= 127 48 | isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 49 | if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { 50 | t |= isSpace 51 | } 52 | if isChar && !isCtl && !isSeparator { 53 | t |= isToken 54 | } 55 | octetTypes[c] = t 56 | } 57 | } 58 | 59 | // ParseAuthHeader func init 60 | func ParseAuthHeader(header http.Header) []*AuthorizationChallenge { 61 | var challenges []*AuthorizationChallenge 62 | for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { 63 | v, p := parseValueAndParams(h) 64 | if v != "" { 65 | challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p}) 66 | } 67 | } 68 | return challenges 69 | } 70 | 71 | func parseValueAndParams(header string) (value string, params map[string]string) { 72 | params = make(map[string]string) 73 | value, s := expectToken(header) 74 | if value == "" { 75 | return 76 | } 77 | value = strings.ToLower(value) 78 | s = "," + skipSpace(s) 79 | for strings.HasPrefix(s, ",") { 80 | var pkey string 81 | pkey, s = expectToken(skipSpace(s[1:])) 82 | if pkey == "" { 83 | return 84 | } 85 | if !strings.HasPrefix(s, "=") { 86 | return 87 | } 88 | var pvalue string 89 | pvalue, s = expectTokenOrQuoted(s[1:]) 90 | if pvalue == "" { 91 | return 92 | } 93 | pkey = strings.ToLower(pkey) 94 | params[pkey] = pvalue 95 | s = skipSpace(s) 96 | } 97 | return 98 | } 99 | 100 | func skipSpace(s string) (rest string) { 101 | i := 0 102 | for ; i < len(s); i++ { 103 | if octetTypes[s[i]]&isSpace == 0 { 104 | break 105 | } 106 | } 107 | return s[i:] 108 | } 109 | 110 | func expectToken(s string) (token, rest string) { 111 | i := 0 112 | for ; i < len(s); i++ { 113 | if octetTypes[s[i]]&isToken == 0 { 114 | break 115 | } 116 | } 117 | return s[:i], s[i:] 118 | } 119 | 120 | func expectTokenOrQuoted(s string) (value, rest string) { 121 | if !strings.HasPrefix(s, "\"") { 122 | return expectToken(s) 123 | } 124 | s = s[1:] 125 | for i := 0; i < len(s); i++ { 126 | switch s[i] { 127 | case '"': 128 | return s[:i], s[i+1:] 129 | case '\\': 130 | p := make([]byte, len(s)-1) 131 | j := copy(p, s[:i]) 132 | escape := true 133 | for i = i + i; i < len(s); i++ { 134 | b := s[i] 135 | switch { 136 | case escape: 137 | escape = false 138 | p[j] = b 139 | j++ 140 | case b == '\\': 141 | escape = true 142 | case b == '"': 143 | return string(p[:j]), s[i+1:] 144 | default: 145 | p[j] = b 146 | j++ 147 | } 148 | } 149 | return "", "" 150 | } 151 | } 152 | return "", "" 153 | } 154 | -------------------------------------------------------------------------------- /pkg/docker/registry/basictransport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // BasicTransport struct init 11 | type BasicTransport struct { 12 | Transport http.RoundTripper 13 | URL string 14 | Username string 15 | Password string 16 | } 17 | 18 | // RoundTrip func init 19 | func (t *BasicTransport) RoundTrip(req *http.Request) (*http.Response, error) { 20 | if strings.HasPrefix(req.URL.String(), t.URL) { 21 | if t.Username != "" || t.Password != "" { 22 | req.SetBasicAuth(t.Username, t.Password) 23 | } 24 | } 25 | resp, err := t.Transport.RoundTrip(req) 26 | return resp, err 27 | } 28 | -------------------------------------------------------------------------------- /pkg/docker/registry/errortransport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | ) 11 | 12 | // HTTPStatusError struct init 13 | type HTTPStatusError struct { 14 | Response *http.Response 15 | 16 | // Copied from `Response.Body` to avoid problems with unclosed bodies later. 17 | // Nobody calls `err.Response.Body.Close()`, ever. 18 | Body []byte 19 | } 20 | 21 | func (err *HTTPStatusError) Error() string { 22 | return fmt.Sprintf("http: non-successful response (status=%v body=%q)", err.Response.StatusCode, err.Body) 23 | } 24 | 25 | var _ error = &HTTPStatusError{} 26 | 27 | // ErrorTransport struct init 28 | type ErrorTransport struct { 29 | Transport http.RoundTripper 30 | } 31 | 32 | // RoundTrip func init 33 | func (t *ErrorTransport) RoundTrip(request *http.Request) (*http.Response, error) { 34 | resp, err := t.Transport.RoundTrip(request) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if resp == nil { 39 | return nil, errors.New("http: empty response") 40 | } 41 | if resp.StatusCode >= 400 { 42 | defer resp.Body.Close() 43 | var body []byte 44 | body, err = ioutil.ReadAll(resp.Body) 45 | if err != nil { 46 | return nil, fmt.Errorf("http: failed to read response body (status=%v, err=%q)", resp.StatusCode, err) 47 | } 48 | 49 | return nil, &HTTPStatusError{ 50 | Response: resp, 51 | Body: body, 52 | } 53 | } 54 | 55 | return resp, err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/docker/registry/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "net/http" 9 | "regexp" 10 | ) 11 | 12 | var ( 13 | // ErrNoMorePages var init 14 | ErrNoMorePages = errors.New("No more pages") 15 | ) 16 | 17 | func (registry *Registry) getJSON(url string, response interface{}) error { 18 | resp, err := registry.Client.Get(url) 19 | if err != nil { 20 | return err 21 | } 22 | defer resp.Body.Close() 23 | 24 | decoder := json.NewDecoder(resp.Body) 25 | err = decoder.Decode(response) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // getPaginatedJson accepts a string and a pointer, and returns the 34 | // next page URL while updating pointed-to variable with a parsed JSON 35 | // value. When there are no more pages it returns `ErrNoMorePages`. 36 | func (registry *Registry) getPaginatedJSON(url string, response interface{}) (string, error) { 37 | resp, err := registry.Client.Get(url) 38 | if err != nil { 39 | return "", err 40 | } 41 | defer resp.Body.Close() 42 | 43 | decoder := json.NewDecoder(resp.Body) 44 | err = decoder.Decode(response) 45 | if err != nil { 46 | return "", err 47 | } 48 | return getNextLink(resp) 49 | } 50 | 51 | // Matches an RFC 5988 (https://tools.ietf.org/html/rfc5988#section-5) 52 | // Link header. For example, 53 | // 54 | // ; type="application/json"; rel="next" 55 | // 56 | // The URL is _supposed_ to be wrapped by angle brackets `< ... >`, 57 | // but e.g., quay.io does not include them. Similarly, params like 58 | // `rel="next"` may not have quoted values in the wild. 59 | var nextLinkRE = regexp.MustCompile(`^ *]+)>? *(?:;[^;]*)*; *rel="?next"?(?:;.*)?`) 60 | 61 | func getNextLink(resp *http.Response) (string, error) { 62 | for _, link := range resp.Header[http.CanonicalHeaderKey("Link")] { 63 | parts := nextLinkRE.FindStringSubmatch(link) 64 | if parts != nil { 65 | return parts[1], nil 66 | } 67 | } 68 | return "", ErrNoMorePages 69 | } 70 | -------------------------------------------------------------------------------- /pkg/docker/registry/manifest.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "bytes" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | manifestV1 "github.com/docker/distribution/manifest/schema1" 11 | manifestV2 "github.com/docker/distribution/manifest/schema2" 12 | "github.com/opencontainers/go-digest" 13 | ) 14 | 15 | // Manifest func init 16 | func (registry *Registry) Manifest(repository, reference string) (*manifestV1.SignedManifest, error) { 17 | url := registry.url("/v2/%s/manifests/%s", repository, reference) 18 | registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) 19 | 20 | req, err := http.NewRequest("GET", url, nil) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | req.Header.Set("Accept", manifestV1.MediaTypeSignedManifest+","+manifestV1.MediaTypeManifest) 26 | 27 | resp, err := registry.Client.Do(req) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | defer resp.Body.Close() 33 | body, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | signedManifest := &manifestV1.SignedManifest{} 39 | err = signedManifest.UnmarshalJSON(body) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return signedManifest, nil 45 | } 46 | 47 | // ManifestV2 func init 48 | func (registry *Registry) ManifestV2(repository, reference string) (*manifestV2.DeserializedManifest, error) { 49 | url := registry.url("/v2/%s/manifests/%s", repository, reference) 50 | registry.Logf("registry.manifest.get url=%s repository=%s reference=%s", url, repository, reference) 51 | 52 | req, err := http.NewRequest("GET", url, nil) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | req.Header.Set("Accept", manifestV2.MediaTypeManifest) 58 | resp, err := registry.Client.Do(req) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | defer resp.Body.Close() 64 | body, err := ioutil.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | deserialized := &manifestV2.DeserializedManifest{} 70 | err = deserialized.UnmarshalJSON(body) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return deserialized, nil 75 | } 76 | 77 | // ManifestDigest func init 78 | func (registry *Registry) ManifestDigest(repository, reference string) (digest.Digest, error) { 79 | url := registry.url("/v2/%s/manifests/%s", repository, reference) 80 | registry.Logf("registry.manifest.head url=%s repository=%s reference=%s", url, repository, reference) 81 | 82 | resp, err := registry.Client.Head(url) 83 | if resp != nil { 84 | defer resp.Body.Close() 85 | } 86 | if err != nil { 87 | return "", err 88 | } 89 | return digest.Parse(resp.Header.Get("Docker-Content-Digest")) 90 | } 91 | 92 | // ManifestDigestV2 func init 93 | func (registry *Registry) ManifestDigestV2(repository, reference string) (digest.Digest, error) { 94 | url := registry.url("/v2/%s/manifests/%s", repository, reference) 95 | registry.Logf("registry.manifest.head url=%s repository=%s reference=%s", url, repository, reference) 96 | 97 | req, err := http.NewRequest("HEAD", url, nil) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | req.Header.Set("Accept", manifestV2.MediaTypeManifest) 103 | resp, err := registry.Client.Do(req) 104 | if err != nil { 105 | return "", err 106 | } 107 | defer resp.Body.Close() 108 | return digest.Parse(resp.Header.Get("Docker-Content-Digest")) 109 | } 110 | 111 | // DeleteManifest func init 112 | func (registry *Registry) DeleteManifest(repository string, digest digest.Digest) error { 113 | url := registry.url("/v2/%s/manifests/%s", repository, digest) 114 | registry.Logf("registry.manifest.delete url=%s repository=%s reference=%s", url, repository, digest) 115 | 116 | req, err := http.NewRequest("DELETE", url, nil) 117 | if err != nil { 118 | return err 119 | } 120 | resp, err := registry.Client.Do(req) 121 | if resp != nil { 122 | defer resp.Body.Close() 123 | } 124 | if err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | 130 | // PutManifest func init 131 | func (registry *Registry) PutManifest(repository, reference string, signedManifest *manifestV1.SignedManifest) error { 132 | url := registry.url("/v2/%s/manifests/%s", repository, reference) 133 | registry.Logf("registry.manifest.put url=%s repository=%s reference=%s", url, repository, reference) 134 | 135 | body, err := signedManifest.MarshalJSON() 136 | if err != nil { 137 | return err 138 | } 139 | 140 | buffer := bytes.NewBuffer(body) 141 | req, err := http.NewRequest("PUT", url, buffer) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | req.Header.Set("Content-Type", manifestV1.MediaTypeManifest) 147 | resp, err := registry.Client.Do(req) 148 | if resp != nil { 149 | defer resp.Body.Close() 150 | } 151 | return err 152 | } 153 | -------------------------------------------------------------------------------- /pkg/docker/registry/registry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | import ( 6 | "crypto/tls" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // LogfCallback func init 15 | type LogfCallback func(format string, args ...interface{}) 16 | 17 | // Quiet function discards log messages silently 18 | func Quiet(format string, args ...interface{}) { 19 | /* discard logs */ 20 | } 21 | 22 | // Log func passes log messages along to Go's "log" module 23 | func Log(format string, args ...interface{}) { 24 | log.Debug(fmt.Sprintf(format, args...)) 25 | } 26 | 27 | // Registry struct init 28 | type Registry struct { 29 | URL string 30 | Client *http.Client 31 | Logf LogfCallback 32 | } 33 | 34 | // New Func init 35 | /* 36 | * Create a new Registry with the given URL and credentials, then Ping()s it 37 | * before returning it to verify that the Registry is available. 38 | * 39 | * Alternately, you can construct a Registry manually by populating the fields. 40 | * This passes http.DefaultTransport to WrapTransport when creating the 41 | * http.Client. 42 | */ 43 | func New(registryURL, username, password string) (*Registry, error) { 44 | transport := http.DefaultTransport 45 | 46 | return newFromTransport(registryURL, username, password, transport, Log) 47 | } 48 | 49 | // NewInsecure func init 50 | /* 51 | * Create a new Registry, as with New, using an http.Transport that disables 52 | * SSL certificate verification. 53 | */ 54 | func NewInsecure(registryURL, username, password string) (*Registry, error) { 55 | transport := &http.Transport{ 56 | TLSClientConfig: &tls.Config{ 57 | InsecureSkipVerify: true, 58 | }, 59 | } 60 | 61 | return newFromTransport(registryURL, username, password, transport, Log) 62 | } 63 | 64 | // WrapTransport func init 65 | /* 66 | * Given an existing http.RoundTripper such as http.DefaultTransport, build the 67 | * transport stack necessary to authenticate to the Docker registry API. This 68 | * adds in support for OAuth bearer tokens and HTTP Basic auth, and sets up 69 | * error handling this library relies on. 70 | */ 71 | func WrapTransport(transport http.RoundTripper, url, username, password string) http.RoundTripper { 72 | tokenTransport := &TokenTransport{ 73 | Transport: transport, 74 | Username: username, 75 | Password: password, 76 | } 77 | basicAuthTransport := &BasicTransport{ 78 | Transport: tokenTransport, 79 | URL: url, 80 | Username: username, 81 | Password: password, 82 | } 83 | errorTransport := &ErrorTransport{ 84 | Transport: basicAuthTransport, 85 | } 86 | return errorTransport 87 | } 88 | 89 | func newFromTransport(registryURL, username, password string, transport http.RoundTripper, logf LogfCallback) (*Registry, error) { 90 | url := strings.TrimSuffix(registryURL, "/") 91 | transport = WrapTransport(transport, url, username, password) 92 | registry := &Registry{ 93 | URL: url, 94 | Client: &http.Client{ 95 | Transport: transport, 96 | }, 97 | Logf: logf, 98 | } 99 | 100 | if err := registry.Ping(); err != nil { 101 | return nil, err 102 | } 103 | 104 | return registry, nil 105 | } 106 | 107 | func (r *Registry) url(pathTemplate string, args ...interface{}) string { 108 | pathSuffix := fmt.Sprintf(pathTemplate, args...) 109 | url := fmt.Sprintf("%s%s", r.URL, pathSuffix) 110 | return url 111 | } 112 | 113 | // Ping func init 114 | func (r *Registry) Ping() error { 115 | url := r.url("/v2/") 116 | r.Logf("registry.ping url=%s", url) 117 | resp, err := r.Client.Get(url) 118 | if resp != nil { 119 | defer resp.Body.Close() 120 | } 121 | if err != nil { 122 | return err 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/docker/registry/repositories.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | type repositoriesResponse struct { 6 | Repositories []string `json:"repositories"` 7 | } 8 | 9 | // Repositories func init 10 | func (registry *Registry) Repositories() ([]string, error) { 11 | url := registry.url("/v2/_catalog") 12 | repos := make([]string, 0, 10) 13 | var err error // We create this here, otherwise url will be rescoped with := 14 | var response repositoriesResponse 15 | for { 16 | registry.Logf("registry.repositories url=%s", url) 17 | url, err = registry.getPaginatedJSON(url, &response) 18 | switch err { 19 | case ErrNoMorePages: 20 | repos = append(repos, response.Repositories...) 21 | return repos, nil 22 | case nil: 23 | repos = append(repos, response.Repositories...) 24 | continue 25 | default: 26 | return nil, err 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/docker/registry/tags.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | package registry 4 | 5 | type tagsResponse struct { 6 | Tags []string `json:"tags"` 7 | } 8 | 9 | // Tags func init 10 | func (registry *Registry) Tags(repository string) (tags []string, err error) { 11 | url := registry.url("/v2/%s/tags/list", repository) 12 | 13 | var response tagsResponse 14 | for { 15 | registry.Logf("registry.tags url=%s repository=%s", url, repository) 16 | url, err = registry.getPaginatedJSON(url, &response) 17 | switch err { 18 | case ErrNoMorePages: 19 | tags = append(tags, response.Tags...) 20 | return tags, nil 21 | case nil: 22 | tags = append(tags, response.Tags...) 23 | continue 24 | default: 25 | return nil, err 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/docker/registry/tokentransport.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Salesforce.com, Inc. All rights reserved. 2 | 3 | // Copyright (c) 2018 Target Brands, Inc. 4 | 5 | package registry 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | // TokenTransport struct init 15 | type TokenTransport struct { 16 | Transport http.RoundTripper 17 | Username string 18 | Password string 19 | } 20 | 21 | // RoundTrip func init 22 | func (t *TokenTransport) RoundTrip(req *http.Request) (*http.Response, error) { 23 | resp, err := t.Transport.RoundTrip(req) 24 | if err != nil { 25 | return resp, err 26 | } 27 | if AuthService := isTokenDemand(resp); AuthService != nil { 28 | if resp != nil { 29 | resp.Body.Close() 30 | } 31 | resp, err = t.authAndRetry(AuthService, req) 32 | } 33 | return resp, err 34 | } 35 | 36 | type authToken struct { 37 | Token string `json:"token"` 38 | } 39 | 40 | func (t *TokenTransport) authAndRetry(AuthService *AuthService, req *http.Request) (*http.Response, error) { 41 | token, authResp, err := t.auth(AuthService) 42 | if err != nil { 43 | return authResp, err 44 | } 45 | 46 | retryResp, err := t.retry(req, token) 47 | return retryResp, err 48 | } 49 | 50 | func (t *TokenTransport) auth(AuthService *AuthService) (string, *http.Response, error) { 51 | authReq, err := AuthService.Request(t.Username, t.Password) 52 | if err != nil { 53 | return "", nil, err 54 | } 55 | 56 | client := http.Client{ 57 | Transport: t.Transport, 58 | } 59 | 60 | response, err := client.Do(authReq) 61 | if err != nil { 62 | return "", nil, err 63 | } 64 | 65 | if response.StatusCode != http.StatusOK { 66 | return "", response, err 67 | } 68 | defer response.Body.Close() 69 | 70 | var myAuthToken authToken 71 | decoder := json.NewDecoder(response.Body) 72 | err = decoder.Decode(&myAuthToken) 73 | if err != nil { 74 | return "", nil, err 75 | } 76 | 77 | return myAuthToken.Token, nil, nil 78 | } 79 | 80 | func (t *TokenTransport) retry(req *http.Request, token string) (*http.Response, error) { 81 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 82 | resp, err := t.Transport.RoundTrip(req) 83 | return resp, err 84 | } 85 | 86 | // AuthService struct init 87 | type AuthService struct { 88 | Realm string 89 | Service string 90 | Scope string 91 | } 92 | 93 | // Request func init 94 | func (AuthService *AuthService) Request(username, password string) (*http.Request, error) { 95 | url, err := url.Parse(AuthService.Realm) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | q := url.Query() 101 | q.Set("service", AuthService.Service) 102 | if AuthService.Scope != "" { 103 | q.Set("scope", AuthService.Scope) 104 | } 105 | url.RawQuery = q.Encode() 106 | 107 | request, err := http.NewRequest("GET", url.String(), nil) 108 | 109 | if username != "" || password != "" { 110 | request.SetBasicAuth(username, password) 111 | } 112 | 113 | return request, err 114 | } 115 | 116 | func isTokenDemand(resp *http.Response) *AuthService { 117 | if resp == nil { 118 | return nil 119 | } 120 | if resp.StatusCode != http.StatusUnauthorized { 121 | return nil 122 | } 123 | return ParseOauthHeader(resp) 124 | } 125 | 126 | // ParseOauthHeader func init 127 | func ParseOauthHeader(resp *http.Response) *AuthService { 128 | challenges := ParseAuthHeader(resp.Header) 129 | for _, challenge := range challenges { 130 | if challenge.Scheme == "bearer" { 131 | return &AuthService{ 132 | Realm: challenge.Parameters["realm"], 133 | Service: challenge.Parameters["service"], 134 | Scope: challenge.Parameters["scope"], 135 | } 136 | } 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/formatter/formatter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package formatter 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "runtime" 21 | "strings" 22 | 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | // JSONExtendedFormatter formats log information to JSON format with time and line number in file 27 | type JSONExtendedFormatter struct { 28 | ShowLn bool 29 | } 30 | 31 | // Format func init 32 | func (f *JSONExtendedFormatter) Format(entry *logrus.Entry) ([]byte, error) { 33 | // Because entry.Data is not concurrent write safe, we need to copy the dictionary 34 | data := make(logrus.Fields, len(entry.Data)+4) 35 | 36 | for k, v := range entry.Data { 37 | switch v := v.(type) { 38 | case error: 39 | // Otherwise errors are ignored by `encoding/json` 40 | // https://github.com/sirupsen/logrus/issues/137 41 | data[k] = v.Error() 42 | default: 43 | data[k] = v 44 | } 45 | } 46 | 47 | if f.ShowLn { 48 | var ( 49 | filename = "???" 50 | filepath string 51 | line int 52 | ok = true 53 | ) 54 | // Worst case is O(call stack size) 55 | for depth := 3; ok; depth++ { 56 | _, filepath, line, ok = runtime.Caller(depth) 57 | if !ok { 58 | line = 0 59 | filename = "???" 60 | break 61 | } else if !strings.Contains(filepath, "logrus") { 62 | if line < 0 { 63 | line = 0 64 | } 65 | slash := strings.LastIndex(filepath, "/") 66 | if slash >= 0 { 67 | filename = filepath[slash+1:] 68 | } else { 69 | filename = filepath 70 | } 71 | break 72 | } 73 | } 74 | data["Location"] = fmt.Sprintf("%s:%d", filename, line) 75 | } 76 | now := entry.Time 77 | ts := now.Format("2006-01-02 15:04:05") 78 | ms := now.Nanosecond() / 1000 79 | 80 | data["Time"] = fmt.Sprintf("%s.%06d", ts, ms) 81 | data["Event"] = entry.Message 82 | data["Level"] = entry.Level.String() 83 | 84 | serialized, err := json.Marshal(data) 85 | if err != nil { 86 | return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) 87 | } 88 | return append(serialized, '\n'), nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/stopper/stopper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 clair authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package stopper 16 | 17 | import ( 18 | "sync" 19 | "time" 20 | ) 21 | 22 | // Stopper eases the graceful termination of a group of goroutines 23 | type Stopper struct { 24 | wg sync.WaitGroup 25 | stop chan struct{} 26 | } 27 | 28 | // NewStopper initializes a new Stopper instance 29 | func NewStopper() *Stopper { 30 | return &Stopper{stop: make(chan struct{}, 0)} 31 | } 32 | 33 | // Begin indicates that a new goroutine has started 34 | func (s *Stopper) Begin() { 35 | s.wg.Add(1) 36 | } 37 | 38 | // End indicates that a goroutine has stopped 39 | func (s *Stopper) End() { 40 | s.wg.Done() 41 | } 42 | 43 | // Chan returns the channel on which goroutines could listen to determine if 44 | // they should stop. The channel is closed when Stop() is called. 45 | func (s *Stopper) Chan() chan struct{} { 46 | return s.stop 47 | } 48 | 49 | // Sleep puts the current goroutine on sleep during a duration d. 50 | // Sleep could be interrupted in the case the goroutine should stop itself, 51 | // in which case Sleep returns false. 52 | func (s *Stopper) Sleep(d time.Duration) bool { 53 | select { 54 | case <-time.After(d): 55 | return true 56 | case <-s.stop: 57 | return false 58 | } 59 | } 60 | 61 | // Stop asks every goroutine to end 62 | func (s *Stopper) Stop() { 63 | close(s.stop) 64 | s.wg.Wait() 65 | } 66 | --------------------------------------------------------------------------------