├── .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 | [](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.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 | 
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 |
--------------------------------------------------------------------------------