├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── go.mod ├── go.sum ├── internal ├── multirange │ ├── multirange.go │ └── multirange_test.go ├── nmap │ └── nmap.go └── report │ └── report.go ├── main.go ├── queries ├── most_recent_import.sql ├── rfc1918.sql └── security_groups.sql └── templates ├── nmap_scan.html ├── run_scan.sh ├── scan_group.gosh └── security_groups.gohtml /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | dist/ 17 | .vscode/ 18 | output/ 19 | 20 | # Generated code for including files/dirs 21 | pkged.go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. “Contributor” 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. “Contributor Version” 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor’s Contribution. 14 | 15 | 1.3. “Contribution” 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. “Covered Software” 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. “Incompatible With Secondary Licenses” 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of version 33 | 1.1 or earlier of the License, but not also under the terms of a 34 | Secondary License. 35 | 36 | 1.6. “Executable Form” 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. “Larger Work” 41 | 42 | means a work that combines Covered Software with other material, in a separate 43 | file or files, that is not Covered Software. 44 | 45 | 1.8. “License” 46 | 47 | means this document. 48 | 49 | 1.9. “Licensable” 50 | 51 | means having the right to grant, to the maximum extent possible, whether at the 52 | time of the initial grant or subsequently, any and all of the rights conveyed by 53 | this License. 54 | 55 | 1.10. “Modifications” 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, deletion 60 | from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. “Patent Claims” of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, process, 67 | and apparatus claims, in any patent Licensable by such Contributor that 68 | would be infringed, but for the grant of the License, by the making, 69 | using, selling, offering for sale, having made, import, or transfer of 70 | either its Contributions or its Contributor Version. 71 | 72 | 1.12. “Secondary License” 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. “Source Code Form” 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. “You” (or “Your”) 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, “You” includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, “control” means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or as 104 | part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its Contributions 108 | or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution become 113 | effective for each Contribution on the date the Contributor first distributes 114 | such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under this 119 | License. No additional rights or licenses will be implied from the distribution 120 | or licensing of Covered Software under this License. Notwithstanding Section 121 | 2.1(b) above, no patent license is granted by a Contributor: 122 | 123 | a. for any code that a Contributor has removed from Covered Software; or 124 | 125 | b. for infringements caused by: (i) Your and any other third party’s 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | c. under Patent Claims infringed by Covered Software in the absence of its 131 | Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, or 134 | logos of any Contributor (except as may be necessary to comply with the 135 | notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this License 141 | (see Section 10.2) or under the terms of a Secondary License (if permitted 142 | under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its Contributions 147 | are its original creation(s) or it has sufficient rights to grant the 148 | rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under applicable 153 | copyright doctrines of fair use, fair dealing, or other equivalents. 154 | 155 | 2.7. Conditions 156 | 157 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 158 | Section 2.1. 159 | 160 | 161 | 3. Responsibilities 162 | 163 | 3.1. Distribution of Source Form 164 | 165 | All distribution of Covered Software in Source Code Form, including any 166 | Modifications that You create or to which You contribute, must be under the 167 | terms of this License. You must inform recipients that the Source Code Form 168 | of the Covered Software is governed by the terms of this License, and how 169 | they can obtain a copy of this License. You may not attempt to alter or 170 | restrict the recipients’ rights in the Source Code Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | a. such Covered Software must also be made available in Source Code Form, 177 | as described in Section 3.1, and You must inform recipients of the 178 | Executable Form how they can obtain a copy of such Source Code Form by 179 | reasonable means in a timely manner, at a charge no more than the cost 180 | of distribution to the recipient; and 181 | 182 | b. You may distribute such Executable Form under the terms of this License, 183 | or sublicense it under different terms, provided that the license for 184 | the Executable Form does not attempt to limit or alter the recipients’ 185 | rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for the 191 | Covered Software. If the Larger Work is a combination of Covered Software 192 | with a work governed by one or more Secondary Licenses, and the Covered 193 | Software is not Incompatible With Secondary Licenses, this License permits 194 | You to additionally distribute such Covered Software under the terms of 195 | such Secondary License(s), so that the recipient of the Larger Work may, at 196 | their option, further distribute the Covered Software under the terms of 197 | either this License or such Secondary License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices (including 202 | copyright notices, patent notices, disclaimers of warranty, or limitations 203 | of liability) contained within the Source Code Form of the Covered 204 | Software, except that You may alter any license notices to the extent 205 | required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on behalf 212 | of any Contributor. You must make it absolutely clear that any such 213 | warranty, support, indemnity, or liability obligation is offered by You 214 | alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | 222 | If it is impossible for You to comply with any of the terms of this License 223 | with respect to some or all of the Covered Software due to statute, judicial 224 | order, or regulation then You must: (a) comply with the terms of this License 225 | to the maximum extent possible; and (b) describe the limitations and the code 226 | they affect. Such description must be placed in a text file included with all 227 | distributions of the Covered Software under this License. Except to the 228 | extent prohibited by statute or regulation, such description must be 229 | sufficiently detailed for a recipient of ordinary skill to be able to 230 | understand it. 231 | 232 | 5. Termination 233 | 234 | 5.1. The rights granted under this License will terminate automatically if You 235 | fail to comply with any of its terms. However, if You become compliant, 236 | then the rights granted under this License from a particular Contributor 237 | are reinstated (a) provisionally, unless and until such Contributor 238 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 239 | if such Contributor fails to notify You of the non-compliance by some 240 | reasonable means prior to 60 days after You have come back into compliance. 241 | Moreover, Your grants from a particular Contributor are reinstated on an 242 | ongoing basis if such Contributor notifies You of the non-compliance by 243 | some reasonable means, this is the first time You have received notice of 244 | non-compliance with this License from such Contributor, and You become 245 | compliant prior to 30 days after Your receipt of the notice. 246 | 247 | 5.2. If You initiate litigation against any entity by asserting a patent 248 | infringement claim (excluding declaratory judgment actions, counter-claims, 249 | and cross-claims) alleging that a Contributor Version directly or 250 | indirectly infringes any patent, then the rights granted to You by any and 251 | all Contributors for the Covered Software under Section 2.1 of this License 252 | shall terminate. 253 | 254 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 255 | license agreements (excluding distributors and resellers) which have been 256 | validly granted by You or Your distributors under this License prior to 257 | termination shall survive termination. 258 | 259 | 6. Disclaimer of Warranty 260 | 261 | Covered Software is provided under this License on an “as is” basis, without 262 | warranty of any kind, either expressed, implied, or statutory, including, 263 | without limitation, warranties that the Covered Software is free of defects, 264 | merchantable, fit for a particular purpose or non-infringing. The entire 265 | risk as to the quality and performance of the Covered Software is with You. 266 | Should any Covered Software prove defective in any respect, You (not any 267 | Contributor) assume the cost of any necessary servicing, repair, or 268 | correction. This disclaimer of warranty constitutes an essential part of this 269 | License. No use of any Covered Software is authorized under this License 270 | except under this disclaimer. 271 | 272 | 7. Limitation of Liability 273 | 274 | Under no circumstances and under no legal theory, whether tort (including 275 | negligence), contract, or otherwise, shall any Contributor, or anyone who 276 | distributes Covered Software as permitted above, be liable to You for any 277 | direct, indirect, special, incidental, or consequential damages of any 278 | character including, without limitation, damages for lost profits, loss of 279 | goodwill, work stoppage, computer failure or malfunction, or any and all 280 | other commercial damages or losses, even if such party shall have been 281 | informed of the possibility of such damages. This limitation of liability 282 | shall not apply to liability for death or personal injury resulting from such 283 | party’s negligence to the extent applicable law prohibits such limitation. 284 | Some jurisdictions do not allow the exclusion or limitation of incidental or 285 | consequential damages, so this exclusion and limitation may not apply to You. 286 | 287 | 8. Litigation 288 | 289 | Any litigation relating to this License may be brought only in the courts of 290 | a jurisdiction where the defendant maintains its principal place of business 291 | and such litigation shall be governed by laws of that jurisdiction, without 292 | reference to its conflict-of-law provisions. Nothing in this Section shall 293 | prevent a party’s ability to bring cross-claims or counter-claims. 294 | 295 | 9. Miscellaneous 296 | 297 | This License represents the complete agreement concerning the subject matter 298 | hereof. If any provision of this License is held to be unenforceable, such 299 | provision shall be reformed only to the extent necessary to make it 300 | enforceable. Any law or regulation which provides that the language of a 301 | contract shall be construed against the drafter shall not be used to construe 302 | this License against a Contributor. 303 | 304 | 305 | 10. Versions of the License 306 | 307 | 10.1. New Versions 308 | 309 | Mozilla Foundation is the license steward. Except as provided in Section 310 | 10.3, no one other than the license steward has the right to modify or 311 | publish new versions of this License. Each version will be given a 312 | distinguishing version number. 313 | 314 | 10.2. Effect of New Versions 315 | 316 | You may distribute the Covered Software under the terms of the version of 317 | the License under which You originally received the Covered Software, or 318 | under the terms of any subsequent version published by the license 319 | steward. 320 | 321 | 10.3. Modified Versions 322 | 323 | If you create software not governed by this License, and you want to 324 | create a new license for such software, you may create and use a modified 325 | version of this License if you rename the license and remove any 326 | references to the name of the license steward (except to note that such 327 | modified license differs from this License). 328 | 329 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 330 | If You choose to distribute Source Code Form that is Incompatible With 331 | Secondary Licenses under the terms of this version of the License, the 332 | notice described in Exhibit B of this License must be attached. 333 | 334 | Exhibit A - Source Code Form License Notice 335 | 336 | This Source Code Form is subject to the 337 | terms of the Mozilla Public License, v. 338 | 2.0. If a copy of the MPL was not 339 | distributed with this file, You can 340 | obtain one at 341 | http://mozilla.org/MPL/2.0/. 342 | 343 | If it is not possible or desirable to put the notice in a particular file, then 344 | You may include the notice in a location (such as a LICENSE file in a relevant 345 | directory) where a recipient would be likely to look for such a notice. 346 | 347 | You may add additional accurate notices of copyright ownership. 348 | 349 | Exhibit B - “Incompatible With Secondary Licenses” Notice 350 | 351 | This Source Code Form is “Incompatible 352 | With Secondary Licenses”, as defined by 353 | the Mozilla Public License, v. 2.0. 354 | 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sgCheckup - Check your Security Groups for Unexpected Open Ports & Generate nmap Output 2 | 3 | ![sgcheckup copy](https://user-images.githubusercontent.com/291215/131573778-34207ba3-35a1-4af4-b3a6-39e32cb806b0.png) 4 | 5 | `sgCheckup` is a tool to scan your AWS Security Groups for a combination of open ports and attached Network Interfaces. The goal is to find anything listening on a port that you wouldn't consider safe. In addition to generating reports for security groups, `sgCheckup` can generate and run `nmap` to get specifics. 6 | 7 | ## Why? 8 | 9 | Security Groups are an important line of defense for your infrastructure, but as you make changes, it's easy to forget to revert some quick fix that was made to get something working. Having a view into what ports are open and what's listening can help you prioritize locking down access. Using `nmap` to pinpoint specifics as well as fingerprint the open ports further aides with context in locking down security groups. 10 | 11 | ## Pre-requisites 12 | 13 | * AWS Credentials (`~/.aws/`, `AWS_*` environment variables, metadata server, etc.) 14 | * Docker 15 | * If running from source, go version >= go1.15 16 | 17 | ## Installation Options 18 | 19 | 1. Download the latest [release](https://github.com/goldfiglabs/sgCheckup/releases): 20 | 21 | Linux: 22 | ``` 23 | curl -Lo sgCheckup https://github.com/goldfiglabs/sgCheckup/releases/latest/download/sgCheckup_linux 24 | chmod a+x ./sgCheckup 25 | ``` 26 | 27 | OSX x86: 28 | ``` 29 | curl -Lo sgCheckup https://github.com/goldfiglabs/sgCheckup/releases/latest/download/sgCheckup_darwin_amd64 30 | chmod a+x ./sgCheckup 31 | ``` 32 | 33 | OSX M1/arm: 34 | ``` 35 | curl -Lo sgCheckup https://github.com/goldfiglabs/sgCheckup/releases/latest/download/sgCheckup_darwin_arm64 36 | chmod a+x ./sgCheckup 37 | ``` 38 | 39 | 2. Run from source: 40 | ``` 41 | git clone https://github.com/goldfiglabs/sgCheckup.git 42 | cd sgCheckup 43 | go run main.go 44 | ``` 45 | 46 | ## Usage 47 | 48 | Run `./sgCheckup` and view the reports generated in `output/`. 49 | 50 | Screen Shot 2021-08-31 at 3 08 35 PM 51 | 52 | `nmap` results are in `output/nmap/` with a summary cross-referencing security groups found in nmap.html: 53 | 54 | Screen Shot 2021-09-01 at 1 54 06 PM 55 | 56 | ## Overview 57 | 58 | sgCheckup uses [goldfiglabs/introspector](https://github.com/goldfiglabs/introspector) to snapshot the Security Groups and Network Interfaces from your AWS Account into a Postgres database. sgCheckup then runs SQL queries to look for Security Groups with open ports and attached Network Interfaces. This list is then used to configure running `nmap` against the targeted list of IPs and ports. The output of nmap is used to determine if a) anything is listening and b) what software is listening on open ports. 59 | 60 | ## Notes 61 | 62 | 1. 2 HTML and CSV reports are provided: one each organized by Security Group, and one each organized by IP/Port combination. 63 | 64 | 1. By default, sgCheckup considers ports 22, 80, and 443 to be open intentionally. You can use the flag `-safePorts ` to override this behavior according to your own policies. Use `--safe-ports ""` to mark all ports unsafe. 65 | 66 | 1. You can skip the nmap phase with `-skip-nmap`. You will still get the report focused on Security Groups, but not the report based on open IP/Port combinations. 67 | 68 | ## License 69 | 70 | Copyright (c) 2021 [Gold Fig Labs Inc.](https://www.goldfiglabs.com/) 71 | 72 | This Source Code Form is subject to the terms of the Mozilla Public License, v.2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 73 | 74 | [Mozilla Public License v2.0](./LICENSE) 75 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | 7 | # Create output folder 8 | mkdir -p $DIR/dist 9 | 10 | cd $DIR 11 | 12 | # Pre-req: go get github.com/markbates/pkger/cmd/pkger 13 | GOBIN="${GOPATH:-${HOME}/go}" 14 | $GOBIN/bin/pkger 15 | 16 | GOOS=darwin GOARCH=amd64 go build -o dist/sgCheckup_darwin_amd64 17 | GOOS=darwin GOARCH=arm64 go build -o dist/sgCheckup_darwin_arm64 18 | 19 | GOOS=linux GOARCH=amd64 go build -o dist/sgCheckup_linux 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module goldfiglabs.com/sgcheckup 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2/config v1.1.1 7 | github.com/docker/docker v20.10.7+incompatible // indirect 8 | github.com/goldfiglabs/go-introspector v0.0.2 9 | github.com/lib/pq v1.9.0 10 | github.com/markbates/pkger v0.17.1 11 | github.com/pkg/errors v0.9.1 12 | github.com/sirupsen/logrus v1.8.1 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 3 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 6 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 7 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 8 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 9 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 10 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 11 | github.com/aws/aws-sdk-go-v2 v1.2.0 h1:BS+UYpbsElC82gB+2E2jiCBg36i8HlubTB/dO/moQ9c= 12 | github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= 13 | github.com/aws/aws-sdk-go-v2/config v1.1.1 h1:ZAoq32boMzcaTW9bcUacBswAmHTbvlvDJICgHFZuECo= 14 | github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= 15 | github.com/aws/aws-sdk-go-v2/credentials v1.1.1 h1:NbvWIM1Mx6sNPTxowHgS2ewXCRp+NGTzUYb/96FZJbY= 16 | github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2 h1:EtEU7WRaWliitZh2nmuxEXrN0Cb8EgPUFGIoTMeqbzI= 18 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2 h1:4AH9fFjUlVktQMznF+YN33aWNXaR4VgDXyP28qokJC0= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.1.1 h1:37QubsarExl5ZuCBlnRP+7l1tNwZPBSTqpTBrPH98RU= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= 23 | github.com/aws/aws-sdk-go-v2/service/sts v1.1.1 h1:TJoIfnIFubCX0ACVeJ0w46HEH5MwjwYN4iFhuYIhfIY= 24 | github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= 25 | github.com/aws/smithy-go v1.1.0 h1:D6CSsM3gdxaGaqXnPgOBCeL6Mophqzu7KJOu7zW78sU= 26 | github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= 27 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 29 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 30 | github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= 31 | github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 32 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 33 | github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 34 | github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= 39 | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 40 | github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= 41 | github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 42 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 43 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 44 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 45 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 46 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 47 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 48 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 49 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 50 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 51 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 52 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 53 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 54 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 55 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 56 | github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= 57 | github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 58 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 59 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 60 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 61 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= 62 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 63 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 64 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 65 | github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= 66 | github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= 67 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 68 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 69 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 70 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 71 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 72 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 73 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 74 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 75 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 76 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 77 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 78 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 79 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 80 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 81 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 82 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 83 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 84 | github.com/goldfiglabs/go-introspector v0.0.2 h1:RjVCgfSuW60zGVWFl828UE6bWp6N5+tZITyHFrKzBtQ= 85 | github.com/goldfiglabs/go-introspector v0.0.2/go.mod h1:DrvbnlMWmwfu4bfo7gnDEhDrQVPwnAvzhzx0ZCLsXX0= 86 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 87 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 88 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 89 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 90 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 91 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 92 | github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= 93 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 95 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 96 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 98 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 99 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 100 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 101 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 102 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 103 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 104 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 105 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 106 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 107 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 108 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 109 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 111 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 112 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 113 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 114 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 115 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 116 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 117 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 118 | github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= 119 | github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 120 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 121 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 122 | github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= 123 | github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= 124 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 125 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 126 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= 127 | github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= 128 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 129 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 130 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 131 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 132 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 133 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 134 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 135 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 136 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 137 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 138 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 139 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 140 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 141 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 142 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 143 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 144 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 145 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 146 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 147 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= 148 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 149 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 150 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 151 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 152 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 153 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 154 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 155 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 156 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 157 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 158 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 159 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 160 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 161 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 162 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 163 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 164 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 165 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 166 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 167 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 168 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 169 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 170 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 171 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 172 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 173 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 174 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 175 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 176 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 177 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 178 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 179 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 180 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 181 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 182 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 183 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 184 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 185 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 187 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 188 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 189 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 190 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 191 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 192 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 193 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 194 | golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM= 195 | golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 196 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 197 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 203 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= 215 | golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 217 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 218 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 219 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 220 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 221 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 222 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= 223 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 224 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 225 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 226 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 227 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 228 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 229 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 230 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 231 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 232 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 233 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 234 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 235 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 236 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 237 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 238 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 239 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 240 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 241 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 242 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 243 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 244 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 245 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 246 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 247 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 248 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 249 | google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= 250 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 251 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 252 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 253 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 254 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 255 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 256 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 257 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 258 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 259 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 260 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 261 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 262 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 263 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 264 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 265 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 266 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 267 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 268 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 269 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 270 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 271 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 272 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 273 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 274 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 275 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 276 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 277 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 278 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 279 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 280 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 281 | k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= 282 | k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= 283 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 284 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 285 | k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= 286 | k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= 287 | k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= 288 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 289 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= 290 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 291 | -------------------------------------------------------------------------------- /internal/multirange/multirange.go: -------------------------------------------------------------------------------- 1 | package multirange 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type intRange struct { 9 | min int 10 | max int 11 | } 12 | 13 | func (i *intRange) Size() int { 14 | return i.max - i.min + 1 15 | } 16 | 17 | func (i *intRange) ToString() string { 18 | s := "[" 19 | s += strconv.Itoa(i.min) 20 | s += "," 21 | s += strconv.Itoa(i.max) 22 | s += "]" 23 | return s 24 | } 25 | 26 | func (i *intRange) Humanize() string { 27 | if i.min == i.max { 28 | return strconv.Itoa(i.min) 29 | } 30 | s := strconv.Itoa(i.min) 31 | s += "-" 32 | s += strconv.Itoa(i.max) 33 | return s 34 | } 35 | 36 | func (i *intRange) Remove(el int) []intRange { 37 | if !i.Contains(el) { 38 | return []intRange{*i} 39 | } 40 | if i.min < el { 41 | if el < i.max { 42 | return []intRange{{ 43 | min: i.min, 44 | max: el - 1, 45 | }, { 46 | min: el + 1, 47 | max: i.max, 48 | }} 49 | } 50 | // el == i.max 51 | return []intRange{{ 52 | min: i.min, 53 | max: el - 1, 54 | }} 55 | } 56 | // el == i.min 57 | if el < i.max { 58 | return []intRange{{ 59 | min: el + 1, 60 | max: i.max, 61 | }} 62 | } 63 | // el == i.min == i.max 64 | return []intRange{} 65 | } 66 | 67 | func (i *intRange) Contains(el int) bool { 68 | return el >= i.min && el <= i.max 69 | } 70 | 71 | func (i *intRange) LessThan(other *intRange) bool { 72 | return i.max < other.min 73 | } 74 | 75 | func max(x, y int) int { 76 | if x < y { 77 | return y 78 | } 79 | return x 80 | } 81 | 82 | func min(x, y int) int { 83 | if x > y { 84 | return y 85 | } 86 | return x 87 | } 88 | 89 | func (i *intRange) Overlaps(other *intRange) (overlaps bool, combined *intRange) { 90 | if i.min < other.min { 91 | // account for adjacency 92 | if i.max >= other.min-1 { 93 | 94 | } else { 95 | return false, nil 96 | } 97 | } else if i.min == other.min { 98 | // definitely overlap 99 | } else { // i.min > other.min 100 | // account for adjacency 101 | if i.min <= other.max+1 { 102 | 103 | } else { 104 | return false, nil 105 | } 106 | } 107 | min := min(i.min, other.min) 108 | max := max(i.max, other.max) 109 | return true, &intRange{ 110 | min: min, 111 | max: max, 112 | } 113 | } 114 | 115 | func newIntRangeFromString(s string) (*intRange, error) { 116 | minInclusive := s[0] == '[' 117 | maxInclusive := s[len(s)-1] == ']' 118 | boundsString := s[1 : len(s)-1] 119 | bounds := strings.Split(boundsString, ",") 120 | min, err := strconv.Atoi(bounds[0]) 121 | if err != nil { 122 | return nil, err 123 | } 124 | if !minInclusive { 125 | min++ 126 | } 127 | max, err := strconv.Atoi(bounds[1]) 128 | if err != nil { 129 | return nil, err 130 | } 131 | if !maxInclusive { 132 | max-- 133 | } 134 | return &intRange{ 135 | min: min, 136 | max: max, 137 | }, nil 138 | } 139 | 140 | type MultiRange struct { 141 | ranges []intRange 142 | } 143 | 144 | func (m *MultiRange) Size() int { 145 | sum := 0 146 | for _, r := range m.ranges { 147 | sum += r.Size() 148 | } 149 | return sum 150 | } 151 | 152 | func (m *MultiRange) ToString() string { 153 | ranges := []string{} 154 | for _, r := range m.ranges { 155 | ranges = append(ranges, r.ToString()) 156 | } 157 | return strings.Join(ranges, ",") 158 | } 159 | 160 | func (m *MultiRange) Humanize() string { 161 | ranges := []string{} 162 | for _, r := range m.ranges { 163 | ranges = append(ranges, r.Humanize()) 164 | } 165 | return strings.Join(ranges, ",") 166 | } 167 | 168 | func FromString(s string) (*MultiRange, error) { 169 | parts := strings.Split(s, ",") 170 | mr := &MultiRange{[]intRange{}} 171 | for i := 0; i < len(parts); i += 2 { 172 | intRange, err := newIntRangeFromString(parts[i] + "," + parts[i+1]) 173 | if err != nil { 174 | return nil, err 175 | } 176 | mr.appendRange(*intRange) 177 | } 178 | return mr, nil 179 | } 180 | 181 | func (m *MultiRange) appendRange(r intRange) { 182 | ranges := []intRange{} 183 | accum := r 184 | appended := false 185 | for i, existing := range m.ranges { 186 | if accum.LessThan(&existing) { 187 | ranges = append(ranges, accum) 188 | ranges = append(ranges, m.ranges[i:]...) 189 | appended = true 190 | break 191 | } else if overlaps, combined := accum.Overlaps(&existing); overlaps { 192 | accum = *combined 193 | } else { 194 | ranges = append(ranges, existing) 195 | } 196 | } 197 | if !appended { 198 | ranges = append(ranges, accum) 199 | } 200 | m.ranges = ranges 201 | } 202 | 203 | func (m *MultiRange) RemoveElement(el int) { 204 | ranges := []intRange{} 205 | for i, r := range m.ranges { 206 | if r.Contains(el) { 207 | split := r.Remove(el) 208 | ranges = append(ranges, split...) 209 | ranges = append(ranges, m.ranges[i+1:]...) 210 | break 211 | } else { 212 | ranges = append(ranges, r) 213 | } 214 | } 215 | m.ranges = ranges 216 | } 217 | -------------------------------------------------------------------------------- /internal/multirange/multirange_test.go: -------------------------------------------------------------------------------- 1 | package multirange_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "goldfiglabs.com/sgcheckup/internal/multirange" 7 | ) 8 | 9 | func TestConstructMultiRange(t *testing.T) { 10 | cases := []string{ 11 | "[22,24]", 12 | "[22,22],[23,24]", 13 | "[22,23),[23,24),[23,24]", 14 | } 15 | want := "[22,24]" 16 | for _, testcase := range cases { 17 | mr, err := multirange.FromString(testcase) 18 | if err != nil { 19 | t.Errorf("Failed %v", err) 20 | } 21 | got := mr.ToString() 22 | if got != want { 23 | t.Errorf("MultiRange(%v) = %v, want %v", testcase, got, want) 24 | } 25 | } 26 | } 27 | 28 | func TestRemoveElement(t *testing.T) { 29 | cases := []struct { 30 | input string 31 | el int 32 | want string 33 | }{ 34 | { 35 | input: "[22,22]", 36 | el: 22, 37 | want: "", 38 | }, 39 | { 40 | input: "[22,23]", 41 | el: 22, 42 | want: "[23,23]", 43 | }, 44 | { 45 | input: "[22,23]", 46 | el: 24, 47 | want: "[22,23]", 48 | }, 49 | { 50 | input: "[0,5],[10,15]", 51 | el: 12, 52 | want: "[0,5],[10,11],[13,15]", 53 | }, 54 | { 55 | input: "[0,5],[10,15],[20,24]", 56 | el: 12, 57 | want: "[0,5],[10,11],[13,15],[20,24]", 58 | }, 59 | } 60 | for _, testcase := range cases { 61 | mr, err := multirange.FromString(testcase.input) 62 | if err != nil { 63 | t.Errorf("Failed %v", err) 64 | } 65 | mr.RemoveElement(testcase.el) 66 | got := mr.ToString() 67 | if got != testcase.want { 68 | t.Errorf("MultiRange(%v).RemoveElement(%v) = %v, want %v", 69 | testcase.input, testcase.input, got, testcase.want) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/nmap/nmap.go: -------------------------------------------------------------------------------- 1 | package nmap 2 | 3 | import ( 4 | "encoding/xml" 5 | "html/template" 6 | "io/ioutil" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/docker/docker/api/types" 12 | "github.com/docker/docker/api/types/container" 13 | "github.com/docker/docker/api/types/mount" 14 | "github.com/docker/docker/api/types/network" 15 | log "github.com/sirupsen/logrus" 16 | 17 | ds "github.com/goldfiglabs/go-introspector/dockersession" 18 | "github.com/markbates/pkger" 19 | "github.com/markbates/pkger/pkging" 20 | "github.com/pkg/errors" 21 | "goldfiglabs.com/sgcheckup/internal/report" 22 | ) 23 | 24 | const defaultNMapDockerRef = "jefftadashi/nmap" 25 | 26 | func copyRunScan(outputDir string) error { 27 | src := "/templates/run_scan.sh" 28 | f, err := pkger.Open(src) 29 | if err != nil { 30 | return errors.Wrap(err, "Failed to load run all shell script") 31 | } 32 | defer f.Close() 33 | dstFilename := outputDir + "/run_scan.sh" 34 | return copyTemplate(f, dstFilename) 35 | } 36 | 37 | func copyTemplate(f pkging.File, dstFilename string) error { 38 | dstFile, err := os.Create(dstFilename) 39 | if err != nil { 40 | return errors.Wrapf(err, "Failed to create output file %v", dstFilename) 41 | } 42 | defer dstFile.Close() 43 | err = dstFile.Chmod(0755) 44 | if err != nil { 45 | return errors.Wrapf(err, "Failed setting file permissions on %v", dstFilename) 46 | } 47 | _, err = dstFile.ReadFrom(f) 48 | if err != nil { 49 | return errors.Wrap(err, "Failed to write shell script") 50 | } 51 | return nil 52 | } 53 | 54 | func RunScan(ds *ds.Session, outputDir string, report *report.Report) (interface{}, error) { 55 | volumeRoot := outputDir + "/nmap" 56 | if _, err := os.Stat(volumeRoot); os.IsNotExist(err) { 57 | err = os.Mkdir(volumeRoot, 0755) 58 | if err != nil { 59 | return nil, errors.Wrap(err, "Failed to create nmap docker volume directory") 60 | } 61 | } 62 | err := copyRunScan(volumeRoot) 63 | if err != nil { 64 | return nil, errors.Wrap(err, "Failed to copy run_scan") 65 | } 66 | dir, err := os.Getwd() 67 | if err != nil { 68 | return nil, errors.Wrap(err, "Failed to get working directory") 69 | } 70 | written, err := writeGroupScans(volumeRoot, report) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "Failed to write scan scripts") 73 | } 74 | if written { 75 | _, err = runNMap(ds, dir+"/"+volumeRoot) 76 | if err != nil { 77 | return nil, errors.Wrap(err, "Failed to run nmap scan") 78 | } 79 | } else { 80 | log.Warn("Skipping NMap, no groups had public IPs and unsafe ports") 81 | } 82 | return nil, nil 83 | } 84 | 85 | type groupScanTemplateData struct { 86 | Bin string 87 | OutputFile string 88 | Ports string 89 | Targets string 90 | Speed string 91 | } 92 | 93 | func writeGroupScans(outputDir string, report *report.Report) (bool, error) { 94 | scriptDir := outputDir + "/groups" 95 | if _, err := os.Stat(scriptDir); os.IsNotExist(err) { 96 | err = os.Mkdir(scriptDir, 0755) 97 | if err != nil { 98 | return false, errors.Wrap(err, "Failed to create groups directory") 99 | } 100 | } 101 | resultsDir := outputDir + "/results" 102 | if _, err := os.Stat(resultsDir); os.IsNotExist(err) { 103 | err = os.Mkdir(resultsDir, 0755) 104 | if err != nil { 105 | return false, errors.Wrap(err, "Failed to create results directory") 106 | } 107 | } 108 | templateFilename := "/templates/scan_group.gosh" 109 | f, err := pkger.Open(templateFilename) 110 | if err != nil { 111 | return false, errors.Wrap(err, "Failed to open group template file") 112 | } 113 | defer f.Close() 114 | bytes, err := ioutil.ReadAll(f) 115 | if err != nil { 116 | return false, errors.Wrap(err, "Failed to read group template file") 117 | } 118 | t := template.New("nmap-security-group") 119 | t, err = t.Parse(string(bytes)) 120 | if err != nil { 121 | return false, errors.Wrap(err, "Failed to load group template") 122 | } 123 | written := false 124 | for _, row := range report.Rows { 125 | if len(row.PublicIps) == 0 || row.UnsafePorts.Size() == 0 { 126 | continue 127 | } 128 | written = true 129 | filename := scriptDir + "/" + row.GroupID + ".sh" 130 | err = writeScanGroup(t, filename, &row) 131 | if err != nil { 132 | return false, err 133 | } 134 | } 135 | return written, nil 136 | } 137 | 138 | func writeScanGroup(t *template.Template, filename string, row *report.Row) error { 139 | outputFile, err := os.Create(filename) 140 | if err != nil { 141 | return errors.Wrapf(err, "Failed to create file %v", filename) 142 | } 143 | defer outputFile.Close() 144 | err = outputFile.Chmod(0755) 145 | if err != nil { 146 | return errors.Wrapf(err, "Failed to chmod file %v", filename) 147 | } 148 | speed := "2" 149 | if row.UnsafePorts.Size() * len(row.PublicIps) > 20 { 150 | speed = "4" 151 | } 152 | t.Execute(outputFile, &groupScanTemplateData{ 153 | Bin: "nmap", 154 | OutputFile: "/opt/sgCheckup/results/" + row.GroupID + ".xml", 155 | Targets: strings.Join(row.PublicIps, " "), 156 | Ports: "-p T:" + row.UnsafePorts.Humanize(), 157 | Speed: speed, 158 | }) 159 | if err != nil { 160 | return errors.Wrapf(err, "Failed to execute template for %v", filename) 161 | } 162 | return nil 163 | } 164 | 165 | func runNMap(ds *ds.Session, volumeRoot string) (interface{}, error) { 166 | ref := "jefftadashi/nmap" 167 | err := ds.RequireImage(ref) 168 | if err != nil { 169 | return nil, errors.Wrapf(err, "Failed to load docker image %v", ref) 170 | } 171 | containerName := "sgCheckup-nmap" 172 | existingContainer, err := ds.FindContainer(containerName) 173 | if err != nil { 174 | return nil, errors.Wrap(err, "Failed to list existing containers") 175 | } 176 | if existingContainer != nil { 177 | err = ds.StopAndRemoveContainer(existingContainer.ID) 178 | if err != nil { 179 | return nil, errors.Wrap(err, "Failed to remove existing container") 180 | } 181 | } 182 | nmapContainer, err := ds.Client.ContainerCreate(ds.Ctx, &container.Config{ 183 | Image: ref, 184 | Cmd: []string{"/opt/sgCheckup/run_scan.sh"}, 185 | Entrypoint: []string{}, 186 | }, &container.HostConfig{ 187 | NetworkMode: "host", 188 | Mounts: []mount.Mount{ 189 | { 190 | Type: mount.TypeBind, 191 | Source: volumeRoot, 192 | Target: "/opt/sgCheckup", 193 | }, 194 | }, 195 | }, &network.NetworkingConfig{}, 196 | nil, 197 | containerName, 198 | ) 199 | if err != nil { 200 | return nil, errors.Wrap(err, "Failed to create nmap container") 201 | } 202 | err = ds.Client.ContainerStart(ds.Ctx, nmapContainer.ID, types.ContainerStartOptions{}) 203 | if err != nil { 204 | return nil, errors.Wrap(err, "Failed to start nmap container") 205 | } 206 | ch, errCh := ds.Client.ContainerWait(ds.Ctx, nmapContainer.ID, container.WaitConditionNextExit) 207 | select { 208 | case <-ch: 209 | break 210 | case err := <-errCh: 211 | return nil, errors.Wrap(err, "Failed waiting for container") 212 | 213 | case <-ds.Ctx.Done(): 214 | return nil, errors.Wrap(ds.Ctx.Err(), "Context failed waiting for container") 215 | } 216 | return nil, nil 217 | } 218 | 219 | type xmlHostname struct { 220 | Name string `xml:"name,attr"` 221 | } 222 | 223 | type xmlHostnames struct { 224 | Hostname []xmlHostname `xml:"hostname"` 225 | } 226 | 227 | type xmlHostStatus struct { 228 | State string `xml:"state,attr"` 229 | } 230 | 231 | type xmlHostAddress struct { 232 | Addr string `xml:"addr,attr"` 233 | } 234 | 235 | type xmlPortState struct { 236 | State string `xml:"state,attr"` 237 | } 238 | 239 | type xmlService struct { 240 | Name string `xml:"name,attr"` 241 | Product string `xml:"product,attr"` 242 | Version string `xml:"version,attr"` 243 | } 244 | 245 | func (s *xmlService) Display() string { 246 | d := s.Name 247 | if s.Product != "" { 248 | d += " - " + s.Product 249 | if s.Version != "" { 250 | d += " (" + s.Version + ")" 251 | } 252 | } 253 | return d 254 | } 255 | 256 | type xmlPort struct { 257 | PortID string `xml:"portid,attr"` 258 | State xmlPortState `xml:"state"` 259 | Service *xmlService `xml:"service"` 260 | } 261 | 262 | func (p *xmlPort) ToScanResult() PortScanResult { 263 | return PortScanResult{ 264 | Service: p.Service, 265 | Status: p.State.State, 266 | SecurityGroups: []string{}, 267 | } 268 | } 269 | 270 | type xmlHostPorts struct { 271 | Port []xmlPort `xml:"port"` 272 | } 273 | 274 | type xmlScanHost struct { 275 | Status xmlHostStatus `xml:"status"` 276 | Address xmlHostAddress `xml:"address"` 277 | Hostnames xmlHostnames `xml:"hostnames"` 278 | Ports xmlHostPorts `xml:"ports"` 279 | } 280 | 281 | func (sh *xmlScanHost) ToGroupHostResult() (string, *groupHostResult, error) { 282 | hostnames := []string{} 283 | for _, hostname := range sh.Hostnames.Hostname { 284 | hostnames = append(hostnames, hostname.Name) 285 | } 286 | ports := map[uint16]PortScanResult{} 287 | for _, port := range sh.Ports.Port { 288 | port64, err := strconv.ParseUint(port.PortID, 10, 16) 289 | if err != nil { 290 | return "", nil, errors.Wrapf(err, "Failed to parse port %v", port.PortID) 291 | } 292 | ports[uint16(port64)] = port.ToScanResult() 293 | } 294 | return sh.Address.Addr, &groupHostResult{ 295 | Hostnames: hostnames, 296 | Ports: ports, 297 | }, nil 298 | } 299 | 300 | type xmlNMapRun struct { 301 | Hosts []xmlScanHost `xml:"host"` 302 | } 303 | 304 | type groupHostResult struct { 305 | Ports map[uint16]PortScanResult 306 | Hostnames []string 307 | } 308 | 309 | type groupScanResult = map[string]groupHostResult 310 | 311 | func readScanResult(filename string) (groupScanResult, error) { 312 | bytes, err := ioutil.ReadFile(filename) 313 | if err != nil { 314 | return nil, errors.Wrapf(err, "Failed to read %v", filename) 315 | } 316 | nmr := xmlNMapRun{} 317 | err = xml.Unmarshal(bytes, &nmr) 318 | if err != nil { 319 | return nil, errors.Wrapf(err, "xml unmarshall failed for %v", filename) 320 | } 321 | results := make(groupScanResult) 322 | for _, h := range nmr.Hosts { 323 | addr, result, err := h.ToGroupHostResult() 324 | if err != nil { 325 | return nil, err 326 | } 327 | results[addr] = *result 328 | } 329 | return results, nil 330 | } 331 | 332 | // Result types 333 | type IPAddr = string 334 | 335 | type PortScanResult struct { 336 | Status string 337 | Service *xmlService 338 | SecurityGroups []string 339 | } 340 | 341 | type PortMap = map[uint16]PortScanResult 342 | type IpScanResults = map[IPAddr]PortMap 343 | 344 | func ReadScanResults(resultsDir string) (IpScanResults, error) { 345 | files, err := ioutil.ReadDir(resultsDir) 346 | if err != nil { 347 | return nil, errors.Wrapf(err, "Failed to list %v", resultsDir) 348 | } 349 | results := make(IpScanResults) 350 | for _, file := range files { 351 | parts := strings.Split(file.Name(), "/") 352 | groupName := strings.TrimSuffix(parts[len(parts)-1], ".xml") 353 | groupResult, err := readScanResult(resultsDir + "/" + file.Name()) 354 | if err != nil { 355 | return nil, errors.Wrapf(err, "Failed to read %v", file.Name()) 356 | } 357 | for addr := range groupResult { 358 | newResults := groupResult[addr] 359 | existingResults, exists := results[addr] 360 | if !exists { 361 | existingResults = make(map[uint16]PortScanResult) 362 | } 363 | for port, scanResult := range newResults.Ports { 364 | existingPortScanResults, ok := existingResults[port] 365 | if !ok { 366 | scanResult.SecurityGroups = append(scanResult.SecurityGroups, groupName) 367 | existingPortScanResults = scanResult 368 | } else { 369 | existingPortScanResults.SecurityGroups = append(existingPortScanResults.SecurityGroups, groupName) 370 | } 371 | existingResults[port] = existingPortScanResults 372 | } 373 | results[addr] = existingResults 374 | } 375 | } 376 | return results, nil 377 | } 378 | -------------------------------------------------------------------------------- /internal/report/report.go: -------------------------------------------------------------------------------- 1 | package report 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | "github.com/lib/pq" 13 | "github.com/markbates/pkger" 14 | "github.com/pkg/errors" 15 | "goldfiglabs.com/sgcheckup/internal/multirange" 16 | 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type PairedGroup struct { 21 | Name string 22 | URI string 23 | ToPort int 24 | FromPort int 25 | IpProtocol string 26 | GroupId string 27 | } 28 | 29 | type PairedGroups []PairedGroup 30 | type ExternalSecurityGroups map[string]PairedGroup 31 | 32 | func (pairedGroups *PairedGroups) Scan(value interface{}) error { 33 | switch v := value.(type) { 34 | case []byte: 35 | return json.Unmarshal(v, pairedGroups) 36 | case string: 37 | return json.Unmarshal([]byte(v), pairedGroups) 38 | case nil: 39 | *pairedGroups = []PairedGroup{} 40 | return nil 41 | } 42 | return errors.New("type assertion failed") 43 | } 44 | 45 | func (externalGroups *ExternalSecurityGroups) Scan(value interface{}) error { 46 | switch v := value.(type) { 47 | case []byte: 48 | return json.Unmarshal(v, externalGroups) 49 | case string: 50 | return json.Unmarshal([]byte(v), externalGroups) 51 | case nil: 52 | *externalGroups = map[string]PairedGroup{} 53 | return nil 54 | } 55 | return errors.New("type assertion failed") 56 | } 57 | 58 | type securityGroupRow struct { 59 | arn string 60 | groupName string 61 | groupID string 62 | ips []string 63 | inUse bool 64 | isDefault bool 65 | portRanges []string 66 | isLargePublicBlock bool 67 | largeRangeCount bool 68 | isRestricted bool 69 | internalOnly bool 70 | pairedSecurityGroups PairedGroups 71 | externalGroups ExternalSecurityGroups 72 | } 73 | 74 | func consoleUrl(arn string) string { 75 | parts := strings.Split(arn, ":") 76 | partition := parts[1] 77 | var console string 78 | if partition == "aws" { 79 | console = "console.aws.amazon.com" 80 | } else if partition == "aws-us-gov" { 81 | console = "console.amazonaws-us-gov.com" 82 | } else { 83 | console = "console.amazonaws.cn" 84 | } 85 | region := parts[3] 86 | groupID := strings.Split(parts[5], "/")[1] 87 | return "https://" + region + "." + console + "/ec2/v2/home?region=" + region + "#SecurityGroup:groupId=" + groupID 88 | } 89 | 90 | func (r *securityGroupRow) isProblematic() bool { 91 | if r.largeRangeCount { 92 | return true 93 | } 94 | if r.isLargePublicBlock { 95 | return true 96 | } 97 | return false 98 | } 99 | 100 | func (r *securityGroupRow) unsafePorts(safePorts []int) (*multirange.MultiRange, error) { 101 | if len(r.portRanges) > 0 { 102 | mr, err := multirange.FromString(r.portRanges[0]) 103 | if err != nil { 104 | return nil, errors.Wrapf(err, "Failed to parse port range %v", r.portRanges) 105 | } 106 | for _, port := range safePorts { 107 | mr.RemoveElement(port) 108 | } 109 | return mr, nil 110 | } 111 | return &multirange.MultiRange{}, nil 112 | } 113 | 114 | func (r *securityGroupRow) notes(unsafePorts *multirange.MultiRange) []string { 115 | notes := []string{} 116 | if unsafePorts.Size() > 0 && !r.internalOnly { 117 | notes = append(notes, fmt.Sprintf("Allows traffic from anywhere on TCP ports (%v)", unsafePorts.Humanize())) 118 | } 119 | if r.isLargePublicBlock { 120 | notes = append(notes, "Has IP restrictions, but they let through large ranges") 121 | } 122 | if r.largeRangeCount { 123 | notes = append(notes, "Uses a lot of IP Ranges") 124 | } 125 | if !r.inUse { 126 | notes = append(notes, "Not in use") 127 | } 128 | if len(r.ips) > 0 { 129 | notes = append(notes, fmt.Sprintf("Contains %v public IP address(es)", len(r.ips))) 130 | } else { 131 | notes = append(notes, "No public IP addresses found") 132 | } 133 | return notes 134 | } 135 | 136 | type Row struct { 137 | Arn string 138 | Url string 139 | Name string 140 | GroupID string 141 | Status string 142 | PublicIps []string 143 | InUse bool 144 | IsDefault bool 145 | Notes []string 146 | UnsafePorts *multirange.MultiRange 147 | } 148 | 149 | // Metadata includes information about the report, such as when the data was 150 | // snapshotted and for what account 151 | type Metadata struct { 152 | Imported time.Time 153 | Generated time.Time 154 | Account string 155 | Organization string 156 | SafePorts []int 157 | } 158 | 159 | type Report struct { 160 | Metadata *Metadata 161 | Rows []Row 162 | } 163 | 164 | type GenerateOpts struct { 165 | SafePorts []int 166 | } 167 | 168 | func (o *GenerateOpts) fillInDefaults() { 169 | if o.SafePorts == nil { 170 | o.SafePorts = []int{} 171 | } 172 | } 173 | 174 | // Generate uses a connection string to postgres and a list of designated-safe ports 175 | // to produce a report assessing the risk of each security group that has been imported. 176 | func Generate(connectionString string, opts GenerateOpts) (*Report, error) { 177 | opts.fillInDefaults() 178 | db, err := sql.Open("postgres", connectionString) 179 | if err != nil { 180 | return nil, errors.Wrap(err, "Failed to connect to db") 181 | } 182 | defer db.Close() 183 | err = db.Ping() 184 | if err != nil { 185 | return nil, errors.Wrap(err, "Failed to ping db") 186 | } 187 | err = installDbFunctions(db) 188 | if err != nil { 189 | return nil, errors.Wrap(err, "Failed to install fixture functions") 190 | } 191 | rows, err := runSecurityGroupQuery(db) 192 | if err != nil { 193 | return nil, errors.Wrap(err, "Failed to run analysis query") 194 | } 195 | reportRows, err := analyzeSecurityGroupResults(rows, opts.SafePorts) 196 | if err != nil { 197 | return nil, errors.Wrap(err, "Failed to generate report from query results") 198 | } 199 | sort.SliceStable(reportRows, func(i, j int) bool { 200 | return sortRowsLess(&reportRows[i], &reportRows[j]) 201 | }) 202 | metadata, err := loadMetadata(db, reportRows, opts.SafePorts) 203 | if err != nil { 204 | return nil, errors.Wrap(err, "Failed to load metadata") 205 | } 206 | return &Report{ 207 | Rows: reportRows, 208 | Metadata: metadata, 209 | }, nil 210 | } 211 | 212 | var statusIndex map[string]int = map[string]int{ 213 | "red": 0, 214 | "yellow": 1, 215 | "green": 2, 216 | } 217 | 218 | func arnRegion(arn string) string { 219 | parts := strings.Split(arn, ":") 220 | return parts[3] 221 | } 222 | 223 | func loadMetadata(db *sql.DB, reportRows []Row, safePorts []int) (*Metadata, error) { 224 | query, err := loadQuery("most_recent_import") 225 | if err != nil { 226 | return nil, errors.Wrap(err, "failed to load query") 227 | } 228 | queryRows, err := db.Query(query) 229 | if err != nil { 230 | return nil, errors.Wrap(err, "Failed to query for most recent import") 231 | } 232 | defer queryRows.Close() 233 | if !queryRows.Next() { 234 | return nil, errors.New("Query for most recent import job found no results") 235 | } 236 | var endDate time.Time 237 | var organization string 238 | err = queryRows.Scan(&endDate, &organization) 239 | if err != nil { 240 | return nil, errors.Wrap(err, "Failed to read most recent import job row") 241 | } 242 | arn := reportRows[0].Arn 243 | parts := strings.Split(arn, ":") 244 | accountID := parts[4] 245 | if strings.HasPrefix(organization, "OrgDummy") { 246 | organization = "" 247 | } 248 | return &Metadata{ 249 | Imported: endDate, 250 | Generated: time.Now(), 251 | Account: accountID, 252 | Organization: organization, 253 | SafePorts: safePorts, 254 | }, nil 255 | } 256 | 257 | // Sort by status first, then region, then name 258 | func sortRowsLess(a, b *Row) bool { 259 | if a.Status == b.Status { 260 | aRegion := arnRegion(a.Arn) 261 | bRegion := arnRegion(b.Arn) 262 | if aRegion == bRegion { 263 | return a.Name < b.Name 264 | } 265 | return aRegion < bRegion 266 | } 267 | aIndex := statusIndex[a.Status] 268 | bIndex := statusIndex[b.Status] 269 | return aIndex < bIndex 270 | } 271 | 272 | func analyzeSecurityGroupResults(results []securityGroupRow, safePorts []int) ([]Row, error) { 273 | reportRows := []Row{} 274 | for _, row := range results { 275 | var status string 276 | unsafePorts, err := row.unsafePorts(safePorts) 277 | if err != nil { 278 | return nil, errors.Wrap(err, "Failed to calculate unsafe ports") 279 | } 280 | externalGroupCount := len(row.externalGroups) 281 | if row.isDefault { 282 | if row.inUse { 283 | if externalGroupCount == 0 && (row.isRestricted || row.internalOnly || len(row.ips) == 0) { 284 | status = "yellow" 285 | } else { 286 | status = "red" 287 | } 288 | } else { 289 | if row.isRestricted { 290 | // best case for default groups, locked down and not in use 291 | status = "green" 292 | } else { 293 | status = "yellow" 294 | } 295 | } 296 | } else { 297 | if row.inUse { 298 | if externalGroupCount == 0 { 299 | if row.isRestricted || (!row.isProblematic() && unsafePorts.Size() == 0) { 300 | status = "green" 301 | } else if len(row.ips) == 0 { 302 | status = "yellow" 303 | } else { 304 | status = "red" 305 | } 306 | } else { 307 | status = "red" 308 | } 309 | } else { 310 | // Not the default, so shouldn't exist if it's not in use 311 | status = "yellow" 312 | } 313 | } 314 | reportRows = append(reportRows, Row{ 315 | Arn: row.arn, 316 | Url: consoleUrl(row.arn), 317 | Name: row.groupName, 318 | GroupID: row.groupID, 319 | Status: status, 320 | PublicIps: row.ips, 321 | InUse: row.inUse, 322 | IsDefault: row.isDefault, 323 | Notes: row.notes(unsafePorts), 324 | UnsafePorts: unsafePorts, 325 | }) 326 | } 327 | return reportRows, nil 328 | } 329 | 330 | func installDbFunctions(db *sql.DB) error { 331 | isRFC1918, err := loadQuery("rfc1918") 332 | if err != nil { 333 | return errors.New("Failed to load sql for is_rfc1918block") 334 | } 335 | _, err = db.Exec(isRFC1918) 336 | if err != nil { 337 | return err 338 | } 339 | return nil 340 | } 341 | 342 | func runSecurityGroupQuery(db *sql.DB) ([]securityGroupRow, error) { 343 | analysisQuery, err := loadQuery("security_groups") 344 | if err != nil { 345 | return nil, errors.Wrap(err, "Failed to to load analysis query") 346 | } 347 | rows, err := db.Query(analysisQuery) 348 | if err != nil { 349 | return nil, errors.Wrap(err, "DB error analyzing") 350 | } 351 | defer rows.Close() 352 | results := make([]securityGroupRow, 0) 353 | for rows.Next() { 354 | row := securityGroupRow{} 355 | err = rows.Scan(&row.arn, &row.groupName, &row.groupID, pq.Array(&row.ips), &row.inUse, &row.isDefault, 356 | pq.Array(&row.portRanges), 357 | &row.isLargePublicBlock, &row.largeRangeCount, &row.isRestricted, &row.internalOnly, &row.pairedSecurityGroups, &row.externalGroups) 358 | if err != nil { 359 | return nil, errors.Wrap(err, "Failed to unmarshal a row") 360 | } 361 | results = append(results, row) 362 | } 363 | log.Infof("# Report Rows: %v", len(results)) 364 | return results, nil 365 | } 366 | 367 | func loadQuery(name string) (string, error) { 368 | filename := "/queries/" + name + ".sql" 369 | f, err := pkger.Open(filename) 370 | if err != nil { 371 | return "", errors.Wrapf(err, "Failed to open %v", filename) 372 | } 373 | defer f.Close() 374 | bytes, err := ioutil.ReadAll(f) 375 | if err != nil { 376 | return "", errors.Wrapf(err, "Failed to read %v", filename) 377 | } 378 | return string(bytes), nil 379 | } 380 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "flag" 7 | "fmt" 8 | "html/template" 9 | "io/ioutil" 10 | "os" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "github.com/markbates/pkger" 17 | "github.com/pkg/errors" 18 | 19 | "github.com/aws/aws-sdk-go-v2/config" 20 | 21 | log "github.com/sirupsen/logrus" 22 | 23 | ds "github.com/goldfiglabs/go-introspector/dockersession" 24 | "github.com/goldfiglabs/go-introspector/introspector" 25 | ps "github.com/goldfiglabs/go-introspector/postgres" 26 | "goldfiglabs.com/sgcheckup/internal/nmap" 27 | "goldfiglabs.com/sgcheckup/internal/report" 28 | ) 29 | 30 | type awsAuthError struct { 31 | Err error 32 | } 33 | 34 | func (e *awsAuthError) Error() string { 35 | return "Failed to find AWS Credentials" 36 | } 37 | 38 | func (e *awsAuthError) Unwrap() error { 39 | return e.Err 40 | } 41 | 42 | func loadAwsCredentials(ctx context.Context) ([]string, error) { 43 | cfg, err := config.LoadDefaultConfig(ctx) 44 | if err != nil { 45 | return nil, &awsAuthError{err} 46 | } 47 | creds, err := cfg.Credentials.Retrieve(ctx) 48 | if err != nil { 49 | return nil, &awsAuthError{err} 50 | } 51 | env := []string{ 52 | fmt.Sprintf("AWS_ACCESS_KEY_ID=%v", creds.AccessKeyID), 53 | fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%v", creds.SecretAccessKey), 54 | } 55 | if len(creds.SessionToken) > 0 { 56 | env = append(env, fmt.Sprintf("AWS_SESSION_TOKEN=%v", creds.SessionToken)) 57 | } 58 | return env, nil 59 | } 60 | 61 | func printReportRows(report *report.Report) { 62 | for _, r := range report.Rows { 63 | fmt.Printf("Name %v Status %v # Public Ips %v In Use %v Is Default %v %v\n", 64 | r.Name, r.Status, len(r.PublicIps), r.InUse, r.IsDefault, strings.Join(r.Notes, ",")) 65 | } 66 | } 67 | 68 | func writeCSVReport(rpReport *report.Report, outputFilename string) error { 69 | outputFile, err := os.Create(outputFilename) 70 | if err != nil { 71 | return errors.Wrapf(err, "Failed to create output file %v", outputFilename) 72 | } 73 | defer outputFile.Close() 74 | writer := csv.NewWriter(outputFile) 75 | defer writer.Flush() 76 | err = writer.Write([]string{"ARN", "Name", "Status", "Public Ips", "In Use", "Is Default", "Notes"}) 77 | if err != nil { 78 | return errors.Wrapf(err, "Failed to write headers to %v", outputFilename) 79 | } 80 | for _, row := range rpReport.Rows { 81 | err = writer.Write([]string{ 82 | row.Arn, 83 | row.Name, 84 | row.Status, 85 | strings.Join(row.PublicIps, ","), 86 | strconv.FormatBool(row.InUse), 87 | strconv.FormatBool(row.IsDefault), 88 | strings.Join(row.Notes, ", "), 89 | }) 90 | if err != nil { 91 | return errors.Wrapf(err, "Failed to write row to %v", outputFilename) 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func writeNMapCSVReport(scanResults nmap.IpScanResults, outputFilename string) error { 98 | outputFile, err := os.Create(outputFilename) 99 | if err != nil { 100 | return errors.Wrapf(err, "Failed to create output file %v", outputFilename) 101 | } 102 | defer outputFile.Close() 103 | writer := csv.NewWriter(outputFile) 104 | defer writer.Flush() 105 | err = writer.Write([]string{"IP", "Port", "Service", "Security Group(s)"}) 106 | if err != nil { 107 | return errors.Wrapf(err, "Failed to write headers to %v", outputFilename) 108 | } 109 | for ipAddr, ports := range scanResults { 110 | for port, portScan := range ports { 111 | err = writer.Write([]string{ 112 | ipAddr, 113 | strconv.Itoa(int(port)), 114 | portScan.Service.Display(), 115 | strings.Join(portScan.SecurityGroups, ", "), 116 | }) 117 | if err != nil { 118 | return errors.Wrapf(err, "Failed writing row to %v", outputFilename) 119 | } 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | type nmapTemplateData struct { 126 | Metadata *report.Metadata 127 | Ports []portRow 128 | } 129 | 130 | type Group struct { 131 | GroupId string 132 | Name string 133 | Last bool 134 | } 135 | 136 | type SGDisplay struct { 137 | Len int 138 | Groups []Group 139 | } 140 | 141 | type portRow struct { 142 | First bool 143 | IP string 144 | Port uint16 145 | Service string 146 | SGDisplay SGDisplay 147 | } 148 | 149 | func toSGDisplay(rows []report.Row, sgIds []string) SGDisplay { 150 | groups := []Group{} 151 | for i, sgId := range sgIds { 152 | for _, row := range rows { 153 | if row.GroupID == sgId { 154 | groups = append(groups, Group{ 155 | GroupId: sgId, 156 | Name: row.Name, 157 | Last: i == len(sgIds)-1, 158 | }) 159 | break 160 | } 161 | } 162 | } 163 | return SGDisplay{ 164 | Len: len(sgIds), 165 | Groups: groups, 166 | } 167 | } 168 | 169 | func toPortRows(report *report.Report, sr nmap.IpScanResults) []portRow { 170 | rows := []portRow{} 171 | for ipAddr, portMap := range sr { 172 | ipPorts := []portRow{} 173 | for port, scanResult := range portMap { 174 | ipPorts = append(ipPorts, portRow{ 175 | First: false, 176 | IP: ipAddr, 177 | Port: port, 178 | Service: scanResult.Service.Display(), 179 | SGDisplay: toSGDisplay(report.Rows, scanResult.SecurityGroups), 180 | }) 181 | } 182 | sort.Slice(ipPorts[:], func(i, j int) bool { 183 | return ipPorts[i].Port < ipPorts[j].Port 184 | }) 185 | ipPorts[0].First = true 186 | rows = append(rows, ipPorts...) 187 | } 188 | return rows 189 | } 190 | 191 | func writeNMapReport(report *report.Report, outputFilename string, scanResults nmap.IpScanResults) error { 192 | filename := "/templates/nmap_scan.html" 193 | f, err := pkger.Open(filename) 194 | if err != nil { 195 | return errors.Wrap(err, "Failed to load nmap html template") 196 | } 197 | defer f.Close() 198 | bytes, err := ioutil.ReadAll(f) 199 | if err != nil { 200 | return errors.Wrap(err, "Failed to read nmap html template") 201 | } 202 | t := template.New("sgCheckup-nmap") 203 | t.Funcs(template.FuncMap{ 204 | "humanize": func(t time.Time) string { 205 | return t.Local().Format("2006-01-02 15:04:05") 206 | }, 207 | "inc": func(i int) int { 208 | return i + 1 209 | }, 210 | }) 211 | t, err = t.Parse(string(bytes)) 212 | if err != nil { 213 | return errors.Wrap(err, "Failed to parse template") 214 | } 215 | outputFile, err := os.Create(outputFilename) 216 | if err != nil { 217 | return errors.Wrapf(err, "Failed to create output file %v", outputFilename) 218 | } 219 | defer outputFile.Close() 220 | err = t.Execute(outputFile, &nmapTemplateData{ 221 | Metadata: report.Metadata, 222 | Ports: toPortRows(report, scanResults), 223 | }) 224 | if err != nil { 225 | return errors.Wrap(err, "Failed to run nmap html template") 226 | } 227 | return nil 228 | } 229 | 230 | type templateData struct { 231 | Metadata report.Metadata 232 | Rows []templateRow 233 | NMapSkipped bool 234 | } 235 | 236 | type templateRow struct { 237 | report.Row 238 | Ips ipList 239 | } 240 | 241 | type ipList struct { 242 | Len int 243 | Subset []string 244 | Overflow bool 245 | } 246 | 247 | func makeTemplateRows(reportRows []report.Row) []templateRow { 248 | rows := []templateRow{} 249 | for _, reportRow := range reportRows { 250 | numIps := len(reportRow.PublicIps) 251 | cap := numIps 252 | if cap >= 8 { 253 | cap = 8 254 | } 255 | tr := templateRow{ 256 | reportRow, 257 | ipList{ 258 | Len: numIps, 259 | Subset: reportRow.PublicIps[:cap], 260 | Overflow: numIps > 8, 261 | }, 262 | } 263 | rows = append(rows, tr) 264 | } 265 | return rows 266 | } 267 | 268 | func writeHTMLReport(report *report.Report, outputFilename string, nmapSkipped bool) error { 269 | filename := "/templates/security_groups.gohtml" 270 | f, err := pkger.Open(filename) 271 | if err != nil { 272 | return errors.Wrap(err, "Failed to load html template") 273 | } 274 | defer f.Close() 275 | bytes, err := ioutil.ReadAll(f) 276 | if err != nil { 277 | return errors.Wrap(err, "Failed to read html template") 278 | } 279 | t := template.New("sgCheckup") 280 | t.Funcs(template.FuncMap{ 281 | "yn": func(b bool) string { 282 | if b { 283 | return "yes" 284 | } 285 | return "no" 286 | }, 287 | "inc": func(i int) int { 288 | return i + 1 289 | }, 290 | "notes": func(s []string) string { 291 | return strings.Join(s, ", ") 292 | }, 293 | "ipList": func(ips []string) string { 294 | if len(ips) == 0 { 295 | return "-" 296 | } 297 | if len(ips) > 8 { 298 | return strings.Join(ips[:8], ", ") + "...(+" + strconv.Itoa(len(ips)-8) + ")" 299 | } 300 | return strings.Join(ips, ", ") + " (" + strconv.Itoa(len(ips)) + ")" 301 | }, 302 | "humanize": func(t time.Time) string { 303 | return t.Local().Format("2006-01-02 15:04:05") 304 | }, 305 | "portList": func(intPorts []int) string { 306 | if len(intPorts) == 0 { 307 | return "-" 308 | } 309 | ports := []string{} 310 | for _, intPort := range intPorts { 311 | ports = append(ports, strconv.Itoa(intPort)) 312 | } 313 | return strings.Join(ports, ", ") 314 | }, 315 | }) 316 | t, err = t.Parse(string(bytes)) 317 | if err != nil { 318 | return errors.Wrap(err, "Failed to parse template") 319 | } 320 | outputFile, err := os.Create(outputFilename) 321 | if err != nil { 322 | return errors.Wrapf(err, "Failed to create output file %v", outputFilename) 323 | } 324 | defer outputFile.Close() 325 | err = t.Execute(outputFile, &templateData{ 326 | Metadata: *report.Metadata, 327 | Rows: makeTemplateRows(report.Rows), 328 | NMapSkipped: nmapSkipped, 329 | }) 330 | if err != nil { 331 | return errors.Wrap(err, "Failed to run html template") 332 | } 333 | return nil 334 | } 335 | 336 | type resourceSpecMap = map[string][]string 337 | 338 | var supportedResources resourceSpecMap = map[string][]string{ 339 | "ec2": {"SecurityGroups", "NetworkInterfaces"}, 340 | } 341 | 342 | func serviceSpec(r resourceSpecMap) string { 343 | services := []string{} 344 | for service, resources := range r { 345 | if resources == nil { 346 | services = append(services, service) 347 | } else { 348 | services = append(services, service+"="+strings.Join(resources, ",")) 349 | } 350 | } 351 | return strings.Join(services, ";") 352 | } 353 | 354 | func parseSafePorts(s string) ([]int, error) { 355 | ports := []int{} 356 | if s == "" { 357 | return ports, nil 358 | } 359 | stringPorts := strings.Split(s, ",") 360 | for _, stringPort := range stringPorts { 361 | port, err := strconv.Atoi(stringPort) 362 | if err != nil { 363 | return nil, err 364 | } 365 | ports = append(ports, port) 366 | } 367 | return ports, nil 368 | } 369 | 370 | func main() { 371 | pkger.Include("/templates") 372 | pkger.Include("/queries") 373 | // introspector options 374 | var skipIntrospector, logIntrospector, skipIntrospectorPull bool 375 | var introspectorRef string 376 | flag.BoolVar(&skipIntrospector, "skip-introspector", false, "Skip running an import, use existing data") 377 | flag.BoolVar(&logIntrospector, "log-introspector", false, "Pass through logs from introspector docker image") 378 | flag.BoolVar(&skipIntrospectorPull, "skip-introspector-pull", false, "Skip pulling the introspector docker image. Allows for using a local image") 379 | flag.StringVar(&introspectorRef, "introspector-ref", "", "Override the introspector docker image to use") 380 | // postgres options 381 | var leavePostgresUp, reusePostgres bool 382 | flag.BoolVar(&leavePostgresUp, "leave-postgres", false, "Leave postgres running in a docker container") 383 | flag.BoolVar(&reusePostgres, "reuse-postgres", false, "Reuse an existing postgres instance, if it is running") 384 | // report options 385 | var safePortsList string 386 | flag.StringVar(&safePortsList, "safe-ports", "22,80,443", "Specify a comma-separated list of ports considered safe. Default is 22,80,443") 387 | // nmap options 388 | var extraNMapArgs, nMapDockerRef string 389 | var nativeNMap, skipNMap bool 390 | flag.BoolVar(&skipNMap, "skip-nmap", false, "Skip generating nmap scripts") 391 | flag.BoolVar(&nativeNMap, "native-nmap", false, "Use natively-installed nmap in nmap scripts, rather than a docker image") 392 | flag.StringVar(&nMapDockerRef, "nmap-docker-ref", "", "Override the docker image used for nmap") 393 | flag.StringVar(&extraNMapArgs, "nmap-args", "", "Extra arguments to be provided to nmap") 394 | // output options 395 | var outputDir string 396 | var printToStdOut bool 397 | flag.BoolVar(&printToStdOut, "print-to-stdout", false, "Print report results to stdout") 398 | flag.StringVar(&outputDir, "output", "output", "Specify a directory for output") 399 | flag.Parse() 400 | 401 | ds, err := ds.NewSession() 402 | if err != nil { 403 | panic(errors.Wrap(err, "Failed to get docker client. Is it installed?")) 404 | } 405 | importer := &ps.DBCredential{ 406 | Username: "introspector", 407 | Password: "introspector", 408 | } 409 | superuser := &ps.DBCredential{ 410 | Username: "postgres", 411 | Password: "postgres", 412 | } 413 | postgresService, err := ps.NewDockerPostgresService(ds, ps.DockerPostgresOptions{ 414 | ReuseExisting: reusePostgres, 415 | SuperUserCredential: superuser, 416 | ContainerName: "sgCheckup-db", 417 | }) 418 | if err != nil { 419 | panic(err) 420 | } 421 | shutdownPostgres := func() { 422 | if !leavePostgresUp { 423 | err = postgresService.ShutDown() 424 | if err != nil { 425 | panic(err) 426 | } 427 | } 428 | } 429 | if !skipIntrospector { 430 | awsCreds, err := loadAwsCredentials(ds.Ctx) 431 | if err != nil { 432 | var authErr *awsAuthError 433 | if errors.As(err, &authErr) { 434 | shutdownPostgres() 435 | log.Fatal("Failed to find AWS Credentials. Please ensure that your enviroment is correctly configued as described here: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html") 436 | } else { 437 | panic(err) 438 | } 439 | } 440 | i, err := introspector.New(ds, postgresService, introspector.Options{ 441 | LogDockerOutput: logIntrospector, 442 | SkipDockerPull: skipIntrospectorPull, 443 | InspectorRef: introspectorRef, 444 | }) 445 | if err != nil { 446 | panic(err) 447 | } 448 | spec := serviceSpec(supportedResources) 449 | log.Infof("Running introspector with service spec %v", spec) 450 | log.Info("Introspector run may take a few minutes") 451 | err = i.ImportAWSService(awsCreds, spec) 452 | if err != nil { 453 | panic(err) 454 | } 455 | err = i.ShutDown() 456 | if err != nil { 457 | panic(err) 458 | } 459 | } 460 | 461 | safePorts, err := parseSafePorts(safePortsList) 462 | if err != nil { 463 | panic(err) 464 | } 465 | report, err := report.Generate(postgresService.ConnectionString(importer), report.GenerateOpts{ 466 | SafePorts: safePorts, 467 | }) 468 | if err != nil { 469 | panic(err) 470 | } 471 | if _, err := os.Stat(outputDir); os.IsNotExist(err) { 472 | err = os.Mkdir(outputDir, 0755) 473 | if err != nil { 474 | panic(err) 475 | } 476 | } 477 | if printToStdOut { 478 | printReportRows(report) 479 | } 480 | if !skipNMap { 481 | log.Info("Running nmap scan") 482 | _, err = nmap.RunScan(ds, outputDir, report) 483 | if err != nil { 484 | panic(err) 485 | } 486 | scanResults, err := nmap.ReadScanResults(outputDir + "/nmap/results") 487 | if err != nil { 488 | panic(err) 489 | } 490 | err = writeNMapReport(report, outputDir+"/nmap.html", scanResults) 491 | if err != nil { 492 | panic(err) 493 | } 494 | err = writeNMapCSVReport(scanResults, outputDir+"/nmap.csv") 495 | if err != nil { 496 | panic(err) 497 | } 498 | } 499 | err = writeHTMLReport(report, outputDir+"/index.html", skipNMap) 500 | if err != nil { 501 | panic(err) 502 | } 503 | err = writeCSVReport(report, outputDir+"/report.csv") 504 | if err != nil { 505 | panic(err) 506 | } 507 | log.Infof("Reports written to directory %v", outputDir) 508 | shutdownPostgres() 509 | } 510 | -------------------------------------------------------------------------------- /queries/most_recent_import.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | I.end_date, 3 | P.name AS organization 4 | FROM 5 | import_job AS I 6 | INNER JOIN provider_account AS P 7 | ON I.provider_account_id = P.id 8 | WHERE 9 | I.end_date IS NOT NULL 10 | ORDER BY 11 | I.end_date DESC 12 | LIMIT 1 -------------------------------------------------------------------------------- /queries/rfc1918.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION is_rfc1918block(block cidr) 2 | RETURNS boolean 3 | LANGUAGE plpgsql 4 | AS 5 | $$ 6 | BEGIN 7 | RETURN 8 | ('192.168.0.0/16' >>= block) 9 | OR ('172.16.0.0/12' >>= block) 10 | OR ('10.0.0.0/8' >>= block); 11 | END; 12 | $$ IMMUTABLE STRICT; 13 | 14 | CREATE OR REPLACE FUNCTION arn_account_id(arn TEXT) 15 | RETURNS TEXT AS $$ 16 | SELECT 17 | CASE 18 | WHEN arn = '*' THEN '*' 19 | ELSE split_part(arn, ':', 5) 20 | END 21 | $$ LANGUAGE sql IMMUTABLE STRICT; -------------------------------------------------------------------------------- /queries/security_groups.sql: -------------------------------------------------------------------------------- 1 | WITH ippermissions AS ( 2 | SELECT 3 | SG._id, 4 | bool_or(is_rfc1918block((IP.value->>'CidrIp')::cidr) = false AND masklen((IP.value->>'CidrIp')::cidr) BETWEEN 1 AND 23) AS is_large_public_block, 5 | bool_and((is_rfc1918block((IP.value->>'CidrIp')::cidr) OR masklen((IP.value->>'CidrIp')::cidr) != 0)) as internal_only, 6 | COUNT(*) AS range_count 7 | FROM 8 | aws_ec2_securitygroup AS SG 9 | CROSS JOIN LATERAL jsonb_array_elements(SG.ippermissions) AS P 10 | CROSS JOIN LATERAL jsonb_array_elements(P.value->'IpRanges') AS IP 11 | GROUP BY SG._id 12 | ), paired_groups AS ( 13 | SELECT 14 | SG._id, 15 | G.value ->> 'UserId' AS paired_account, 16 | jsonb_AGG(jsonb_build_object( 17 | 'Name', 18 | COALESCE(SG2.groupname, 'Security Group In Account '::text || (G.value ->> 'UserId')), 19 | 'URI', 20 | SG2.uri, 21 | 'ToPort', 22 | P.value -> 'ToPort', 23 | 'FromPort', 24 | P.value -> 'FromPort', 25 | 'IpProtocol', 26 | P.value -> 'IpProtocol', 27 | 'GroupId', 28 | G.value -> 'GroupId' 29 | )) AS paired_security_groups 30 | FROM 31 | aws_ec2_securitygroup AS SG 32 | CROSS JOIN LATERAL jsonb_array_elements(SG.ippermissions) AS P 33 | CROSS JOIN LATERAL unpack_maybe_array(P.value -> 'UserIdGroupPairs') AS G 34 | LEFT JOIN aws_ec2_securitygroup AS SG2 35 | ON SG2.groupid = G.value ->> 'GroupId' 36 | GROUP BY SG._id, G.value ->> 'UserId' 37 | ), external_groups AS ( 38 | SELECT 39 | PG._id, 40 | jsonb_object_agg(PG.paired_account, PG.paired_security_groups) AS external_groups 41 | FROM 42 | paired_groups AS PG 43 | INNER JOIN aws_ec2_securitygroup AS SG 44 | ON SG._id = PG._id 45 | WHERE 46 | PG.paired_account != arn_account_id(SG.uri) 47 | GROUP BY PG._id 48 | ), permissions AS ( 49 | SELECT 50 | SG._id, 51 | permissions.* AS permissions 52 | FROM 53 | aws_ec2_securitygroup AS SG 54 | CROSS JOIN LATERAL jsonb_array_elements(SG.ippermissions) AS permissions 55 | ), raw_ranges AS ( 56 | SELECT 57 | P._id, 58 | P.value AS permission, 59 | (R.value ->> 'CidrIp')::cidr AS cidr, 60 | COALESCE(((P.value ->> 'FromPort')::int), 0) AS from_port, 61 | COALESCE(((P.value ->> 'ToPort')::int), 65535) AS to_port 62 | FROM 63 | permissions as P 64 | CROSS JOIN LATERAL jsonb_array_elements(P.value->'IpRanges') AS R 65 | ), public_tcp_ranges AS ( 66 | SELECT 67 | R._id, 68 | ARRAY_AGG(int4range(R.from_port, R.to_port, '[]')) AS port_ranges 69 | FROM 70 | raw_ranges AS R 71 | WHERE 72 | R.from_port <= R.to_port 73 | AND R.cidr = '0.0.0.0/0'::cidr 74 | AND R.permission ->> 'IpProtocol' IN ('-1', 'tcp') 75 | GROUP BY R._id 76 | ), security_group_attrs AS ( 77 | SELECT 78 | SG._id, 79 | SG._id IN ( 80 | SELECT DISTINCT(securitygroup_id) 81 | FROM aws_ec2_networkinterface_securitygroup 82 | UNION 83 | SELECT DISTINCT(securitygroup_id) 84 | FROM aws_ec2_securitygroup_vpcpeeringconnection 85 | ) AS in_use, 86 | SG.groupname = 'default' AS is_default, 87 | R.port_ranges, 88 | COALESCE(IP.is_large_public_block, false) AS is_large_public_block, 89 | COALESCE(IP.range_count, 0) > 50 AS large_range_count, 90 | COALESCE(IP.range_count, 0) = 0 AS is_restricted, 91 | COALESCE(IP.internal_only, true) AS internal_only, 92 | Internal.paired_security_groups, 93 | COALESCE(E.external_groups, '{}'::jsonb) AS external_groups 94 | FROM 95 | aws_ec2_securitygroup AS SG 96 | LEFT JOIN ippermissions AS IP 97 | ON SG._id = IP._id 98 | LEFT JOIN public_tcp_ranges AS R 99 | ON SG._id = R._id 100 | LEFT JOIN paired_groups AS Internal 101 | ON SG._id = Internal._id 102 | AND arn_account_id(SG.uri) = Internal.paired_account 103 | LEFT JOIN external_groups AS E 104 | ON SG._id = E._id 105 | ), publicips AS ( 106 | SELECT 107 | SG._id, 108 | ARRAY_AGG((NI.association ->> 'PublicIp')::inet) AS ips 109 | FROM 110 | aws_ec2_securitygroup AS SG 111 | INNER JOIN aws_ec2_networkinterface_securitygroup AS NI2SG 112 | ON SG._id = NI2SG.securitygroup_id 113 | INNER JOIN aws_ec2_networkinterface AS NI 114 | ON NI2SG.networkinterface_id = NI._id 115 | WHERE 116 | NI.association IS NOT NULL 117 | GROUP BY 118 | SG._id 119 | ) 120 | SELECT 121 | SG.uri AS arn, 122 | SG.groupname, 123 | SG.groupid, 124 | COALESCE(P.ips, '{}') AS ips, 125 | Attrs.in_use, 126 | Attrs.is_default, 127 | Attrs.port_ranges, 128 | Attrs.is_large_public_block, 129 | Attrs.large_range_count, 130 | Attrs.is_restricted, 131 | Attrs.internal_only, 132 | Attrs.paired_security_groups, 133 | Attrs.external_groups 134 | FROM 135 | aws_ec2_securitygroup AS SG 136 | LEFT JOIN security_group_attrs AS Attrs 137 | ON SG._id = Attrs._id 138 | LEFT JOIN publicips AS P 139 | ON P._id = SG._id -------------------------------------------------------------------------------- /templates/nmap_scan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sgCheckup - By Gold Fig Labs 5 | 58 | 59 | 60 |
61 |

sgCheckup - IP Scan Results

62 |
63 |
64 |

65 | Account snapshot: 66 | 67 |

68 |

69 | Report generated: 70 | 71 |

72 |
73 |
74 |

75 | Organization: 76 | 77 |

78 |

Account ID:

79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {{range $i, $row := .Ports}} 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | {{end}} 106 | 107 |
#IP AddressPortServiceSecurity Group(s)
{{ inc $i }}{{ if $row.First }}{{ $row.IP }}{{ end }}{{ $row.Port }}{{ $row.Service }} 99 | {{ range $j, $group := $row.SGDisplay.Groups }} 100 | {{ $group.Name }} 101 | {{ if not $group.Last }}, {{ end }} 102 | {{ end }} 103 |
108 |
109 | 110 | -------------------------------------------------------------------------------- /templates/run_scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NUM_PROCS=$(grep -c ^processor /proc/cpuinfo) 4 | echo "Using ${NUM_PROCS} processes" 5 | 6 | ls /opt/sgCheckup/groups | ( 7 | while read filepath; do 8 | echo $filepath 9 | /opt/sgCheckup/groups/$filepath & 10 | if [[ $(jobs -p | wc -l) -ge $NUM_PROCS ]]; then wait -n; fi 11 | done; 12 | wait 13 | ) 14 | -------------------------------------------------------------------------------- /templates/scan_group.gosh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # -sT look for connect success 6 | # -sV, --version-all look for versions of services 7 | # -T2 relatively slow 8 | {{ .Bin }} -oX {{ .OutputFile }} -Pn -sT -sV --version-light -T{{ .Speed }} {{ .Ports }} {{ .Targets }} 9 | -------------------------------------------------------------------------------- /templates/security_groups.gohtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sgCheckup - By Gold Fig Labs 5 | 77 | 78 | 79 |
80 |

sgCheckup - By Gold Fig Labs

81 |
82 |
83 |

84 | Security Groups snapshotted at: 85 | 86 |

87 |

88 | Report generated at: 89 | 90 |

91 |
92 |
93 |

Account ID:

94 |

Safe Ports Assumed:

95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {{ $nmapSkipped := .NMapSkipped }} 111 | {{range $index, $row := .Rows}} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 134 | 135 | 136 | {{end}} 137 | 138 |
#NameStatusIn UseIs DefaultPublic IPsNotes
{{inc $index}}{{$row.Name}}{{$row.Status}}{{yn $row.InUse}}{{yn $row.IsDefault}} 119 | {{ if eq $row.Ips.Len 0 }} 120 | - 121 | {{ else }} 122 | {{ range $i, $ip := $row.Ips.Subset }} 123 | {{ if $nmapSkipped }} 124 | {{ $ip }} 125 | {{ else }} 126 | {{ $ip }} 127 | {{ end }} 128 | {{ if ne (inc $i) $row.Ips.Len }}, 129 | {{ end }} 130 | {{ end }} 131 | {{ if $row.Ips.Overflow }}...{{ end }} 132 | {{ end }} 133 | {{notes $row.Notes}}
139 |
140 |

Legend

141 |
142 |
143 |

Colors

144 |

145 | Red - This security group has ports open and contains instances with public IP 146 | addresses. Those instances can accept traffic on those ports, and it should be 147 | verified that this is intended. 148 |

149 |

150 | Yellow - This security 151 | group is not ideal, but does not present an immediate risk. It is 152 | worth examining to see if it can be further locked down or removed, 153 | if unused. 154 |

155 |

156 | Green - No recommendations 157 | for this security group. 158 |

159 |
160 |
161 |

Assumptions

162 |

163 | Default security groups should not be used, and should be locked down. 164 | This prevents instances from accidentally inheriting open ports when 165 | created. 166 |

167 |

168 | Unused security groups should be removed, as they represent surface area 169 | that doesn't need to exist. 170 |

171 |

172 | While we do list IP addresses alongside ports that are open, we do not 173 | verify whether any of the instances are actually listening on those ports. 174 | We recommend not relying only on instances not actively listening 175 | on a port. The principle of defense-in-depth suggests that if an instance 176 | does not need a port open, the firewall should also enforce that. 177 |

178 |
179 |
180 |
181 |
182 | 183 | --------------------------------------------------------------------------------