├── .gitattributes ├── .gitignore ├── AUTHORS ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── deprecated-clients └── ssllabs-scan-v3.go ├── pulse.sql ├── ssllabs-api-docs-v2-deprecated.md ├── ssllabs-api-docs-v3.md ├── ssllabs-api-docs-v4.md ├── ssllabs-api-docs.md ├── ssllabs-scan-v4-register.go └── ssllabs-scan-v4.go /.gitattributes: -------------------------------------------------------------------------------- 1 | ssllabs-scan.go ident -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ssllabs-scan 3 | ssllabs-scan-v3 4 | local-test-*.txt 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ivan Ristic 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build ssllabs-scan-v3.go 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ssllabs-scan 2 | ============ 3 | 4 | This tool is a command-line client for the SSL Labs APIs, designed for 5 | automated and/or bulk testing. 6 | 7 | If you'd like to contribute, please have a look at the TODO file. For larger work, 8 | please get in touch first. For smaller work (there are some TODO comments in the 9 | source code), feel free to submit pull requests. 10 | 11 | To report a problem related to this tool, please create a new issue on GitHub: https://github.com/ssllabs/ssllabs-scan/issues 12 | Please don't send bug reports to the community. 13 | 14 | To discuss the API and the development of the reference client implementation and other questions not related to this command line tool, please 15 | join the SSL Labs community: https://community.qualys.com/community/ssllabs 16 | 17 | Before you use this tool please review the terms and conditions, which can be found here: 18 | https://www.ssllabs.com/about/terms.html 19 | 20 | Deprecated clients are now available at [deprecated-clients](deprecated-clients) 21 | 22 | ## Requirements 23 | 24 | * Go >= 1.3 25 | 26 | ## Usage 27 | 28 | SYNOPSIS 29 | 30 | If you're using API v4 for the first time then please use the [ssllabs-scan-v4-register](ssllabs-scan-v4-register.go) 31 | 32 | ``` 33 | ssllabs-scan-v4-register --firstName John --lastName Doe --organization Example --email johndoe@example.com 34 | ssllabs-scan-v4 [options] --email johndoe@example.com hostname 35 | ssllabs-scan-v4 [options] --email johndoe@example.com --hostfile file 36 | ``` 37 | 38 | OPTIONS 39 | [ssllabs-scan-v4.go](ssllabs-scan-v4.go) 40 | 41 | | Option | Default value | Description | 42 | |-------------------|---------------|---------------------------------------------------------------------| 43 | | --api | BUILTIN | API entry point, for example https://www.example.com/api/ | 44 | | --verbosity | info | Configure log verbosity: error, info, debug, or trace | 45 | | --quiet | false | Disable status messages (logging) | 46 | | --ignore-mismatch | false | Proceed with assessments on certificate mismatch | 47 | | --json-flat | false | Output results in flattened JSON format | 48 | | --hostfile | none | File containing hosts to scan (one per line) | 49 | | --usecache | false | If true, accept cached results (if available), else force live scan | 50 | | --grade | false | Output only the hostname: grade | 51 | | --hostcheck | false | If true, host resolution failure will result in a fatal error | 52 | | --email | "" | Registered organization email for API v4 **(required)** | 53 | 54 | [ssllabs-scan-v4-register.go](ssllabs-scan-v4-register.go) 55 | 56 | | Option | Default value | Description | 57 | |------------------|---------------|----------------------------------------------------------------------------| 58 | | --firstName | "" | First name of the user | 59 | | --lastName | "" | Last name of the user | 60 | | --organization | "" | Organization of the user | 61 | | --email | "" | Organization email of the user | 62 | | --registerApiUrl | BUILTIN | Register API entry point, for example https://www.example.com/api/register | 63 | 64 | ## Third-Party Tools and Libraries 65 | 66 | A list of libraries and tools that rely on the SSL Labs APIs can be found on the SSL Labs web site: https://www.ssllabs.com/projects/ssllabs-apis/ 67 | 68 | ## Docker 69 | 70 | Docker images for this project are available at: 71 | 72 | * [https://github.com/jumanjihouse/docker-ssllabs-scan](https://github.com/jumanjihouse/docker-ssllabs-scan) 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## ssllabs-scan v3.0 4 | 5 | At present, ssllabs-scan is a barebones reference client application for SSL Labs APIs. While it correctly implements the basic logic of invoking the APIs and executing many assessments in parallel, it lacks many features that are needed for useful continuous testing of multiple TLS servers. In the next major version, we wish to elevate ssllabs-scan into a more useful full-featured tool. 6 | 7 | ## Better output for single-server testing 8 | 9 | Currently, ssllabs-scan doesn't present test results in a nice way (reporting). It would be nice to do this for both ad-hoc scanning and for saved results (see next section). 10 | 11 | ## Persistent multi-server testing 12 | 13 | A scan is defined via a unique ID and a list of hostnames that should be scanned. The unique ID can be any string, but it is recommended to use IDs that carry some meaning. For example, if you wish to perform monthly scanning, an ID consisting of year and month would be a good choice: "2016-01". 14 | 15 | The following is a list of scan operations that should be supported: 16 | 17 | * Start new scan (ID, filename) 18 | * Error if there are any results with the supplied ID in the database 19 | * There should be an option to restart a scan, in which case existing data is deleted 20 | * Continue scan (ID, filename); continues an existing scan. Skips over servers whose results we already have. 21 | * List scans IDs, possibly with some basic information (e.g., times of first and last test, # of tests) 22 | * Delete scan results (ID); deletes all results associated with one scan. 23 | * Delete everything; deletes all data from the database. 24 | * Option to save results of an ad-hoc test to the database. 25 | 26 | * Options to query stored tests by scan ID, date, server hostname, grade, etc. 27 | * Export raw test results as JSON. 28 | * Export test results as custom-formatted CSV. 29 | 30 | ## Persistence options 31 | 32 | For ssllabs-scan, possibly the most useful database to use would be SQLite, which can be embedded into ssllabs-scan itself, thus requiring least effort to get up and running. The option of being database agnostic is tempting, but the file-oriented nature of SQLite has certain advantages that make it useful to rely on it exclusively. 33 | 34 | Raw test results (JSON) should always be stored in full. However, it might also be useful to extract key information into one or more additional tables in order to allow for easier reporting and data mining. For example, there could be tables to represent hosts, normalised test results, and certificates. New versions of ssllabs-scan could change the table structure. Because we'll keep raw results, we should be able to simply recreate the necessary table structure and populate it from the raw data. The previous version could always be kept in case of a bothched upgrade. This is a good example of how SQLite is good to rely on. 35 | 36 | # Other Requirements 37 | 38 | * To minimise confusion related to mismatched version numbers, the major version of ssllabs-scan should always follow the major version of SSL Labs APIs. 39 | -------------------------------------------------------------------------------- /deprecated-clients/ssllabs-scan-v3.go: -------------------------------------------------------------------------------- 1 | //go:build go1.3 2 | // +build go1.3 3 | 4 | /* 5 | * Licensed to Qualys, Inc. (QUALYS) under one or more 6 | * contributor license agreements. See the NOTICE file distributed with 7 | * this work for additional information regarding copyright ownership. 8 | * QUALYS licenses this file to You under the Apache License, Version 2.0 9 | * (the "License"); you may not use this file except in compliance with 10 | * the License. You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | // work in progress 22 | package main 23 | 24 | import "crypto/tls" 25 | import "errors" 26 | import "encoding/json" 27 | import "flag" 28 | import "fmt" 29 | import "io/ioutil" 30 | import "bufio" 31 | import "os" 32 | import "log" 33 | import "math/rand" 34 | import "net" 35 | import "net/http" 36 | import "net/url" 37 | import "strconv" 38 | import "strings" 39 | import "sync/atomic" 40 | import "time" 41 | import "sort" 42 | 43 | const ( 44 | LOG_NONE = -1 45 | LOG_EMERG = 0 46 | LOG_ALERT = 1 47 | LOG_CRITICAL = 2 48 | LOG_ERROR = 3 49 | LOG_WARNING = 4 50 | LOG_NOTICE = 5 51 | LOG_INFO = 6 52 | LOG_DEBUG = 7 53 | LOG_TRACE = 8 54 | ) 55 | 56 | var USER_AGENT = "ssllabs-scan v1.5.0 (dev $Id$)" 57 | 58 | var logLevel = LOG_NOTICE 59 | 60 | // How many assessment do we have in progress? 61 | var activeAssessments = 0 62 | 63 | // How many assessments does the server think we have in progress? 64 | var currentAssessments = -1 65 | 66 | // The maximum number of assessments we can have in progress at any one time. 67 | var maxAssessments = -1 68 | 69 | var requestCounter uint64 = 0 70 | 71 | var apiLocation = "https://api.ssllabs.com/api/v3" 72 | 73 | var globalNewAssessmentCoolOff int64 = 1100 74 | 75 | var globalIgnoreMismatch = false 76 | 77 | var globalStartNew = true 78 | 79 | var globalFromCache = false 80 | 81 | var globalMaxAge = 0 82 | 83 | var globalInsecure = false 84 | 85 | var httpClient *http.Client 86 | 87 | type LabsError struct { 88 | Field string 89 | Message string 90 | } 91 | 92 | type LabsErrorResponse struct { 93 | ResponseErrors []LabsError `json:"errors"` 94 | } 95 | 96 | func (e LabsErrorResponse) Error() string { 97 | msg, err := json.Marshal(e) 98 | if err != nil { 99 | return err.Error() 100 | } else { 101 | return string(msg) 102 | } 103 | } 104 | 105 | type LabsKey struct { 106 | Size int 107 | Strength int 108 | Alg string 109 | DebianFlaw bool 110 | Q int 111 | } 112 | 113 | type LabsCaaRecord struct { 114 | Tag string 115 | Value string 116 | Flags int 117 | } 118 | 119 | type LabsCaaPolicy struct { 120 | PolicyHostname string 121 | CaaRecords []LabsCaaRecord 122 | } 123 | 124 | type LabsCert struct { 125 | Id string 126 | Subject string 127 | CommonNames []string 128 | AltNames []string 129 | NotBefore int64 130 | NotAfter int64 131 | IssuerSubject string 132 | SigAlg string 133 | RevocationInfo int 134 | CrlURIs []string 135 | OcspURIs []string 136 | RevocationStatus int 137 | CrlRevocationStatus int 138 | OcspRevocationStatus int 139 | DnsCaa bool 140 | Caapolicy LabsCaaPolicy 141 | MustStaple bool 142 | Sgc int 143 | ValidationType string 144 | Issues int 145 | Sct bool 146 | Sha1Hash string 147 | PinSha256 string 148 | KeyAlg string 149 | KeySize int 150 | KeyStrength int 151 | KeyKnownDebianInsecure bool 152 | Raw string 153 | } 154 | 155 | type LabsChainCert struct { 156 | Subject string 157 | Label string 158 | NotBefore int64 159 | NotAfter int64 160 | IssuerSubject string 161 | IssuerLabel string 162 | SigAlg string 163 | Issues int 164 | KeyAlg string 165 | KeySize int 166 | KeyStrength int 167 | RevocationStatus int 168 | CrlRevocationStatus int 169 | OcspRevocationStatus int 170 | Raw string 171 | } 172 | 173 | type LabsChain struct { 174 | Certs []LabsChainCert 175 | Issues int 176 | } 177 | 178 | type LabsProtocol struct { 179 | Id int 180 | Name string 181 | Version string 182 | V2SuitesDisabled bool 183 | Q int 184 | } 185 | 186 | type LabsSimClient struct { 187 | Id int 188 | Name string 189 | Platform string 190 | Version string 191 | IsReference bool 192 | } 193 | 194 | type LabsSimulation struct { 195 | Client LabsSimClient 196 | ErrorCode int 197 | ErrorMessage string 198 | Attempts int 199 | CertChainId string 200 | ProtocolId int 201 | SuiteId int 202 | SuiteName string 203 | KxType string 204 | KxStrength int 205 | DhBits int 206 | DhP int 207 | DhG int 208 | DhYs int 209 | NamedGroupBits int 210 | NamedGroupId int 211 | NamedGroupName string 212 | AlertType int 213 | AlertCode int 214 | KeyAlg string 215 | KeySize int 216 | SigAlg string 217 | } 218 | 219 | type LabsSimDetails struct { 220 | Results []LabsSimulation 221 | } 222 | 223 | type LabsSuite struct { 224 | Id int 225 | Name string 226 | CipherStrength int 227 | KxType string 228 | KxStrength int 229 | DhBits int 230 | DhP int 231 | DhG int 232 | DhYs int 233 | NamedGroupBits int 234 | NamedGroupId int 235 | NamedGroudName string 236 | Q int 237 | } 238 | 239 | type LabsSuites struct { 240 | Protocol int 241 | List []LabsSuite 242 | Preference bool 243 | } 244 | 245 | type LabsHstsPolicy struct { 246 | LONG_MAX_AGE int64 247 | Header string 248 | Status string 249 | Error string 250 | MaxAge int64 251 | IncludeSubDomains bool 252 | Preload bool 253 | Directives map[string]string 254 | } 255 | 256 | type LabsHstsPreload struct { 257 | Source string 258 | HostName string 259 | Status string 260 | Error string 261 | SourceTime int64 262 | } 263 | 264 | type LabsHpkpPin struct { 265 | HashFunction string 266 | Value string 267 | } 268 | 269 | type LabsHpkpDirective struct { 270 | Name string 271 | Value string 272 | } 273 | 274 | type LabsHpkpPolicy struct { 275 | Header string 276 | Status string 277 | Error string 278 | MaxAge int64 279 | IncludeSubDomains bool 280 | ReportUri string 281 | Pins []LabsHpkpPin 282 | MatchedPins []LabsHpkpPin 283 | Directives []LabsHpkpDirective 284 | } 285 | 286 | type LabsDrownHost struct { 287 | Ip string 288 | Export bool 289 | Port int 290 | Special bool 291 | Sslv2 bool 292 | Status string 293 | } 294 | 295 | type LabsCertChain struct { 296 | Id string 297 | CertIds []string 298 | Trustpath []LabsTrustPath 299 | Issues int 300 | NoSni bool 301 | } 302 | 303 | type LabsTrustPath struct { 304 | CertIds []string 305 | Trust []LabsTrust 306 | IsPinned bool 307 | MatchedPins int 308 | UnMatchedPins int 309 | } 310 | 311 | type LabsTrust struct { 312 | RootStore string 313 | IsTrusted bool 314 | TrustErrorMessage string 315 | } 316 | 317 | type LabsNamedGroups struct { 318 | List []LabsNamedGroup 319 | Preference bool 320 | } 321 | 322 | type LabsNamedGroup struct { 323 | Id int 324 | Name string 325 | Bits int 326 | } 327 | 328 | type LabsHttpTransaction struct { 329 | RequestUrl string 330 | StatusCode int 331 | RequestLine string 332 | RequestHeaders []string 333 | ResponseLine string 334 | ResponseRawHeader []string 335 | ResponseHeader []LabsHttpHeader 336 | FragileServer bool 337 | } 338 | 339 | type LabsHttpHeader struct { 340 | Name string 341 | Value string 342 | } 343 | 344 | type LabsEndpointDetails struct { 345 | HostStartTime int64 346 | CertChains []LabsCertChain 347 | Protocols []LabsProtocol 348 | Suites []LabsSuites 349 | NoSniSuites LabsSuites 350 | NamedGroups LabsNamedGroups 351 | ServerSignature string 352 | PrefixDelegation bool 353 | NonPrefixDelegation bool 354 | VulnBeast bool 355 | RenegSupport int 356 | SessionResumption int 357 | CompressionMethods int 358 | SupportsNpn bool 359 | NpnProtocols string 360 | SupportsAlpn bool 361 | AlpnProtocols string 362 | SessionTickets int 363 | OcspStapling bool 364 | StaplingRevocationStatus int 365 | StaplingRevocationErrorMessage string 366 | SniRequired bool 367 | HttpStatusCode int 368 | HttpForwarding string 369 | SupportsRc4 bool 370 | Rc4WithModern bool 371 | Rc4Only bool 372 | ForwardSecrecy int 373 | ProtocolIntolerance int 374 | MiscIntolerance int 375 | Sims LabsSimDetails 376 | Heartbleed bool 377 | Heartbeat bool 378 | OpenSslCcs int 379 | OpenSSLLuckyMinus20 int 380 | Ticketbleed int 381 | Bleichenbacher int 382 | Poodle bool 383 | PoodleTLS int 384 | FallbackScsv bool 385 | Freak bool 386 | HasSct int 387 | DhPrimes []string 388 | DhUsesKnownPrimes int 389 | DhYsReuse bool 390 | EcdhParameterReuse bool 391 | Logjam bool 392 | ChaCha20Preference bool 393 | HstsPolicy LabsHstsPolicy 394 | HstsPreloads []LabsHstsPreload 395 | HpkpPolicy LabsHpkpPolicy 396 | HpkpRoPolicy LabsHpkpPolicy 397 | HttpTransactions []LabsHttpTransaction 398 | DrownHosts []LabsDrownHost 399 | DrownErrors bool 400 | DrownVulnerable bool 401 | } 402 | 403 | type LabsEndpoint struct { 404 | IpAddress string 405 | ServerName string 406 | StatusMessage string 407 | StatusDetailsMessage string 408 | Grade string 409 | GradeTrustIgnored string 410 | FutureGrade string 411 | HasWarnings bool 412 | IsExceptional bool 413 | Progress int 414 | Duration int 415 | Eta int 416 | Delegation int 417 | Details LabsEndpointDetails 418 | } 419 | 420 | type LabsReport struct { 421 | Host string 422 | Port int 423 | Protocol string 424 | IsPublic bool 425 | Status string 426 | StatusMessage string 427 | StartTime int64 428 | TestTime int64 429 | EngineVersion string 430 | CriteriaVersion string 431 | CacheExpiryTime int64 432 | CertHostnames []string 433 | Endpoints []LabsEndpoint 434 | Certs []LabsCert 435 | rawJSON string 436 | } 437 | 438 | type LabsResults struct { 439 | reports []LabsReport 440 | responses []string 441 | } 442 | 443 | type LabsInfo struct { 444 | EngineVersion string 445 | CriteriaVersion string 446 | MaxAssessments int 447 | CurrentAssessments int 448 | NewAssessmentCoolOff int64 449 | Messages []string 450 | } 451 | 452 | func invokeGetRepeatedly(url string) (*http.Response, []byte, error) { 453 | retryCount := 0 454 | 455 | for { 456 | var reqId = atomic.AddUint64(&requestCounter, 1) 457 | 458 | if logLevel >= LOG_DEBUG { 459 | log.Printf("[DEBUG] Request #%v: %v", reqId, url) 460 | } 461 | 462 | req, err := http.NewRequest("GET", url, nil) 463 | if err != nil { 464 | return nil, nil, err 465 | } 466 | 467 | req.Header.Add("User-Agent", USER_AGENT) 468 | 469 | resp, err := httpClient.Do(req) 470 | if err == nil { 471 | if logLevel >= LOG_DEBUG { 472 | log.Printf("[DEBUG] Response #%v status: %v %v", reqId, resp.Proto, resp.Status) 473 | } 474 | 475 | if logLevel >= LOG_TRACE { 476 | for key, values := range resp.Header { 477 | for _, value := range values { 478 | log.Printf("[TRACE] %v: %v\n", key, value) 479 | } 480 | } 481 | } 482 | 483 | if logLevel >= LOG_NOTICE { 484 | for key, values := range resp.Header { 485 | if strings.ToLower(key) == "x-message" { 486 | for _, value := range values { 487 | log.Printf("[NOTICE] Server message: %v\n", value) 488 | } 489 | } 490 | } 491 | } 492 | 493 | // Update current assessments. 494 | 495 | headerValue := resp.Header.Get("X-Current-Assessments") 496 | if headerValue != "" { 497 | i, err := strconv.Atoi(headerValue) 498 | if err == nil { 499 | if currentAssessments != i { 500 | currentAssessments = i 501 | 502 | if logLevel >= LOG_DEBUG { 503 | log.Printf("[DEBUG] Server set current assessments to %v", headerValue) 504 | } 505 | } 506 | } else { 507 | if logLevel >= LOG_WARNING { 508 | log.Printf("[WARNING] Ignoring invalid X-Current-Assessments value (%v): %v", headerValue, err) 509 | } 510 | } 511 | } 512 | 513 | // Update maximum assessments. 514 | 515 | headerValue = resp.Header.Get("X-Max-Assessments") 516 | if headerValue != "" { 517 | i, err := strconv.Atoi(headerValue) 518 | if err == nil { 519 | if maxAssessments != i { 520 | maxAssessments = i 521 | 522 | if maxAssessments <= 0 { 523 | log.Fatalf("[ERROR] Server doesn't allow further API requests") 524 | } 525 | 526 | if logLevel >= LOG_DEBUG { 527 | log.Printf("[DEBUG] Server set maximum assessments to %v", headerValue) 528 | } 529 | } 530 | } else { 531 | if logLevel >= LOG_WARNING { 532 | log.Printf("[WARNING] Ignoring invalid X-Max-Assessments value (%v): %v", headerValue, err) 533 | } 534 | } 535 | } 536 | 537 | // Retrieve the response body. 538 | 539 | defer resp.Body.Close() 540 | 541 | body, err := ioutil.ReadAll(resp.Body) 542 | if err != nil { 543 | return nil, nil, err 544 | } 545 | 546 | if logLevel >= LOG_TRACE { 547 | log.Printf("[TRACE] Response #%v body:\n%v", reqId, string(body)) 548 | } 549 | 550 | return resp, body, nil 551 | } else { 552 | if strings.Contains(err.Error(), "EOF") { 553 | // Server closed a persistent connection on us, which 554 | // Go doesn't seem to be handling well. So we'll try one 555 | // more time. 556 | if retryCount > 5 { 557 | log.Fatalf("[ERROR] Too many HTTP requests (5) failed with EOF (ref#2)") 558 | } 559 | 560 | if logLevel >= LOG_DEBUG { 561 | log.Printf("[DEBUG] HTTP request failed with EOF (ref#2)") 562 | } 563 | } else { 564 | log.Fatalf("[ERROR] HTTP request failed: %v (ref#2)", err.Error()) 565 | } 566 | 567 | retryCount++ 568 | } 569 | } 570 | } 571 | 572 | func invokeApi(command string) (*http.Response, []byte, error) { 573 | var url = apiLocation + "/" + command 574 | 575 | for { 576 | resp, body, err := invokeGetRepeatedly(url) 577 | if err != nil { 578 | return nil, nil, err 579 | } 580 | 581 | // Status codes 429, 503, and 529 essentially mean try later. Thus, 582 | // if we encounter them, we sleep for a while and try again. 583 | if resp.StatusCode == 429 { 584 | return resp, body, errors.New("Assessment failed: 429") 585 | } else if (resp.StatusCode == 503) || (resp.StatusCode == 529) { 586 | // In case of the overloaded server, randomize the sleep time so 587 | // that some clients reconnect earlier and some later. 588 | 589 | sleepTime := 15 + rand.Int31n(15) 590 | 591 | if logLevel >= LOG_NOTICE { 592 | log.Printf("[NOTICE] Sleeping for %v minutes after a %v response", sleepTime, resp.StatusCode) 593 | } 594 | 595 | time.Sleep(time.Duration(sleepTime) * time.Minute) 596 | } else if (resp.StatusCode != 200) && (resp.StatusCode != 400) { 597 | log.Fatalf("[ERROR] Unexpected response status code %v", resp.StatusCode) 598 | } else { 599 | return resp, body, nil 600 | } 601 | } 602 | } 603 | 604 | func invokeInfo() (*LabsInfo, error) { 605 | var command = "info" 606 | 607 | _, body, err := invokeApi(command) 608 | if err != nil { 609 | return nil, err 610 | } 611 | 612 | var labsInfo LabsInfo 613 | err = json.Unmarshal(body, &labsInfo) 614 | if err != nil { 615 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 616 | return nil, err 617 | } 618 | 619 | return &labsInfo, nil 620 | } 621 | 622 | func invokeAnalyze(host string, startNew bool, fromCache bool) (*LabsReport, error) { 623 | var command = "analyze?host=" + host + "&all=done" 624 | 625 | if fromCache { 626 | command = command + "&fromCache=on" 627 | 628 | if globalMaxAge != 0 { 629 | command = command + "&maxAge=" + strconv.Itoa(globalMaxAge) 630 | } 631 | } else if startNew { 632 | command = command + "&startNew=on" 633 | } 634 | 635 | if globalIgnoreMismatch { 636 | command = command + "&ignoreMismatch=on" 637 | } 638 | 639 | resp, body, err := invokeApi(command) 640 | if err != nil { 641 | return nil, err 642 | } 643 | 644 | // Use the status code to determine if the response is an error. 645 | if resp.StatusCode == 400 { 646 | // Parameter validation error. 647 | 648 | var apiError LabsErrorResponse 649 | err = json.Unmarshal(body, &apiError) 650 | if err != nil { 651 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 652 | return nil, err 653 | } 654 | 655 | return nil, apiError 656 | } else { 657 | // We should have a proper response. 658 | 659 | var analyzeResponse LabsReport 660 | err = json.Unmarshal(body, &analyzeResponse) 661 | if err != nil { 662 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 663 | return nil, err 664 | } 665 | 666 | // Add the JSON body to the response 667 | analyzeResponse.rawJSON = string(body) 668 | 669 | return &analyzeResponse, nil 670 | } 671 | } 672 | 673 | type Event struct { 674 | host string 675 | eventType int 676 | report *LabsReport 677 | } 678 | 679 | const ( 680 | ASSESSMENT_FAILED = -1 681 | ASSESSMENT_STARTING = 0 682 | ASSESSMENT_COMPLETE = 1 683 | ) 684 | 685 | func NewAssessment(host string, eventChannel chan Event) { 686 | eventChannel <- Event{host, ASSESSMENT_STARTING, nil} 687 | 688 | var report *LabsReport 689 | var startTime int64 = -1 690 | var startNew = globalStartNew 691 | 692 | for { 693 | myResponse, err := invokeAnalyze(host, startNew, globalFromCache) 694 | if err != nil { 695 | eventChannel <- Event{host, ASSESSMENT_FAILED, nil} 696 | return 697 | } 698 | 699 | if startTime == -1 { 700 | startTime = myResponse.StartTime 701 | startNew = false 702 | } else { 703 | // Abort this assessment if the time we receive in a follow-up check 704 | // is older than the time we got when we started the request. The 705 | // upstream code should then retry the hostname in order to get 706 | // consistent results. 707 | if myResponse.StartTime > startTime { 708 | eventChannel <- Event{host, ASSESSMENT_FAILED, nil} 709 | return 710 | } else { 711 | startTime = myResponse.StartTime 712 | } 713 | } 714 | 715 | if (myResponse.Status == "READY") || (myResponse.Status == "ERROR") { 716 | report = myResponse 717 | break 718 | } 719 | 720 | time.Sleep(5 * time.Second) 721 | } 722 | 723 | eventChannel <- Event{host, ASSESSMENT_COMPLETE, report} 724 | } 725 | 726 | type HostProvider struct { 727 | hostnames []string 728 | StartingLen int 729 | } 730 | 731 | func NewHostProvider(hs []string) *HostProvider { 732 | hostnames := make([]string, len(hs)) 733 | copy(hostnames, hs) 734 | hostProvider := HostProvider{hostnames, len(hs)} 735 | return &hostProvider 736 | } 737 | 738 | func (hp *HostProvider) next() (string, bool) { 739 | if len(hp.hostnames) == 0 { 740 | return "", false 741 | } 742 | 743 | var e string 744 | e, hp.hostnames = hp.hostnames[0], hp.hostnames[1:] 745 | 746 | return e, true 747 | } 748 | 749 | func (hp *HostProvider) retry(host string) { 750 | hp.hostnames = append(hp.hostnames, host) 751 | } 752 | 753 | type Manager struct { 754 | hostProvider *HostProvider 755 | FrontendEventChannel chan Event 756 | BackendEventChannel chan Event 757 | results *LabsResults 758 | } 759 | 760 | func NewManager(hostProvider *HostProvider) *Manager { 761 | manager := Manager{ 762 | hostProvider: hostProvider, 763 | FrontendEventChannel: make(chan Event), 764 | BackendEventChannel: make(chan Event), 765 | results: &LabsResults{reports: make([]LabsReport, 0)}, 766 | } 767 | 768 | go manager.run() 769 | 770 | return &manager 771 | } 772 | 773 | func (manager *Manager) startAssessment(h string) { 774 | go NewAssessment(h, manager.BackendEventChannel) 775 | activeAssessments++ 776 | } 777 | 778 | func (manager *Manager) run() { 779 | transport := &http.Transport{ 780 | TLSClientConfig: &tls.Config{InsecureSkipVerify: globalInsecure}, 781 | DisableKeepAlives: false, 782 | Proxy: http.ProxyFromEnvironment, 783 | } 784 | 785 | httpClient = &http.Client{Transport: transport} 786 | 787 | // Ping SSL Labs to determine how many concurrent 788 | // assessments we're allowed to use. Print the API version 789 | // information and the limits. 790 | 791 | labsInfo, err := invokeInfo() 792 | if err != nil { 793 | // TODO Signal error so that we return the correct exit code 794 | close(manager.FrontendEventChannel) 795 | } 796 | 797 | if logLevel >= LOG_INFO { 798 | log.Printf("[INFO] SSL Labs v%v (criteria version %v)", labsInfo.EngineVersion, labsInfo.CriteriaVersion) 799 | } 800 | 801 | if logLevel >= LOG_NOTICE { 802 | for _, message := range labsInfo.Messages { 803 | log.Printf("[NOTICE] Server message: %v", message) 804 | } 805 | } 806 | 807 | maxAssessments = labsInfo.MaxAssessments 808 | 809 | if maxAssessments <= 0 { 810 | if logLevel >= LOG_WARNING { 811 | log.Printf("[WARNING] You're not allowed to request new assessments") 812 | } 813 | } 814 | 815 | moreAssessments := true 816 | 817 | if labsInfo.NewAssessmentCoolOff >= 1000 { 818 | globalNewAssessmentCoolOff = 100 + labsInfo.NewAssessmentCoolOff 819 | } else { 820 | if logLevel >= LOG_WARNING { 821 | log.Printf("[WARNING] Info.NewAssessmentCoolOff too small: %v", labsInfo.NewAssessmentCoolOff) 822 | } 823 | } 824 | 825 | for { 826 | select { 827 | // Handle assessment events (e.g., starting and finishing). 828 | case e := <-manager.BackendEventChannel: 829 | if e.eventType == ASSESSMENT_FAILED { 830 | activeAssessments-- 831 | manager.hostProvider.retry(e.host) 832 | } 833 | 834 | if e.eventType == ASSESSMENT_STARTING { 835 | if logLevel >= LOG_INFO { 836 | log.Printf("[INFO] Assessment starting: %v", e.host) 837 | } 838 | } 839 | 840 | if e.eventType == ASSESSMENT_COMPLETE { 841 | if logLevel >= LOG_INFO { 842 | msg := "" 843 | 844 | if len(e.report.Endpoints) == 0 { 845 | msg = fmt.Sprintf("[WARN] Assessment failed: %v (%v)", e.host, e.report.StatusMessage) 846 | } else if len(e.report.Endpoints) > 1 { 847 | msg = fmt.Sprintf("[INFO] Assessment complete: %v (%v hosts in %v seconds)", 848 | e.host, len(e.report.Endpoints), (e.report.TestTime-e.report.StartTime)/1000) 849 | } else { 850 | msg = fmt.Sprintf("[INFO] Assessment complete: %v (%v host in %v seconds)", 851 | e.host, len(e.report.Endpoints), (e.report.TestTime-e.report.StartTime)/1000) 852 | } 853 | 854 | for _, endpoint := range e.report.Endpoints { 855 | if endpoint.Grade != "" { 856 | msg = msg + "\n " + endpoint.IpAddress + ": " + endpoint.Grade 857 | if endpoint.FutureGrade != "" { 858 | msg = msg + " -> " + endpoint.FutureGrade 859 | } 860 | } else { 861 | msg = msg + "\n " + endpoint.IpAddress + ": Err: " + endpoint.StatusMessage 862 | } 863 | } 864 | 865 | log.Println(msg) 866 | } 867 | 868 | activeAssessments-- 869 | 870 | manager.results.reports = append(manager.results.reports, *e.report) 871 | manager.results.responses = append(manager.results.responses, e.report.rawJSON) 872 | 873 | if logLevel >= LOG_DEBUG { 874 | log.Printf("[DEBUG] Active assessments: %v (more: %v)", activeAssessments, moreAssessments) 875 | } 876 | } 877 | 878 | // Are we done? 879 | if (activeAssessments == 0) && (moreAssessments == false) { 880 | close(manager.FrontendEventChannel) 881 | return 882 | } 883 | 884 | break 885 | 886 | // Once a second, start a new assessment, provided there are 887 | // hostnames left and we're not over the concurrent assessment limit. 888 | default: 889 | if manager.hostProvider.StartingLen > 0 { 890 | <-time.NewTimer(time.Duration(globalNewAssessmentCoolOff) * time.Millisecond).C 891 | } 892 | 893 | if moreAssessments { 894 | if currentAssessments < maxAssessments { 895 | host, hasNext := manager.hostProvider.next() 896 | if hasNext { 897 | manager.startAssessment(host) 898 | } else { 899 | // We've run out of hostnames and now just need 900 | // to wait for all the assessments to complete. 901 | moreAssessments = false 902 | 903 | if activeAssessments == 0 { 904 | close(manager.FrontendEventChannel) 905 | return 906 | } 907 | } 908 | } 909 | } 910 | break 911 | } 912 | } 913 | } 914 | 915 | func parseLogLevel(level string) int { 916 | switch { 917 | case level == "error": 918 | return LOG_ERROR 919 | case level == "notice": 920 | return LOG_NOTICE 921 | case level == "info": 922 | return LOG_INFO 923 | case level == "debug": 924 | return LOG_DEBUG 925 | case level == "trace": 926 | return LOG_TRACE 927 | } 928 | 929 | log.Fatalf("[ERROR] Unrecognized log level: %v", level) 930 | return -1 931 | } 932 | 933 | func flattenJSON(inputJSON map[string]interface{}, rootKey string, flattened *map[string]interface{}) { 934 | var keysep = "." // Char to separate keys 935 | var Q = "\"" // Char to envelope strings 936 | 937 | for rkey, value := range inputJSON { 938 | key := rootKey + rkey 939 | if _, ok := value.(string); ok { 940 | (*flattened)[key] = Q + value.(string) + Q 941 | } else if _, ok := value.(float64); ok { 942 | (*flattened)[key] = fmt.Sprintf("%.f", value) 943 | } else if _, ok := value.(bool); ok { 944 | (*flattened)[key] = value.(bool) 945 | } else if _, ok := value.([]interface{}); ok { 946 | for i := 0; i < len(value.([]interface{})); i++ { 947 | aKey := key + keysep + strconv.Itoa(i) 948 | if _, ok := value.([]interface{})[i].(string); ok { 949 | (*flattened)[aKey] = Q + value.([]interface{})[i].(string) + Q 950 | } else if _, ok := value.([]interface{})[i].(float64); ok { 951 | (*flattened)[aKey] = value.([]interface{})[i].(float64) 952 | } else if _, ok := value.([]interface{})[i].(bool); ok { 953 | (*flattened)[aKey] = value.([]interface{})[i].(bool) 954 | } else { 955 | flattenJSON(value.([]interface{})[i].(map[string]interface{}), key+keysep+strconv.Itoa(i)+keysep, flattened) 956 | } 957 | } 958 | } else if value == nil { 959 | (*flattened)[key] = nil 960 | } else { 961 | flattenJSON(value.(map[string]interface{}), key+keysep, flattened) 962 | } 963 | } 964 | } 965 | 966 | func flattenAndFormatJSON(inputJSON []byte) *[]string { 967 | var flattened = make(map[string]interface{}) 968 | 969 | mappedJSON := map[string]interface{}{} 970 | err := json.Unmarshal(inputJSON, &mappedJSON) 971 | if err != nil { 972 | log.Fatalf("[ERROR] Reconsitution of JSON failed: %v", err) 973 | } 974 | 975 | // Flatten the JSON structure, recursively 976 | flattenJSON(mappedJSON, "", &flattened) 977 | 978 | // Make a sorted index, so we can print keys in order 979 | kIndex := make([]string, len(flattened)) 980 | ki := 0 981 | for key, _ := range flattened { 982 | kIndex[ki] = key 983 | ki++ 984 | } 985 | sort.Strings(kIndex) 986 | 987 | // Ordered flattened data 988 | var flatStrings []string 989 | for _, value := range kIndex { 990 | flatStrings = append(flatStrings, fmt.Sprintf("\"%v\": %v\n", value, flattened[value])) 991 | } 992 | return &flatStrings 993 | } 994 | 995 | func readLines(path *string) ([]string, error) { 996 | file, err := os.Open(*path) 997 | if err != nil { 998 | return nil, err 999 | } 1000 | defer file.Close() 1001 | 1002 | var lines []string 1003 | scanner := bufio.NewScanner(file) 1004 | for scanner.Scan() { 1005 | var line = strings.TrimSpace(scanner.Text()) 1006 | if (!strings.HasPrefix(line, "#")) && (line != "") { 1007 | lines = append(lines, line) 1008 | } 1009 | } 1010 | return lines, scanner.Err() 1011 | } 1012 | 1013 | func validateURL(URL string) bool { 1014 | _, err := url.Parse(URL) 1015 | if err != nil { 1016 | return false 1017 | } else { 1018 | return true 1019 | } 1020 | } 1021 | 1022 | func validateHostname(hostname string) bool { 1023 | addrs, err := net.LookupHost(hostname) 1024 | 1025 | // In some cases there is no error 1026 | // but there are also no addresses 1027 | if err != nil || len(addrs) < 1 { 1028 | return false 1029 | } else { 1030 | return true 1031 | } 1032 | } 1033 | 1034 | func main() { 1035 | var conf_api = flag.String("api", "BUILTIN", "API entry point, for example https://www.example.com/api/") 1036 | var conf_grade = flag.Bool("grade", false, "Output only the hostname: grade") 1037 | var conf_hostcheck = flag.Bool("hostcheck", false, "If true, host resolution failure will result in a fatal error.") 1038 | var conf_hostfile = flag.String("hostfile", "", "File containing hosts to scan (one per line)") 1039 | var conf_ignore_mismatch = flag.Bool("ignore-mismatch", false, "If true, certificate hostname mismatch does not stop assessment.") 1040 | var conf_insecure = flag.Bool("insecure", false, "Skip certificate validation. For use in development only. Do not use.") 1041 | var conf_json_flat = flag.Bool("json-flat", false, "Output results in flattened JSON format") 1042 | var conf_quiet = flag.Bool("quiet", false, "Disable status messages (logging)") 1043 | var conf_usecache = flag.Bool("usecache", false, "If true, accept cached results (if available), else force live scan.") 1044 | var conf_maxage = flag.Int("maxage", 0, "Maximum acceptable age of cached results, in hours. A zero value is ignored.") 1045 | var conf_verbosity = flag.String("verbosity", "info", "Configure log verbosity: error, notice, info, debug, or trace.") 1046 | var conf_version = flag.Bool("version", false, "Print version and API location information and exit") 1047 | 1048 | flag.Parse() 1049 | 1050 | if *conf_version { 1051 | fmt.Println(USER_AGENT) 1052 | fmt.Println("API location: " + apiLocation) 1053 | return 1054 | } 1055 | 1056 | globalIgnoreMismatch = *conf_ignore_mismatch 1057 | 1058 | if *conf_quiet { 1059 | logLevel = LOG_NONE 1060 | } else { 1061 | logLevel = parseLogLevel(strings.ToLower(*conf_verbosity)) 1062 | } 1063 | 1064 | // We prefer cached results 1065 | if *conf_usecache { 1066 | globalFromCache = true 1067 | globalStartNew = false 1068 | } 1069 | 1070 | if *conf_maxage != 0 { 1071 | globalMaxAge = *conf_maxage 1072 | } 1073 | 1074 | // Verify that the API entry point is a URL. 1075 | if *conf_api != "BUILTIN" { 1076 | apiLocation = *conf_api 1077 | } 1078 | 1079 | if validateURL(apiLocation) == false { 1080 | log.Fatalf("[ERROR] Invalid API URL: %v", apiLocation) 1081 | } 1082 | 1083 | var hostnames []string 1084 | 1085 | if *conf_hostfile != "" { 1086 | // Open file, and read it 1087 | var err error 1088 | hostnames, err = readLines(conf_hostfile) 1089 | if err != nil { 1090 | log.Fatalf("[ERROR] Reading from specified hostfile failed: %v", err) 1091 | } 1092 | 1093 | } else { 1094 | // Read hostnames from the rest of the args 1095 | hostnames = flag.Args() 1096 | } 1097 | 1098 | if *conf_hostcheck { 1099 | // Validate all hostnames before we attempt to test them. At least 1100 | // one hostname is required. 1101 | for _, host := range hostnames { 1102 | if validateHostname(host) == false { 1103 | log.Fatalf("[ERROR] Invalid hostname: %v", host) 1104 | } 1105 | } 1106 | } 1107 | 1108 | if *conf_insecure { 1109 | globalInsecure = *conf_insecure 1110 | } 1111 | 1112 | hp := NewHostProvider(hostnames) 1113 | manager := NewManager(hp) 1114 | 1115 | // Respond to events until all the work is done. 1116 | for { 1117 | _, running := <-manager.FrontendEventChannel 1118 | if running == false { 1119 | var err error 1120 | 1121 | if hp.StartingLen == 0 { 1122 | return 1123 | } 1124 | 1125 | if *conf_grade { 1126 | for i := range manager.results.responses { 1127 | results := []byte(manager.results.responses[i]) 1128 | 1129 | // Fill LabsReport with json response received i.e results 1130 | var labsReport LabsReport 1131 | err = json.Unmarshal(results, &labsReport) 1132 | // Check for error while unmarshalling. If yes then display error messsage and terminate the program 1133 | if err != nil { 1134 | log.Fatalf("[ERROR] JSON unmarshal error: %v", err) 1135 | } 1136 | 1137 | // Printing the Hostname and IpAddress with grades 1138 | fmt.Println() 1139 | if !strings.EqualFold(labsReport.StatusMessage, "ERROR") { 1140 | fmt.Printf("HostName:\"%v\"\n", labsReport.Host) 1141 | for _, endpoints := range labsReport.Endpoints { 1142 | if endpoints.FutureGrade != "" { 1143 | fmt.Printf("\"%v\":\"%v\"->\"%v\"\n", endpoints.IpAddress, endpoints.Grade, endpoints.FutureGrade) 1144 | } else { 1145 | if endpoints.Grade != "" { 1146 | fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.Grade) 1147 | } else { 1148 | // When no grade is seen print Status Message 1149 | fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.StatusMessage) 1150 | } 1151 | } 1152 | } 1153 | } 1154 | } 1155 | } else if *conf_json_flat { 1156 | // Flat JSON and RAW 1157 | 1158 | for i := range manager.results.responses { 1159 | results := []byte(manager.results.responses[i]) 1160 | 1161 | flattened := flattenAndFormatJSON(results) 1162 | 1163 | // Print the flattened data 1164 | fmt.Println(*flattened) 1165 | } 1166 | } else { 1167 | // Raw (non-Go-mangled) JSON output 1168 | 1169 | fmt.Println("[") 1170 | for i := range manager.results.responses { 1171 | results := manager.results.responses[i] 1172 | 1173 | if i > 0 { 1174 | fmt.Println(",") 1175 | } 1176 | fmt.Println(results) 1177 | 1178 | } 1179 | fmt.Println("]") 1180 | } 1181 | 1182 | if err != nil { 1183 | log.Fatalf("[ERROR] Output to JSON failed: %v", err) 1184 | } 1185 | 1186 | if logLevel >= LOG_INFO { 1187 | log.Println("[INFO] All assessments complete; shutting down") 1188 | } 1189 | 1190 | return 1191 | } 1192 | } 1193 | } 1194 | -------------------------------------------------------------------------------- /pulse.sql: -------------------------------------------------------------------------------- 1 | 2 | # Note: This schema designed for PostgreSQL. 3 | 4 | CREATE TABLE domains ( 5 | domainName VARCHAR(254) NOT NULL, 6 | -- The domain name, with the "www" prefix (where such prefix exists). 7 | 8 | domainDepth INTEGER NOT NULL CHECK (domainDepth > 0), 9 | -- How far is the doimain name from its TLD? For example, 10 | -- example.com would have a depth of 1; webmail.example.com a depth of 2. 11 | -- The "www" prefix is ignored, if it exists. 12 | 13 | ipAddress VARCHAR(16) NOT NULL, 14 | -- The IP address the domain name resolved to at the time of testing 15 | 16 | port INTEGER NOT NULL, 17 | -- Always 443. 18 | 19 | 20 | -- Time-related fields 21 | 22 | checkTime TIMESTAMP NOT NULL, 23 | -- The time when the test began. 24 | 25 | handshakeDuration INTEGER CHECK (handshakeDuration > 0) NOT NULL, 26 | -- Unused. 27 | 28 | validationDuration INTEGER CHECK (validationDuration >= 0) NOT NULL, 29 | -- Unused. 30 | 31 | requestDuration INTEGER CHECK (requestDuration > 0) NOT NULL, 32 | -- The time, in milliseconds, it took the server to respond to the 33 | -- request we issued on an established SSL connection. 34 | 35 | configCheckDuration INTEGER CHECK (configCheckDuration > 0) NOT NULL, 36 | -- The time, in milliseconds, it took us to execute all other tests (those 37 | -- performed using the custom SSL inspection library). 38 | 39 | 40 | -- Certificate information 41 | 42 | subject TEXT NOT NULL, 43 | -- X500 principal in RFC 2253 format. Encoded. 44 | 45 | subjectCommonName TEXT, 46 | -- Common names extracted from the subject, if any. If multiple common names 47 | -- are present, they are separated with spaces. Encoded. 48 | 49 | subjectCountry VARCHAR(32) NOT NULL, 50 | -- The country name extracted from the subject. The field should contain a 51 | -- two-letter country code, but many are invalid. Contains "Unknown" if unknown. 52 | -- Encoded. 53 | 54 | altNames TEXT NOT NULL, 55 | -- Space-separated alternative names from the certificates. Encoded. 56 | 57 | altNameCount INTEGER NOT NULL, 58 | -- How many alternative names are there in the certificate? 59 | 60 | isWildcard BOOLEAN NOT NULL, 61 | -- Is the common name a wildcard? 62 | 63 | prefixSupport BOOLEAN NOT NULL, 64 | -- Does the certificate support both prefix and prefix-less domain name? 65 | 66 | issuer TEXT NOT NULL, 67 | -- X500 issuer in RFC 2253 format. Encoded. 68 | 69 | issuerCommonName TEXT, 70 | -- Issuer common name. Encoded. 71 | 72 | notAfter TIMESTAMP WITH TIME ZONE NOT NULL, 73 | -- Certificate expiration date. 74 | 75 | notBefore TIMESTAMP WITH TIME ZONE NOT NULL, 76 | -- Certificate activation date. 77 | 78 | signatureAlg VARCHAR(64), 79 | -- Hash algorithm, e.g., MD5, SHA, etc. Encoded. 80 | 81 | keyAlg VARCHAR(64) NOT NULL, 82 | -- Public key algorithm: RSA, DSA, or EC. Encoded. 83 | 84 | keySize INTEGER NOT NULL CHECK (keySize > 0), 85 | -- Key size, in the units specific to the algorithm (also see keyStrength, 86 | -- which contains the strength of the key in RSA bits). 87 | 88 | validationType CHAR(1) NOT NULL, 89 | -- Values: Domain-validated (D), Organisation-validated (O), Extended Validation (E) 90 | -- ONLY IMPLEMENTED FOR EXTENDED VALIDATION; OTHER VALUES NOT RELIABLE 91 | 92 | isScg INTEGER NOT NULL, 93 | -- Support for server-gated cryptography, as below: 94 | -- 0 - none 95 | -- 1 - Netscape SGC 96 | -- 2 - Microsoft SGC 97 | -- 3 - Netscape & Microsoft SGC 98 | 99 | revocationInfo INTEGER NOT NULL, 100 | -- bit 0: 1 - CRL 101 | -- bit 1: 2 - OCSP 102 | 103 | chainLength INTEGER NOT NULL CHECK (chainLength > 0), 104 | -- The length of the chain sent by the server. 105 | 106 | chainSize INTEGER NOT NULL CHECK (chainSize > 0), 107 | -- Chain size, in bytes. 108 | 109 | chainIssuers TEXT NOT NULL, 110 | -- A list of all chain issuers, separated with newlines. Encoded. 111 | 112 | chainData TEXT NOT NULL, 113 | -- Contains raw chain data. Base64-encoded. 114 | 115 | isTrusted INTEGER NOT NULL, 116 | -- a certificate is valid if it is trusted, if the time is right, and 117 | -- if the common name matches the expected hostname. The value of this 118 | -- field is a combination of the following bits: 119 | -- 1 - no chain of trust 120 | -- 2 - not before 121 | -- 4 - not after 122 | -- 8 - hostname mismatch 123 | -- 16 - revoked 124 | -- 32 - bad common name 125 | -- 64 - self-signed 126 | 127 | 128 | -- Protocol support 129 | 130 | supports_SSL2_hello INTEGER NOT NULL, 131 | -- 0 - no support 132 | -- 1 - responded with SSLv2 133 | -- 2 - responded with SSLv3+ 134 | 135 | supports_SSL_2_0 BOOLEAN NOT NULL, 136 | supports_SSL_3_0 BOOLEAN NOT NULL, 137 | supports_TLS_1_0 BOOLEAN NOT NULL, 138 | supports_TLS_1_1 BOOLEAN NOT NULL, 139 | supports_TLS_1_2 BOOLEAN NOT NULL, 140 | 141 | 142 | -- Cipher suite information 143 | 144 | suites TEXT NOT NULL, 145 | -- A space-separated list of supported cipher suites (hexadecimal numbers). 146 | 147 | suiteCount INTEGER NOT NULL, 148 | -- How many supported cipher suites are there? 149 | 150 | suitesInOrder BOOLEAN, 151 | -- TRUE if server has cipher suite preference, in which 152 | -- case the suites will be listed in the preferred order. 153 | -- FALSE if there's no preference, or NULL if we could 154 | -- not determine the preference. 155 | 156 | supports_no_bits BOOLEAN NOT NULL, 157 | supports_low_bits BOOLEAN NOT NULL, 158 | supports_128_bits BOOLEAN NOT NULL, 159 | supports_256_bits BOOLEAN NOT NULL, 160 | 161 | em_ssl2 BOOLEAN NOT NULL, 162 | em_40_bits BOOLEAN NOT NULL, 163 | em_56_bits BOOLEAN NOT NULL, 164 | em_64_bits BOOLEAN NOT NULL, 165 | -- The above fields indicate if error messages are used to refuse to 166 | -- respond to HTTP requests over weak protocols or suites. The tests 167 | -- currently only know how to detect the error messagees from 168 | -- NetScaler and Microsoft. 169 | 170 | serverSignature VARCHAR(254), 171 | -- HTTP Server signature; can be NULL. Encoded. 172 | 173 | grade INTEGER CHECK ((grade >= 0) AND (grade <= 100)) NOT NULL, 174 | -- SSL Labs numerical grade, according to the 2009c rating guide. 175 | 176 | gradeLetter CHAR(1) NOT NULL, 177 | -- SSL Labs letter grade (A-F), according to the 2009c rating guide. 178 | 179 | 180 | -- Special tests 181 | 182 | debianFlawed BOOLEAN, 183 | -- Has a low-entropy certificate been detected? 184 | 185 | sessionResumption INTEGER NOT NULL, 186 | -- 0 - empty session IDs 187 | -- 1 - session IDs provided, but not reused 188 | -- 2 - enabled 189 | 190 | stsResponseHeader TEXT, 191 | -- Strict-Transport-Security response header, if seen. 192 | 193 | renegSupport INTEGER, 194 | -- bit 0: insecure client-initiated renegotiation supported 195 | -- bit 1: secure renegotiation supported 196 | -- bit 2: secure client-initiated renegotiation supported 197 | -- bit 3: server requires secure renegotiation support 198 | 199 | toleranceMinorLow INTEGER NOT NULL, 200 | -- The protocol version we received in response to 201 | -- attempting to negotiate version 0x0304. Contains -1 202 | -- if connection failed without response. 203 | 204 | toleranceMinorHigh INTEGER NOT NULL, 205 | -- The protocol version we received in response to 206 | -- attempting to negotiate version 0x0399. Contains -1 207 | -- if connection failed without response. 208 | 209 | toleranceMajorHigh INTEGER NOT NULL, 210 | -- The protocol version we received in response to 211 | -- attempting to negotiate version 0x0499. Contains -1 212 | -- if connection failed without response. 213 | 214 | pciReady BOOLEAN NOT NULL, 215 | -- TRUE if the certificate is trusted, the key size is 1024 or better, 216 | -- and only strong protocols (no SSLv2) and cipher suites (>= 128 bits, 217 | -- no ADH, or export ones) are supported. 218 | 219 | fipsReady BOOLEAN NOT NULL, 220 | -- As above, but only allows the use of FIPS-approved cipher suites 221 | -- along with TLS v1.0 and better. 222 | 223 | chainIssues INTEGER NOT NULL, 224 | -- bit 0 - unused 225 | -- bit 1 - incomplete chain (set only when we were able to build a trusted 226 | -- chain by adding missing intermediate certificates from external sources) 227 | -- bit 2 - chain contains unrelated certificates (i.e., certificates that are not 228 | -- part of the same chain) 229 | -- bit 3 - the certificates form a chain (trusted or not), but the order is incorrect 230 | -- bit 4 - contains root certificate (not set for self-signed leafs) 231 | -- bit 5 - the certificates form a chain, but we could not validate it (if the leaf 232 | -- was trusted, that means that we built a different chain we trusted). 233 | 234 | fixedChainLength INTEGER NOT NULL, 235 | -- Unused. 236 | 237 | fixedChainSize INTEGER NOT NULL, 238 | -- Unused. 239 | 240 | trustAnchor TEXT, 241 | -- X500 trust anchor in RFC 2253 format. Encoded. 242 | 243 | engineVersion VARCHAR(64) NOT NULL, 244 | -- Assessment engine version number. 245 | 246 | criteriaVersion VARCHAR(64) NOT NULL, 247 | -- Version number of the criteria used for grading. 248 | 249 | crlUris TEXT NOT NULL, 250 | -- Space-separated CRL endpoints. Can be empty. 251 | 252 | ocspUris TEXT NOT NULL, 253 | -- Space-separated OCSP endpoints. Can be empty. 254 | 255 | vulnBEAST BOOLEAN, 256 | -- Is the server vulnerable to the BEAST attack? 257 | 258 | stsMaxAge INTEGER, 259 | -- Maximum age used in HSTS. Available only when HSTS is present. 260 | 261 | stsIncludeSubdomains BOOLEAN, 262 | -- Whether the includeSubDomains feature was enabled in 263 | -- the HSTS response. Available only when HSTS is present. 264 | 265 | pkpResponseHeader TEXT, 266 | -- Public-Key-Pins response header, if seen. 267 | 268 | surveyId VARCHAR(32) NOT NULL, 269 | -- Used to identify the assessments that are part of the same round. 270 | 271 | compression INTEGER, 272 | -- bit 0: DEFLATE 273 | 274 | npnSupport BOOLEAN, 275 | 276 | npnProtocols TEXT, 277 | -- Space-separated list of supported NPN protocols 278 | 279 | sessionTickets INTEGER, 280 | -- bit 0 - set if session tickets are supported 281 | -- bit 1 (not implemented) - faulty 282 | -- bit 2 - set if the server is intolerant to the extension 283 | 284 | ocspStapling BOOLEAN, 285 | -- True if OCSP stapling support was detected 286 | 287 | sniRequired BOOLEAN, 288 | -- True if SNI is required for the hostname 289 | 290 | httpStatusCode INTEGER, 291 | -- The status code we received on the main HTTP request. Can 292 | -- be NULL if the request was not successful. 293 | 294 | httpForwarding VARCHAR(254), 295 | -- Contains the URL to which the server being tested redirects to 296 | -- (redirections to URLs within the same hostname are excluded). 297 | 298 | keyStrength INTEGER CHECK (keyStrength > 0), 299 | -- Key size, converted to an RSA equivalent when Elliptic Curves are used. 300 | 301 | -- This table has a design flaw, in that it assumes a server 302 | -- can have only one certificate chain. But some servers may have 303 | -- several, for example, one for each RSA, DSA, and ECDSA. This 304 | -- is very rare, but will increase in the future as ECDSA certificates 305 | -- become more popular. 306 | -- 307 | -- To fix the issue, the schema will need to be significantly changed. Until 308 | -- then, I am adding a couple of fields to track the usage of multiple 309 | -- certificate chains. 310 | 311 | chainCount INTEGER CHECK (chainCount > 0), 312 | -- How many different certificate chains were seen? 313 | 314 | chainData2 TEXT, 315 | -- Contains raw chain data. Base64-encoded. 316 | 317 | chainData3 TEXT, 318 | -- Contains raw chain data. Base64-encoded. 319 | 320 | rg2009b_grade INTEGER CHECK ((rg2009b_grade >= 0) AND (rg2009b_grade <= 100)), 321 | -- SSL Labs numerical grade, according to the 2009b rating guide. 322 | 323 | rg2009b_letter CHAR(1) CHECK (rg2009b_letter IN ('A', 'B', 'C', 'D', 'E', 'F')), 324 | -- SSL Labs letter grade (A-F), according to the 2009b rating guide. 325 | 326 | beastSuites VARCHAR(128), 327 | -- Cipher suites obtained when attempting the protocols vulnerable to the BEAST attack. 328 | -- Example "300:35; 301:35". Protocol number first, followed by the suite number. Both hex. 329 | 330 | protocolIntolerance INTEGER, 331 | -- bit 0: TLS 1.0 332 | -- bit 1: TLS 1.1 333 | -- bit 2: TLS 1.2 334 | 335 | miscIntolerance INTEGER, 336 | -- bit 0: extension intolerance 337 | -- bit 1: long handshake intolerance 338 | -- bit 2: long handshake intolerance workaround success 339 | 340 | sims TEXT, 341 | -- simulation results (e.g., "34 (0 1 301 c011); 35 (0 1 303 c028)" 342 | -- The first number is the client ID. Then follows the error code (zero 343 | -- means no error), how many connection attempts were made, and the negotiated 344 | -- protocol and cipher suite (the last two are hexadecimal). 345 | 346 | forwardSecrecy INTEGER, 347 | -- bit 0: none/some 348 | -- bit 1: modern browsers 349 | -- bit 2: robust (ECDHE + DHE) 350 | 351 | rc4 INTEGER, 352 | -- bit 0: RC4 present among the suites 353 | -- bit 1: RC4 seen used by grade 0 clients 354 | 355 | hasWarnings BOOLEAN, 356 | 357 | isExceptional BOOLEAN, 358 | 359 | heartbeat BOOLEAN, 360 | 361 | heartbleed BOOLEAN, 362 | 363 | cve_2014_0224 INTEGER, 364 | -- One of the following values are possible: 365 | -- -1: the test failed 366 | -- 0: unknown 367 | -- 1: not vulnerable 368 | -- 2: vulnerable but not exploitable (OpenSSL 0.9.x and 1.0.0; false positives possible) 369 | -- 3: vulnerable and exploitable (OpenSSL 1.0.1+) 370 | 371 | PRIMARY KEY (domainName, surveyId) 372 | ); 373 | 374 | CREATE INDEX domains_ipAddress ON domains(ipAddress); 375 | -------------------------------------------------------------------------------- /ssllabs-api-docs-v2-deprecated.md: -------------------------------------------------------------------------------- 1 | # SSL Labs API Documentation v1.21.13 # 2 | 3 | **Last update:** 9 December 2015
4 | **Author:** Ivan Ristic 5 | 6 | This document explains the SSL Labs Assessment APIs, which can be used to test SSL servers available on the public Internet. 7 | 8 | ## Protocol Overview ## 9 | 10 | The protocol is based on HTTP and JSON. All invocations of the API should use the GET method and specify the parameters in the query string, as documented below. The results will be returned in the response body as a JSON payload. In essence, the client submits an assessment reque`sts` to the servers. If an acceptable report is already available, it's received straight away. Otherwise, the server will start a new assessment and the client should periodically check to see if the job is complete. 11 | 12 | ### Terms and Conditions ### 13 | 14 | SSL Labs APIs are provided free of charge, subject to our terms and conditions: . The spirit of the license is that the APIs are made available so that system operators can test their own infrastructure. Please read the actual terms and conditions, which are more involved and cover things such as integrating with open source projects, and so on. For example, it's important (for reasons of privacy, compliance, etc) for end users to understand that assessments are carried out by Qualys's servers, not locally. 15 | 16 | Commercial use is generally not allowed, except with an explicit permission from Qualys. That said, we're usually happy to support good causes, even uses by commercial organizations that help improve the security of their customers. If you're a CA, CDN, hosting company, domain name registrar, we're happy for you to use our APIs (but you still have to get in touch with us before you begin). 17 | 18 | ### Protocol Calls ### 19 | 20 | This section documents the available protocol calls. The main API entry point is "https://api.ssllabs.com/api/v2/". If you want to check the API availability from a browser, invoke "https://api.ssllabs.com/api/v2/info". There is also an additional API entry point that can be used to test features that have not yet been deployed to production: "https://api.dev.ssllabs.com/api/v2/". You should expect that this second entry point is not consistently available. Further it offers only reduced assessment limits in comparison with the production version. 21 | 22 | #### Check SSL Labs availability #### 23 | 24 | This call should be used to check the availability of the SSL Labs servers, retrieve the engine and criteria version, and initialize the maximum number of concurrent assessments. Returns one [Info object](#info) on success. 25 | 26 | **API Call:** `info` 27 | 28 | Parameters: 29 | 30 | * None. 31 | 32 | #### Invoke assessment and check progress #### 33 | 34 | This call is used to initiate an assessment, or to retrieve the status of an assessment in progress or in the cache. It will return a single [Host object](#host) on success. The Endpoint object embedded in the Host object will provide partial endpoint results. Please note that assessments of individual endpoints can fail even when the overall assessment is successful (e.g., one server might be down). At this time, you can determine the success of an endpoint assessment by checking the statusMessage field; it should contain "Ready". 35 | 36 | **API Call:** `analyze` 37 | 38 | Parameters: 39 | 40 | * **host** - hostname; required. 41 | * **publish** - set to "on" if assessment results should be published on the public results boards; optional, defaults to "off". 42 | * **startNew** - if set to "on" then cached assessment results are ignored and a new assessment is started. However, if there's already an assessment in progress, its status is delivered instead. This parameter should be used only once to initiate a new assessment; further invocations should omit it to avoid causing an assessment loop. 43 | * **fromCache** - always deliver cached assessment reports if available; optional, defaults to "off". This parameter is intended for API consumers that don't want to wait for assessment results. Can't be used at the same time as the startNew parameter. 44 | * **maxAge** - maximum report age, in hours, if retrieving from cache (fromCache parameter set). 45 | * **all** - by default this call results only summaries of individual endpoints. If this parameter is set to "on", full information will be returned. If set to "done", full information will be returned only if the assessment is complete (status is READY or ERROR). 46 | * **ignoreMismatch** - set to "on" to proceed with assessments even when the server certificate doesn't match the assessment hostname. Set to off by default. Please note that this parameter is ignored if a cached report is returned. 47 | 48 | Examples: 49 | 50 | * `/analyze?host=www.ssllabs.com` 51 | * `/analyze?host=www.ssllabs.com&publish=on` 52 | 53 | #### Retrieve detailed endpoint information #### 54 | 55 | This call is used to retrieve detailed endpoint information. It will return a single [Endpoint object](#endpoint) on success. The object will contain complete assessment information. This API call does not initiate new assessments, even when a cached report is not found. 56 | 57 | **API Call:** `getEndpointData` 58 | 59 | Parameters: 60 | 61 | * **host** - as above 62 | * **s** - endpoint IP address 63 | * **fromCache** - see above. 64 | 65 | Example: 66 | 67 | * `/getEndpointData?host=www.ssllabs.com&s=173.203.82.166` 68 | 69 | #### Retrieve known status codes #### 70 | 71 | This call will return one [StatusCodes instance](#statuscodes). 72 | 73 | **API Call:** `getStatusCodes` 74 | 75 | Parameters: 76 | 77 | * None. 78 | 79 | #### Retrieve root certificates #### 80 | 81 | This call returns the root certificates used for trust validation. 82 | 83 | **API Call:** `getRootCertsRaw` 84 | 85 | Parameters: 86 | 87 | * None. 88 | 89 | ### Protocol Usage ### 90 | 91 | When you want to obtain fresh test results for a particular host: 92 | 93 | 1. Invoke `analyze` with the `startNew` parameter to `on`. Set `all` to `done`. 94 | 2. The assessment is now in progress. Call `analyze` periodically (without the `startNew` parameter!) until the assessment is finished. You can tell by observing the `Host.status` field for either READY or ERROR values. 95 | 3. When there are multiple servers behind one hostname, they will be tested one at a time. 96 | 4. During the assessment, interim responses will contain only endpoint status, but not full information. 97 | 5. At the end of the assessment, the response will contain all available information; no further API calls will need to be made for that host. 98 | 99 | When you're happy to receive cached information (e.g., in a browser add-on): 100 | 101 | 1. Invoke `analyze` with `fromCache` set to `on` and `all` set to `done`. 102 | 2. Set `maxAge` to control the maximum age of the cached report. If you don't set this parameter, your IP address will not be forwarded to the tested server. 103 | 3. If the information you requested is available in the cache, it will be returned straight away. 104 | 4. Otherwise, a new assessment will be started. 105 | 5. You can continue to call `analyze` periodically until the assessment is complete. 106 | 107 | ### Error Reporting ### 108 | 109 | When an API call is incorrectly invoked, it will cause an error response to be sent back. The response will include an array of error messages. For example: 110 | 111 | {"errors":[{"field":"host","message":"qp.mandatory"}]} 112 | 113 | The field value references the API parameter name that has an incorrect value. The message value will tell you what the issue is. It is also possible to receive errors without the field parameter set; such messages will usually refer to the request as a whole. 114 | 115 | ### Error Response Status Codes ### 116 | 117 | The following status codes are used: 118 | 119 | * 400 - invocation error (e.g., invalid parameters) 120 | * 429 - client request rate too high or too many new assessments too fast 121 | * 500 - internal error 122 | * 503 - the service is not available (e.g., down for maintenance) 123 | * 529 - the service is overloaded 124 | 125 | A well-written client should never get a 429 response. If you do get one, it means that you're either submitting new assessments at a rate that is too fast, or that you're not correctly tracking how many concurrent requests you're allowed to have. If you get a 503 or 529 status code, you should sleep for several minutes (e.g., 15 and 30 minutes, respectively) then try again. It's best to randomize the delay, especially if you're writing a client tool -- you don't want everyone to retry exactly at the same time. If you get 500, it means that there's a severe problem with the SSL Labs application itself. A sensible approach would be to mark that one assessment as flawed, but to continue on. However, if you continue to receive 500 responses, it's best to give up. 126 | 127 | ### Access Rate and Rate Limiting ### 128 | 129 | Please note the following: 130 | 131 | * Server assessments usually take at least 60 seconds. (They are intentionally slow, to avoid harming servers.) Thus, there is no need to poll for the results very often. In fact, polling too often slows down the service for everyone. It's best to use variable polling: 5 seconds until an assessment gets under way (status changes to IN_PROGRESS), then 10 seconds until it completes. 132 | * Keep down the number of concurrent assessments to a minimum. If you're not in a hurry, test only one hostname at a time. 133 | 134 | We may limit your usage of the API, by enforcing a limit on concurrent assessments, and the overall number of assessments performed in a time period. If that happens, we will respond with 429 (Too Many Requests) to API calls that wish to initiate new assessments. Your ability to follow previously initiated assessments, or retrieve assessment results from the cache, will not be impacted. If you receive a 429 response, reduce the number of concurrent assessments and check that you're not submitting new assessments at a rate higher than allowed. 135 | 136 | If the server is overloaded (a condition that is not a result of the client's behaviour), the 529 status code will be used instead. This is not a situation we wish to be in. If you encounter it, take a break and come back later. 137 | 138 | All successful API calls contain response headers `X-Max-Assessments` and `X-Current-Assessments`. They can be used to calculate how many new 139 | assessments can be submitted. It is recommended that clients update their internal state after each complete response. 140 | 141 | ### Protocol Evolution ### 142 | 143 | The API is versioned. New versions of the API will be introduced whenever incompatible changes need to be made to the protocol. When a new version becomes available, existing applications can continue to use the previous version for as long as it is supported. 144 | 145 | To reduce version number inflation, new fields may be added to the results without a change in protocol version number. 146 | 147 | ## Response Objects ## 148 | 149 | The remainder of the document explains the structure of the returned objects. The following conventions are used: 150 | 151 | * **field** - a simple field 152 | * **object{}** - an object 153 | * **array[]** - an array 154 | 155 | ### Host ### 156 | 157 | * **host** - assessment host, which can be a hostname or an IP address 158 | * **port** - assessment port (e.g., 443) 159 | * **protocol** - protocol (e.g., HTTP) 160 | * **isPublic** - true if this assessment publicly available (listed on the SSL Labs assessment boards) 161 | * **status** - assessment status; possible values: DNS, ERROR, IN_PROGRESS, and READY. 162 | * **statusMessage** - status message in English. When status is ERROR, this field will contain an error message. 163 | * **startTime** - assessment starting time, in milliseconds since 1970 164 | * **testTime** - assessment completion time, in milliseconds since 1970 165 | * **engineVersion** - assessment engine version (e.g., "1.0.120") 166 | * **criteriaVersion** - grading criteria version (e.g., "2009") 167 | * **cacheExpiryTime** - when will the assessment results expire from the cache (typically set only for assessment with errors; otherwise the results stay in the cache for as long as there's sufficient room) 168 | * **endpoints[]** - list of [Endpoint objects](#endpoint) 169 | * **certHostnames[]** - the list of certificate hostnames collected from the certificates seen during assessment. The hostnames may not be valid. This field is available only if the server certificate doesn't match the requested hostname. In that case, this field saves you some time as you don't have to inspect the certificates yourself to find out what valid hostnames might be. 170 | 171 | ### Endpoint ### 172 | 173 | * **ipAddress** - endpoint IP address, in IPv4 or IPv6 format. 174 | * **serverName** - server name retrieved via reverse DNS 175 | * **statusMessage** - assessment status message; this field will contain "Ready" if the endpoint assessment was successful. 176 | * **statusDetails** - code of the operation currently in progress 177 | * **statusDetailsMessage** - description of the operation currently in progress 178 | * **grade** - possible values: A+, A-, A-F, T (no trust) and M (certificate name mismatch) 179 | * **gradeTrustIgnored** - grade (as above), if trust issues are ignored 180 | * **hasWarnings** - if this endpoint has warnings that might affect the score (e.g., get A- instead of A). 181 | * **isExceptional** - this flag will be raised when an exceptional configuration is encountered. The SSL Labs test will give such sites an A+. 182 | * **progress** - assessment progress, which is a value from 0 to 100, and -1 if the assessment has not yet started 183 | * **duration** - assessment duration, in milliseconds 184 | * **eta** - estimated time, in seconds, until the completion of the assessment 185 | * **delegation** - indicates domain name delegation with and without the www prefix 186 | * bit 0 (1) - set for non-prefixed access 187 | * bit 1 (2) - set for prefixed access 188 | * **details** - this field contains an EndpointDetails object. It's not present by default, but can be enabled by using the "all" paramerer to the `analyze` API call. 189 | 190 | ### EndpointDetails ### 191 | 192 | * **hostStartTime** = endpoint assessment starting time, in milliseconds since 1970. This field is useful when test results are retrieved in several HTTP invocations. Then, you should check that the hostStartTime value matches the startTime value of the host. 193 | * **key{}** - [key information](#key) 194 | * **cert{}** - [certificate information](#cert) 195 | * **chain{}** - [chain information](#chain) 196 | * **protocols[]** - supported [protocols](#protocol) 197 | * **suites{}** - supported [cipher suites](#suites) 198 | * **serverSignature** - Contents of the HTTP Server response header when known. This field could be absent for one of two reasons: 1) the HTTP request failed (check httpStatusCode) or 2) there was no Server response header returned. 199 | * **prefixDelegation** - true if this endpoint is reachable via a hostname with the www prefix 200 | * **nonPrefixDelegation** (moved here from the summary) - true if this endpoint is reachable via a hostname without the www prefix 201 | * **vulnBeast** - true if the endpoint is vulnerable to the BEAST attack 202 | * **renegSupport** - this is an integer value that describes the endpoint support for renegotiation: 203 | * bit 0 (1) - set if insecure client-initiated renegotiation is supported 204 | * bit 1 (2) - set if secure renegotiation is supported 205 | * bit 2 (4) - set if secure client-initiated renegotiation is supported 206 | * bit 3 (8) - set if the server requires secure renegotiation support 207 | * **stsStatus** - deprecated 208 | * **stsResponseHeader** - deprecated 209 | * **stsMaxAge** - deprecated 210 | * **stsSubdomains** - deprecated 211 | * **stsPreload** - deprecated 212 | * **pkpResponseHeader** - deprecated 213 | * **sessionResumption** - this is an integer value that describes endpoint support for session resumption. The possible values are: 214 | * 0 - session resumption is not enabled and we're seeing empty session IDs 215 | * 1 - endpoint returns session IDs, but sessions are not resumed 216 | * 2 - session resumption is enabled 217 | * **compressionMethods** - integer value that describes supported compression methods 218 | * bit 0 is set for DEFLATE 219 | * **supportsNpn** - true if the server supports NPN 220 | * **npnProtocols** - space separated list of supported protocols 221 | * **sessionTickets** - indicates support for Session Tickets 222 | * bit 0 (1) - set if session tickets are supported 223 | * bit 1 (2) - set if the implementation is faulty [not implemented] 224 | * bit 2 (4) - set if the server is intolerant to the extension 225 | * **ocspStapling** - true if OCSP stapling is deployed on the server 226 | * **staplingRevocationStatus** - same as Cert.revocationStatus, but for the stapled OCSP response. 227 | * **staplingRevocationErrorMessage** - description of the problem with the stapled OCSP response, if any. 228 | * **sniRequired** - if SNI support is required to access the web site. 229 | * **httpStatusCode** - status code of the final HTTP response seen. When submitting HTTP requests, redirections are followed, but only if they lead to the same hostname. If this field is not available, that means the HTTP request failed. 230 | * **httpForwarding** - available on a server that responded with a redirection to some other hostname. 231 | * **supportsRc4** - true if the server supports at least one RC4 suite. 232 | * **rc4WithModern** - true if RC4 is used with modern clients. 233 | * **rc4Only** - true if only RC4 suites are supported. 234 | * **forwardSecrecy** - indicates support for Forward Secrecy 235 | * bit 0 (1) - set if at least one browser from our simulations negotiated a Forward Secrecy suite. 236 | * bit 1 (2) - set based on Simulator results if FS is achieved with modern clients. For example, the server supports ECDHE suites, but not DHE. 237 | * bit 2 (4) - set if all simulated clients achieve FS. In other words, this requires an ECDHE + DHE combination to be supported. 238 | * **sims** - instance of [SimDetails](#simdetails). 239 | * **heartbleed** - true if the server is vulnerable to the Heartbleed attack. 240 | * **heartbeat** - true if the server supports the Heartbeat extension. 241 | * **openSslCcs** - results of the CVE-2014-0224 test: 242 | * -1 - test failed 243 | * 0 - unknown 244 | * 1 - not vulnerable 245 | * 2 - possibly vulnerable, but not exploitable 246 | * 3 - vulnerable and exploitable 247 | * **poodle** - true if the endpoint is vulnerable to POODLE; false otherwise 248 | * **poodleTls** - results of the POODLE TLS test: 249 | * -3 - timeout 250 | * -2 - TLS not supported 251 | * -1 - test failed 252 | * 0 - unknown 253 | * 1 - not vulnerable 254 | * 2 - vulnerable 255 | * **fallbackScsv** - true if the server supports TLS_FALLBACK_SCSV, false if it doesn't. This field will not be available if the server's support for TLS_FALLBACK_SCSV can't be tested because it supports only one protocol version (e.g., only TLS 1.2). 256 | * **freak** - true of the server is vulnerable to the FREAK attack, meaning it supports 512-bit key exchange. 257 | * **hasSct** - information about the availability of certificate transparency information (embedded SCTs): 258 | * bit 0 (1) - SCT in certificate 259 | * bit 1 (2) - SCT in the stapled OCSP response 260 | * bit 2 (4) - SCT in the TLS extension (ServerHello) 261 | * **dhPrimes[]** - list of hex-encoded DH primes used by the server. Not present if the server doesn't support the DH key exchange. 262 | * **dhUsesKnownPrimes** - whether the server uses known DH primes. Not present if the server doesn't support the DH key exchange. Possible values: 263 | * 0 - no 264 | * 1 - yes, but they're not weak 265 | * 2 - yes and they're weak 266 | * **dhYsReuse** - true if the DH ephemeral server value is reused. Not present if the server doesn't support the DH key exchange. 267 | * **logjam** - true if the server uses DH parameters weaker than 1024 bits. 268 | * **chaCha20Preference** - true if the server takes into account client preferences when deciding if to use ChaCha20 suites. 269 | * **hstsPolicy** - server's HSTS policy. Experimental. 270 | * **hstsPreloads[]** - information about preloaded HSTS policies. 271 | * **hpkpPolicy** - server's HPKP policy. Experimental. 272 | * **hpkpRoPolicy** - server's HPKP RO (Report Only) policy. Experimental. 273 | 274 | ### Info ### 275 | 276 | * **version** - SSL Labs software version as a string (e.g., "1.11.14") 277 | * **criteriaVersion** - rating criteria version as a string (e.g., "2009f") 278 | * **maxAssessments** - the maximum number of concurrent assessments the client is allowed to initiate. 279 | * **currentAssessments** - the number of ongoing assessments submitted by this client. 280 | * **newAssessmentCoolOff** - the cool-off period after each new assessment, in milliseconds; you're not allowed to submit a new assessment before the cool-off expires, otherwise you'll get a 429. 281 | * **messages** - a list of messages (strings). Messages can be public (sent to everyone) and private (sent only to the invoking client). 282 | Private messages are prefixed with "[Private]". 283 | 284 | ### Key ### 285 | 286 | * **size** - key size, e.g., 1024 or 2048 for RSA and DSA, or 256 bits for EC. 287 | * **strength** - key size expressed in RSA bits. 288 | * **alg** - key algorithm; possible values: RSA, DSA, and EC. 289 | * **debianFlaw** - true if we suspect that the key was generated using a weak random number generator (detected via a blacklist database) 290 | * **q** - 0 if key is insecure, null otherwise 291 | 292 | ### Cert ### 293 | 294 | * **subject** - certificate subject 295 | * **commonNames[]** - common names extracted from the subject 296 | * **altNames[]** - alternative names 297 | * **notBefore** - timestamp before which the certificate is not valid 298 | * **notAfter** - timestamp after which the certificate is not valid 299 | * **issuerSubject** - issuer subject 300 | * **sigAlg** - certificate signature algorithm 301 | * **issuerLabel** - issuer name 302 | * **revocationInfo** - a number that represents revocation information present in the certificate: 303 | * bit 0 (1) - CRL information available 304 | * bit 1 (2) - OCSP information available 305 | * **crlURIs[]** - CRL URIs extracted from the certificate 306 | * **ocspURIs[]** - OCSP URIs extracted from the certificate 307 | * **revocationStatus** - a number that describes the revocation status of the certificate: 308 | * 0 - not checked 309 | * 1 - certificate revoked 310 | * 2 - certificate not revoked 311 | * 3 - revocation check error 312 | * 4 - no revocation information 313 | * 5 - internal error 314 | * **crlRevocationStatus** - same as revocationStatus, but only for the CRL information (if any). 315 | * **ocspRevocationStatus** - same as revocationStatus, but only for the OCSP information (if any). 316 | * **sgc** - Server Gated Cryptography support; integer: 317 | * bit 1 (1) - Netscape SGC 318 | * bit 2 (2) - Microsoft SGC 319 | * **validationType** - E for Extended Validation certificates; may be null if unable to determine 320 | * **issues** - list of certificate issues, one bit per issue: 321 | * bit 0 (1) - no chain of trust 322 | * bit 1 (2) - not before 323 | * bit 2 (4) - not after 324 | * bit 3 (8) - hostname mismatch 325 | * bit 4 (16) - revoked 326 | * bit 5 (32) - bad common name 327 | * bit 6 (64) - self-signed 328 | * bit 7 (128) - blacklisted 329 | * bit 8 (256) - insecure signature 330 | * **sct** - true if the certificate contains an embedded SCT; false otherwise. 331 | 332 | ### Chain ### 333 | 334 | * **certs[]** - a list of [ChainCert objects](#chaincert), representing the chain certificates in the order in which they were retrieved from the server 335 | * **issues** - a number of flags that describe the chain and the problems it has: 336 | * bit 0 (1) - unused 337 | * bit 1 (2) - incomplete chain (set only when we were able to build a chain by adding missing intermediate certificates from external sources) 338 | * bit 2 (4) - chain contains unrelated or duplicate certificates (i.e., certificates that are not part of the same chain) 339 | * bit 3 (8) - the certificates form a chain (trusted or not), but the order is incorrect 340 | * bit 4 (16) - contains a self-signed root certificate (not set for self-signed leafs) 341 | * bit 5 (32) - the certificates form a chain (if we added external certificates, bit 1 will be set), but we could not validate it. If the leaf was trusted, that means that we built a different chain we trusted. 342 | 343 | ### ChainCert ### 344 | 345 | * **subject** - certificate subject 346 | * **label** - certificate label (user-friendly name) 347 | * **notBefore** - 348 | * **notAfter** - 349 | * **issuerSubject** - issuer subject 350 | * **issuerLabel** - issuer label (user-friendly name) 351 | * **sigAlg** - 352 | * **issues** - a number of flags the describe the problems with this certificate: 353 | * bit 0 (1) - certificate not yet valid 354 | * bit 1 (2) - certificate expired 355 | * bit 2 (4) - weak key 356 | * bit 3 (8) - weak signature 357 | * bit 4 (16) - blacklisted 358 | * **keyAlg** - key algorithm. 359 | * **keySize** - key size, in bits appopriate for the key algorithm. 360 | * **keyStrength** - key strength, in equivalent RSA bits. 361 | * **revocationStatus** - a number that describes the revocation status of the certificate: 362 | * 0 - not checked 363 | * 1 - certificate revoked 364 | * 2 - certificate not revoked 365 | * 3 - revocation check error 366 | * 4 - no revocation information 367 | * 5 - internal error 368 | * **crlRevocationStatus** - same as revocationStatus, but only for the CRL information (if any). 369 | * **ocspRevocationStatus** - same as revocationStatus, but only for the OCSP information (if any). 370 | * **raw** - PEM-encoded certificate data 371 | 372 | ### Protocol ### 373 | 374 | * **id** - protocol version number, e.g. 0x0303 for TLS 1.2 375 | * **name** - protocol name, i.e. SSL or TLS. 376 | * **version** - protocol version, e.g. 1.2 (for TLS) 377 | * **v2SuitesDisabled** - some servers have SSLv2 protocol enabled, but with all SSLv2 cipher suites disabled. In that case, this field is set to true. 378 | * **q** - 0 if the protocol is insecure, null otherwise 379 | 380 | ### SimClient ### 381 | 382 | * **id** - unique client ID (integer) 383 | * **name** - text. 384 | * **platform** - text. 385 | * **version** - text. 386 | * **isReference** - true if the browser is considered representative of modern browsers, false otherwise. This flag does not correlate to client's capabilities, but is used by SSL Labs to determine if a particular configuration is effective. For example, to track Forward Secrecy support, we mark several representative browsers as "modern" and then test to see if they succeed in negotiating a FS suite. Just as an illustration, modern browsers are currently Chrome, Firefox (not ESR versions), IE/Win7, and Safari. 387 | 388 | ### SimDetails ### 389 | 390 | * **results[]** - instances of [Simulation](#simulation). 391 | 392 | ### Simulation ### 393 | 394 | * **client** - instance of [SimClient](#simclient). 395 | * **errorCode** - zero if handshake was successful, 1 if it was not. 396 | * **attempts** - always 1 with the current implementation. 397 | * **protocolId** - Negotiated protocol ID. 398 | * **suiteId** - Negotiated suite ID. 399 | 400 | ### Suites ### 401 | 402 | * **list[]** - list of [Suite objects](#suite) 403 | * **preference** - true if the server actively selects cipher suites; if null, we were not able to determine if the server has a preference 404 | 405 | ### Suite ### 406 | 407 | * **id** - suite RFC ID (e.g., 5) 408 | * **name** - suite name (e.g., TLS_RSA_WITH_RC4_128_SHA) 409 | * **cipherStrength** - suite strength (e.g., 128) 410 | * **dhStrength** - strength of DH params (e.g., 1024) 411 | * **dhP** - DH params, p component 412 | * **dhG** - DH params, g component 413 | * **dhYs** - DH params, Ys component 414 | * **ecdhBits** - ECDH bits 415 | * **ecdhStrength** - ECDH RSA-equivalent strength 416 | * **q** - 0 if the suite is insecure, null otherwise 417 | 418 | ### HstsPolicy ### 419 | 420 | * **LONG_MAX_AGE** - this constant contains what SSL Labs considers to be sufficiently large max-age value 421 | * **header** - the contents of the HSTS response header, if present 422 | * **status** - HSTS status: 423 | * unknown - either before the server is checked or when its HTTP response headers are not available 424 | * absent - header not present 425 | * present - header present and syntatically correct 426 | * invalid - header present, but couldn't be parsed 427 | * disabled - header present and syntatically correct, but HSTS is disabled 428 | * **error** - error message when error is encountered, null otherwise 429 | * **maxAge** - the max-age value specified in the policy; null if policy is missing or invalid or on parsing error; the maximum value currently supported is 9223372036854775807 430 | * **includeSubDomains** - true if the includeSubDomains directive is set; null otherwise 431 | * **preload** - true if the preload directive is set; null otherwise 432 | * **directives[][]** - list of raw policy directives 433 | 434 | ### HstsPreload ### 435 | 436 | The HstsPreload object contains preload HSTS status of one source for the current hostname. Preload checks are done for the current hostname, not for a domain name. For example, a hostname "www.example.com" tested in SSL Labs would come back as "present" if there is an entry for "example.com" with includeSubDomains enabled or if there is an explicit entry for "www.example.com". 437 | 438 | * **source** - source name 439 | * **status** - preload status: 440 | * error 441 | * unknown - either before the preload status is checked, or if the information is not available for some reason. 442 | * absent 443 | * present 444 | * **error** - error message, when status is "error" 445 | * **sourceTime** - time, as a Unix timestamp, when the preload database was retrieved 446 | 447 | ### HpkpPolicy ### 448 | 449 | * **status** - HPKP status: 450 | * unknown - either before the server is checked or when its HTTP response headers are not available 451 | * absent - header not present 452 | * invalid - header present, but couldn't be parsed 453 | * disabled - header present and syntatically correct, but HPKP is disabled 454 | * incomplete - header present and syntatically correct, incorrectly used 455 | * valid - header present, syntatically correct, and correctly used 456 | * **header** - the contents of the HPKP response header, if present 457 | * **error** - error message, when the policy is invalid 458 | * **maxAge** - the max-age value from the policy 459 | * **includeSubDomains** - true if the includeSubDomains directive is set; null otherwise 460 | * **reportUri** - the report-uri value from the policy 461 | * **pins[]** - list of all pins used by the policy 462 | * **matchedPins[]** - list of pins that match the current configuration 463 | * **directives[][]** - list of raw policy directives 464 | 465 | 466 | ### StatusCodes ### 467 | 468 | * **statusDetails** - a map containing all status details codes and the corresponding English translations. Please note that, once in use, the codes will not change, whereas the translations may change at any time. 469 | 470 | ## Changes ## 471 | 472 | ### 1.14.x (3 March 2015) ### 473 | 474 | * First public release. 475 | 476 | ### 1.15.x (16 March 2015) ### 477 | 478 | * Added ignoreMismatch parameter to control if assessments proceed when server certificate does not match the assessment hostname. 479 | 480 | ### 1.16.x (27 April 2015) ### 481 | 482 | * Changed API versioning to match software version numbers. 483 | * Added EndpointDetails.freak. 484 | * Added several new fields to ChainCert: notBefore, notAfter, sigAlg, keyAlg, keySize, keyStrength. 485 | * Field ChainCert.issues is now set to zero if there are no issues. Previously this field wouldn't exist in the JSON structure. 486 | * Fixed ChainCert.issues didn't flag weak (e.g., SHA1) certificates. 487 | * Added Cert.sct. 488 | * Added EndpointDetails.hasSct. 489 | * Added EndpointDetails.poodle. 490 | * Added EndpointDetails.staplingRevocationStatus and EndpointDetails.staplingRevocationErrorMessage. 491 | * Added Cert.crlRevocationStatus and Cert.ocspRevocationStatus. 492 | * Added ChainCert.revocationStatus, ChainCert.crlRevocationStatus and ChainCert.ocspRevocationStatus. 493 | * Added Endpoint.gradeTrustIgnored. 494 | 495 | ### 1.19.x (1 August 2015) ### 496 | 497 | * New EndpointDetails fields: dhPrimes, dhUsesKnownPrimes, dhYsReuse, and logjam. 498 | * New Info field: newAssessmentCoolOff. There is now a mandatory cool-off period after each new assessment. 499 | 500 | ### 1.21.x (9 December 2015) ### 501 | 502 | * New EndpointDetails fields: rc4Only, chaCha20Preference. 503 | * The maximum value supported by the stsMaxAge field has been increased to 9223372036854775807. 504 | * [Experimental] New API call: getRootCertsRaw. 505 | * [Experimental] HSTS information is now contained within its own structure EndpointDetails.hstsPolicy. The previously-used fields are deprecated but continue to be supported for backward compatibility. 506 | * [Experimental] New fields: HPKP and HPKP-RO information is now exposed in EndpointDetails.hpkpPolicy and EndpointDetails.hpkpRoPolicy. The field pkpResponseHeader is now deprecated, but continues to be supported for backward compatibility. 507 | 508 | -------------------------------------------------------------------------------- /ssllabs-api-docs-v3.md: -------------------------------------------------------------------------------- 1 | # SSL Labs API v3 Documentation v1.35.x (work in progress)# 2 | 3 | **Last update:** 30 May 2019
4 | **Author:** Ivan Ristic 5 | 6 | This document explains the SSL Labs Assessment APIs, which can be used to test SSL servers available on the public Internet. 7 | 8 | ## Protocol Overview ## 9 | 10 | The protocol is based on HTTP and JSON. All invocations of the API should use the GET method and specify the parameters in the query string, as documented below. The results will be returned in the response body as a JSON payload. In essence, the client submits an assessment reque`sts` to the servers. If an acceptable report is already available, it's received straight away. Otherwise, the server will start a new assessment and the client should periodically check to see if the job is complete. 11 | 12 | ### Terms and Conditions ### 13 | 14 | SSL Labs APIs are provided free of charge, subject to our terms and conditions: . The spirit of the license is that the APIs are made available so that system operators can test their own infrastructure. Please read the actual terms and conditions, which are more involved and cover things such as integrating with open source projects, and so on. For example, it's important (for reasons of privacy, compliance, etc) for end users to understand that assessments are carried out by Qualys's servers, not locally. 15 | 16 | Commercial use is generally not allowed, except with an explicit permission from Qualys. That said, we're usually happy to support good causes, even uses by commercial organizations that help improve the security of their customers. If you're a CA, CDN, hosting company, domain name registrar, we're happy for you to use our APIs (but you still have to get in touch with us before you begin). 17 | 18 | ### Protocol Calls ### 19 | 20 | This section documents the available protocol calls. The main API entry point is "https://api.ssllabs.com/api/v3/". If you want to check the API availability from a browser, invoke "https://api.ssllabs.com/api/v3/info". There is also an additional API entry point that can be used to test features that have not yet been deployed to production: "https://api.dev.ssllabs.com/api/v3/". You should expect that this second entry point is not consistently available. Further it offers only reduced assessment limits in comparison with the production version. 21 | 22 | #### Check SSL Labs availability #### 23 | 24 | This call should be used to check the availability of the SSL Labs servers, retrieve the engine and criteria version, and initialize the maximum number of concurrent assessments. Returns one [Info object](#info) on success. 25 | 26 | **API Call:** `info` 27 | 28 | Parameters: 29 | 30 | * None. 31 | 32 | #### Invoke assessment and check progress #### 33 | 34 | This call is used to initiate an assessment, or to retrieve the status of an assessment in progress or in the cache. It will return a single [Host object](#host) on success. The Endpoint object embedded in the Host object will provide partial endpoint results. Please note that assessments of individual endpoints can fail even when the overall assessment is successful (e.g., one server might be down). At this time, you can determine the success of an endpoint assessment by checking the statusMessage field; it should contain "Ready". 35 | 36 | **API Call:** `analyze` 37 | 38 | Parameters: 39 | 40 | * **host** - hostname; required. 41 | * **publish** - set to "on" if assessment results should be published on the public results boards; optional, defaults to "off". 42 | * **startNew** - if set to "on" then cached assessment results are ignored and a new assessment is started. However, if there's already an assessment in progress, its status is delivered instead. This parameter should be used only once to initiate a new assessment; further invocations should omit it to avoid causing an assessment loop. 43 | * **fromCache** - always deliver cached assessment reports if available; optional, defaults to "off". This parameter is intended for API consumers that don't want to wait for assessment results. Can't be used at the same time as the startNew parameter. 44 | * **maxAge** - maximum report age, in hours, if retrieving from cache (fromCache parameter set). 45 | * **all** - by default this call results only summaries of individual endpoints. If this parameter is set to "on", full information will be returned. If set to "done", full information will be returned only if the assessment is complete (status is READY or ERROR). 46 | * **ignoreMismatch** - set to "on" to proceed with assessments even when the server certificate doesn't match the assessment hostname. Set to off by default. Please note that this parameter is ignored if a cached report is returned. 47 | 48 | Examples: 49 | 50 | * `/analyze?host=www.ssllabs.com` 51 | * `/analyze?host=www.ssllabs.com&publish=on` 52 | 53 | #### Retrieve detailed endpoint information #### 54 | 55 | This call is used to retrieve detailed endpoint information. It will return a single [Endpoint object](#endpoint) on success. The object will contain complete assessment information. This API call does not initiate new assessments, even when a cached report is not found. 56 | 57 | **API Call:** `getEndpointData` 58 | 59 | Parameters: 60 | 61 | * **host** - as above 62 | * **s** - endpoint IP address 63 | * **fromCache** - see above. 64 | 65 | Example: 66 | 67 | * `/getEndpointData?host=www.ssllabs.com&s=173.203.82.166` 68 | 69 | #### Retrieve known status codes #### 70 | 71 | This call will return one [StatusCodes instance](#statuscodes). 72 | 73 | **API Call:** `getStatusCodes` 74 | 75 | Parameters: 76 | 77 | * None. 78 | 79 | #### Retrieve root certificates #### 80 | 81 | This call returns the latest root certificates(Mozilla, Apple MacOS, Android, Java and Windows) used for trust validation. 82 | 83 | **API Call:** `getRootCertsRaw` 84 | 85 | Parameters: 86 | 87 | * **trustStore** (1-Mozilla(default), 2-Apple MacOS, 3-Android, 4-Java, 5-Windows) 88 | 89 | Example: 90 | 91 | * `https://api.ssllabs.com/api/v3/getRootCertsRaw?trustStore=1` Or `https://api.ssllabs.com/api/v3/getRootCertsRaw` 92 | * `https://api.ssllabs.com/api/v3/getRootCertsRaw?trustStore=2` 93 | * `https://api.ssllabs.com/api/v3/getRootCertsRaw?trustStore=3` 94 | * `https://api.ssllabs.com/api/v3/getRootCertsRaw?trustStore=4` 95 | * `https://api.ssllabs.com/api/v3/getRootCertsRaw?trustStore=5` 96 | 97 | ### Protocol Usage ### 98 | 99 | When you want to obtain fresh test results for a particular host: 100 | 101 | 1. Invoke `analyze` with the `startNew` parameter to `on`. Set `all` to `done`. 102 | 2. The assessment is now in progress. Call `analyze` periodically (without the `startNew` parameter!) until the assessment is finished. You can tell by observing the `Host.status` field for either READY or ERROR values. 103 | 3. When there are multiple servers behind one hostname, they will be tested one at a time. 104 | 4. During the assessment, interim responses will contain only endpoint status, but not full information. 105 | 5. At the end of the assessment, the response will contain all available information; no further API calls will need to be made for that host. 106 | 107 | When you're happy to receive cached information (e.g., in a browser add-on): 108 | 109 | 1. Invoke `analyze` with `fromCache` set to `on` and `all` set to `done`. 110 | 2. Set `maxAge` to control the maximum age of the cached report. If you don't set this parameter, your IP address will not be forwarded to the tested server. 111 | 3. If the information you requested is available in the cache, it will be returned straight away. 112 | 4. Otherwise, a new assessment will be started. 113 | 5. You can continue to call `analyze` periodically until the assessment is complete. 114 | 115 | ### Error Reporting ### 116 | 117 | When an API call is incorrectly invoked, it will cause an error response to be sent back. The response will include an array of error messages. For example: 118 | 119 | {"errors":[{"field":"host","message":"qp.mandatory"}]} 120 | 121 | The field value references the API parameter name that has an incorrect value. The message value will tell you what the issue is. It is also possible to receive errors without the field parameter set; such messages will usually refer to the request as a whole. 122 | 123 | ### Error Response Status Codes ### 124 | 125 | The following status codes are used: 126 | 127 | * 400 - invocation error (e.g., invalid parameters) 128 | * 429 - client request rate too high or too many new assessments too fast 129 | * 500 - internal error 130 | * 503 - the service is not available (e.g., down for maintenance) 131 | * 529 - the service is overloaded 132 | 133 | A well-written client should never get a 429 response. If you do get one, it means that you're either submitting new assessments at a rate that is too fast, or that you're not correctly tracking how many concurrent requests you're allowed to have. If you get a 503 or 529 status code, you should sleep for several minutes (e.g., 15 and 30 minutes, respectively) then try again. It's best to randomize the delay, especially if you're writing a client tool -- you don't want everyone to retry exactly at the same time. If you get 500, it means that there's a severe problem with the SSL Labs application itself. A sensible approach would be to mark that one assessment as flawed, but to continue on. However, if you continue to receive 500 responses, it's best to give up. 134 | 135 | ### Access Rate and Rate Limiting ### 136 | 137 | Please note the following: 138 | 139 | * Server assessments usually take at least 60 seconds. (They are intentionally slow, to avoid harming servers.) Thus, there is no need to poll for the results very often. In fact, polling too often slows down the service for everyone. It's best to use variable polling: 5 seconds until an assessment gets under way (status changes to IN_PROGRESS), then 10 seconds until it completes. 140 | * Keep down the number of concurrent assessments to a minimum. If you're not in a hurry, test only one hostname at a time. 141 | 142 | We may limit your usage of the API, by enforcing a limit on concurrent assessments, and the overall number of assessments performed in a time period. If that happens, we will respond with 429 (Too Many Requests) to API calls that wish to initiate new assessments. Your ability to follow previously initiated assessments, or retrieve assessment results from the cache, will not be impacted. If you receive a 429 response, reduce the number of concurrent assessments and check that you're not submitting new assessments at a rate higher than allowed. 143 | 144 | If the server is overloaded (a condition that is not a result of the client's behaviour), the 529 status code will be used instead. This is not a situation we wish to be in. If you encounter it, take a break and come back later. 145 | 146 | All successful API calls contain response headers `X-Max-Assessments` and `X-Current-Assessments`. They can be used to calculate how many new 147 | assessments can be submitted. It is recommended that clients update their internal state after each complete response. 148 | 149 | ### Protocol Evolution ### 150 | 151 | The API is versioned. New versions of the API will be introduced whenever incompatible changes need to be made to the protocol. When a new version becomes available, existing applications can continue to use the previous version for as long as it is supported. 152 | 153 | To reduce version number inflation, new fields may be added to the results without a change in protocol version number. 154 | 155 | ## Response Objects ## 156 | 157 | The remainder of the document explains the structure of the returned objects. The following conventions are used: 158 | 159 | * **field** - a simple field 160 | * **object{}** - an object 161 | * **array[]** - an array 162 | 163 | 164 | ### Info ### 165 | 166 | * **engineVersion** - SSL Labs software version as a string (e.g., "1.11.14") 167 | * **criteriaVersion** - rating criteria version as a string (e.g., "2009f") 168 | * **maxAssessments** - the maximum number of concurrent assessments the client is allowed to initiate. 169 | * **currentAssessments** - the number of ongoing assessments submitted by this client. 170 | * **newAssessmentCoolOff** - the cool-off period after each new assessment, in milliseconds; you're not allowed to submit a new assessment before the cool-off expires, otherwise you'll get a 429. 171 | * **messages** - a list of messages (strings). Messages can be public (sent to everyone) and private (sent only to the invoking client). 172 | Private messages are prefixed with "[Private]". 173 | 174 | ### Host ### 175 | 176 | * **host** - assessment host, which can be a hostname or an IP address 177 | * **port** - assessment port (e.g., 443) 178 | * **protocol** - protocol (e.g., HTTP) 179 | * **isPublic** - true if this assessment is publicly available (listed on the SSL Labs assessment boards) 180 | * **status** - assessment status; possible values: DNS, ERROR, IN_PROGRESS, and READY. 181 | * **statusMessage** - status message in English. When status is ERROR, this field will contain an error message. 182 | * **startTime** - assessment starting time, in milliseconds since 1970 183 | * **testTime** - assessment completion time, in milliseconds since 1970 184 | * **engineVersion** - assessment engine version (e.g., "1.26.5") 185 | * **criteriaVersion** - grading criteria version (e.g., "2009l") 186 | * **cacheExpiryTime** - when will the assessment results expire from the cache (typically set only for assessment with errors; otherwise the results stay in the cache for as long as there's sufficient room) 187 | * **certHostnames[]** - the list of certificate hostnames collected from the certificates seen during assessment. The hostnames may not be valid. This field is available only if the server certificate doesn't match the requested hostname. In that case, this field saves you some time as you don't have to inspect the certificates yourself to find out what valid hostnames might be. 188 | * **endpoints[]** - list of [Endpoint objects](#endpoint) 189 | * **certs[]** - a list of [Cert object](#cert), representing the chain certificates in the order in which they were retrieved from the server. 190 | 191 | ### Endpoint ### 192 | 193 | * **ipAddress** - endpoint IP address, in IPv4 or IPv6 format. 194 | * **serverName** - server name retrieved via reverse DNS 195 | * **statusMessage** - assessment status message; this field will contain "Ready" if the endpoint assessment was successful. 196 | * **statusDetails** - code of the operation currently in progress 197 | * **statusDetailsMessage** - description of the operation currently in progress 198 | * **grade** - possible values: A+, A-, A-F, T (no trust) and M (certificate name mismatch) 199 | * **gradeTrustIgnored** - grade (as above), if trust issues are ignored 200 | * **futureGrade** - next grade because of upcoming grading criteria changes, Null if there is no impact on current grade. 201 | * **hasWarnings** - if this endpoint has warnings that might affect the score (e.g., get A- instead of A). 202 | * **isExceptional** - this flag will be raised when an exceptional configuration is encountered. The SSL Labs test will give such sites an A+. 203 | * **progress** - assessment progress, which is a value from 0 to 100, and -1 if the assessment has not yet started 204 | * **duration** - assessment duration, in milliseconds 205 | * **eta** - estimated time, in seconds, until the completion of the assessment 206 | * **delegation** - indicates domain name delegation with and without the www prefix 207 | * bit 0 (1) - set for non-prefixed access 208 | * bit 1 (2) - set for prefixed access 209 | * **details** - this field contains an [EndpointDetails object](#endpointdetails). It's not present by default, but can be enabled by using the "all" parameter to the `analyze` API call. 210 | 211 | ### EndpointDetails ### 212 | 213 | * **hostStartTime** = endpoint assessment starting time, in milliseconds since 1970. This field is useful when test results are retrieved in several HTTP invocations. Then, you should check that the hostStartTime value matches the startTime value of the host. 214 | * **certChains[]** - Server [Certificate chains](#certificatechain) 215 | * **protocols[]** - supported [protocols](#protocol) 216 | * **suites[]** - supported [cipher suites](#protocolsuites) per protocol 217 | * **noSniSuites** - [cipher suites](#protocolsuites) observed only with client that does not support Server Name Indication (SNI). 218 | * **namedGroups** - instance of [NamedGroups](#namedgroups) object. 219 | * **serverSignature** - Contents of the HTTP Server response header when known. This field could be absent for one of two reasons: 1) the HTTP request failed (check httpStatusCode) or 2) there was no Server response header returned. 220 | * **prefixDelegation** - true if this endpoint is reachable via a hostname with the www prefix 221 | * **nonPrefixDelegation** (moved here from the summary) - true if this endpoint is reachable via a hostname without the www prefix 222 | * **vulnBeast** - true if the endpoint is vulnerable to the BEAST attack 223 | * **renegSupport** - this is an integer value that describes the endpoint support for renegotiation: 224 | * bit 0 (1) - set if insecure client-initiated renegotiation is supported 225 | * bit 1 (2) - set if secure renegotiation is supported 226 | * bit 2 (4) - set if secure client-initiated renegotiation is supported 227 | * bit 3 (8) - set if the server requires secure renegotiation support 228 | * **sessionResumption** - this is an integer value that describes endpoint support for session resumption. The possible values are: 229 | * 0 - session resumption is not enabled and we're seeing empty session IDs 230 | * 1 - endpoint returns session IDs, but sessions are not resumed 231 | * 2 - session resumption is enabled 232 | * **compressionMethods** - integer value that describes supported compression methods 233 | * bit 0 (1) is set for DEFLATE 234 | * **supportsNpn** - true if the server supports NPN 235 | * **npnProtocols** - space separated list of supported NPN protocols 236 | * **supportsAlpn** - true if the server supports ALPN 237 | * **alpnProtocols** - space separated list of supported ALPN protocols 238 | * **sessionTickets** - indicates support for Session Tickets 239 | * bit 0 (1) - set if session tickets are supported 240 | * bit 1 (2) - set if the implementation is faulty [not implemented] 241 | * bit 2 (4) - set if the server is intolerant to the extension 242 | * **ocspStapling** - true if OCSP stapling is deployed on the server 243 | * **staplingRevocationStatus** - same as Cert.revocationStatus, but for the stapled OCSP response. 244 | * **staplingRevocationErrorMessage** - description of the problem with the stapled OCSP response, if any. 245 | * **sniRequired** - if SNI support is required to access the web site. 246 | * **httpStatusCode** - status code of the final HTTP response seen. When submitting HTTP requests, redirections are followed, but only if they lead to the same hostname. If this field is not available, that means the HTTP request failed. 247 | * **httpForwarding** - available on a server that responded with a redirection to some other hostname. 248 | * **supportsRc4** - true if the server supports at least one RC4 suite. 249 | * **rc4WithModern** - true if RC4 is used with modern clients. 250 | * **rc4Only** - true if only RC4 suites are supported. 251 | * **forwardSecrecy** - indicates support for Forward Secrecy 252 | * bit 0 (1) - set if at least one browser from our simulations negotiated a Forward Secrecy suite. 253 | * bit 1 (2) - set based on Simulator results if FS is achieved with modern clients. For example, the server supports ECDHE suites, but not DHE. 254 | * bit 2 (4) - set if all simulated clients achieve FS. In other words, this requires an ECDHE + DHE combination to be supported. 255 | * **supportsAead** - true if the server supports at least one AEAD suite. 256 | * **supportsCBC** - true if the server supports at least one CBC suite. 257 | * **protocolIntolerance** - indicates protocol version intolerance issues: 258 | * bit 0 (1) - TLS 1.0 259 | * bit 1 (2) - TLS 1.1 260 | * bit 2 (4) - TLS 1.2 261 | * bit 3 (8) - TLS 1.3 262 | * bit 4 (16) - TLS 1.152 263 | * bit 5 (32) - TLS 2.152 264 | * **miscIntolerance** - indicates various other types of intolerance: 265 | * bit 0 (1) - extension intolerance 266 | * bit 1 (2) - long handshake intolerance 267 | * bit 2 (4) - long handshake intolerance workaround success 268 | * **sims** - instance of [SimDetails](#simdetails). 269 | * **heartbleed** - true if the server is vulnerable to the Heartbleed attack. 270 | * **heartbeat** - true if the server supports the Heartbeat extension. 271 | * **openSslCcs** - results of the CVE-2014-0224 test: 272 | * -1 - test failed 273 | * 0 - unknown 274 | * 1 - not vulnerable 275 | * 2 - possibly vulnerable, but not exploitable 276 | * 3 - vulnerable and exploitable 277 | * **openSSLLuckyMinus20** - results of the CVE-2016-2107 test: 278 | * -1 - test failed 279 | * 0 - unknown 280 | * 1 - not vulnerable 281 | * 2 - vulnerable and insecure 282 | * **ticketbleed** - results of the ticketbleed CVE-2016-9244 test: 283 | * -1 - test failed 284 | * 0 - unknown 285 | * 1 - not vulnerable 286 | * 2 - vulnerable and insecure 287 | * 3 - not vulnerable but a similar bug detected [\(additional details\)](https://community.qualys.com/thread/17180-is-ticketbleed-cve-2016-9244-possible-in-a-non-f5-environment#comment-36958) 288 | * **bleichenbacher** - results of the Return Of Bleichenbacher's Oracle Threat (ROBOT) test: 289 | * -1 - test failed 290 | * 0 - unknown 291 | * 1 - not vulnerable 292 | * 2 - vulnerable (weak oracle) 293 | * 3 - vulnerable (strong oracle) 294 | * 4 - inconsistent results 295 | * **zombiePoodle** - results of the Zombie POODLE test: 296 | * -1 - test failed 297 | * 0 - unknown 298 | * 1 - not vulnerable 299 | * 2 - vulnerable 300 | * 3 - vulnerable and exploitable 301 | * **goldenDoodle** - results of the GOLDENDOODLE test: 302 | * -1 - test failed 303 | * 0 - unknown 304 | * 1 - not vulnerable 305 | * 4 - vulnerable 306 | * 5 - vulnerable and exploitable 307 | * **zeroLengthPaddingOracle** - results of the 0-Length Padding Oracle (CVE-2019-1559) test: 308 | * -1 - test failed 309 | * 0 - unknown 310 | * 1 - not vulnerable 311 | * 6 - vulnerable 312 | * 7 - vulnerable and exploitable 313 | * **sleepingPoodle** - results of the Sleeping POODLE test: 314 | * -1 - test failed 315 | * 0 - unknown 316 | * 1 - not vulnerable 317 | * 10 - vulnerable 318 | * 11 - vulnerable and exploitable 319 | * **poodle** - true if the endpoint is vulnerable to POODLE; false otherwise 320 | * **poodleTls** - results of the POODLE TLS test: 321 | * -3 - timeout 322 | * -2 - TLS not supported 323 | * -1 - test failed 324 | * 0 - unknown 325 | * 1 - not vulnerable 326 | * 2 - vulnerable 327 | * **fallbackScsv** - true if the server supports TLS_FALLBACK_SCSV, false if it doesn't. This field will not be available if the server's support for TLS_FALLBACK_SCSV can't be tested because it supports only one protocol version (e.g., only TLS 1.2). 328 | * **freak** - true if the server is vulnerable to the FREAK attack, meaning it supports 512-bit key exchange. 329 | * **hasSct** - information about the availability of certificate transparency information (embedded SCTs): 330 | * bit 0 (1) - SCT in certificate 331 | * bit 1 (2) - SCT in the stapled OCSP response 332 | * bit 2 (4) - SCT in the TLS extension (ServerHello) 333 | * **dhPrimes[]** - list of hex-encoded DH primes used by the server. Not present if the server doesn't support the DH key exchange. 334 | * **dhUsesKnownPrimes** - whether the server uses known DH primes. Not present if the server doesn't support the DH key exchange. Possible values: 335 | * 0 - no 336 | * 1 - yes, but they're not weak 337 | * 2 - yes and they're weak 338 | * **dhYsReuse** - true if the DH ephemeral server value is reused. Not present if the server doesn't support the DH key exchange. 339 | * **ecdhParameterReuse** - true if the server reuses its ECDHE values 340 | * **logjam** - true if the server uses DH parameters weaker than 1024 bits. 341 | * **chaCha20Preference** - true if the server takes into account client preferences when deciding if to use ChaCha20 suites. Will be deprecated in new version. 342 | * **hstsPolicy{}** - server's [HSTS policy](#hstspolicy). Experimental. 343 | * **hstsPreloads[]** - information about [preloaded HSTS policies](#hstspreload). 344 | * **hpkpPolicy{}** - server's [HPKP policy](#hpkppolicy). 345 | * **hpkpRoPolicy{}** - server's [HPKP-RO policy](#hpkppolicy). 346 | * **staticPkpPolicy{}** - server's [SPKP policy](#staticpkppolicy). 347 | * **httpTransactions[]** - an array of [HttpTransaction](#httptransaction) objects. 348 | * **drownHosts[]** - list of [DROWN hosts](#drownhosts). 349 | * **drownErrors** - true if error occurred in the DROWN test. 350 | * **drownVulnerable** - true if server vulnerable to the DROWN attack. 351 | * **implementsTLS13MandatoryCS** - true if server supports mandatory TLS 1.3 cipher suite (TLS_AES_128_GCM_SHA256), null if TLS 1.3 not supported. 352 | * **zeroRTTEnabled** - results of the 0-RTT test. This test will only be performed if TLS 1.3 is enabled: 353 | * -2 - test failed 354 | * -1 - test not performed (default) 355 | * 0 - 0-RTT is not enabled 356 | * 1 - 0-RTT is enabled 357 | 358 | ### CertificateChain ### 359 | 360 | * **id** - Certificate chain ID 361 | * **certIds[]** - list of IDs of each [certificate](#cert), representing the chain certificates in the order in which they were retrieved from the server 362 | * **trustPaths[]** - [trust path object](#trustpath) 363 | * **issues** - a number of flags that describe the chain and the problems it has: 364 | * bit 0 (1) - unused 365 | * bit 1 (2) - incomplete chain (set only when we were able to build a chain by adding missing intermediate certificates from external sources) 366 | * bit 2 (4) - chain contains unrelated or duplicate certificates (i.e., certificates that are not part of the same chain) 367 | * bit 3 (8) - the certificates form a chain (trusted or not), but the order is incorrect 368 | * bit 4 (16) - contains a self-signed root certificate (not set for self-signed leafs) 369 | * bit 5 (32) - the certificates form a chain (if we added external certificates, bit 1 will be set), but we could not validate it. If the leaf was trusted, that means that we built a different chain we trusted. 370 | 371 | * **noSni** - true for certificate obtained only with No Server Name Indication (SNI). 372 | 373 | ### trustPath ### 374 | 375 | * **certIds[]** - list of certificate ID from leaf to root. 376 | * **trust[]** - [trust object](#trust). This object shows info about the trusted certificate by using Mozilla trust store. 377 | * **isPinned** - true if a key is pinned, else false 378 | * **mactchedPins** - number of matched pins with HPKP policy 379 | * **unmatchedPins** - number of unmatched pins with HPKP policy 380 | 381 | ### trust ### 382 | 383 | * **rootStore** - this field shows the Trust store being used (eg. "Mozilla") 384 | * **isTrusted** - true if trusted against above rootStore 385 | * **trustErrorMessage** - shows the error message if any, Null otherwise. 386 | 387 | ### Protocol ### 388 | 389 | * **id** - protocol version, e.g. 771 for TLS 1.2 (0x0303) 390 | * **name** - protocol name SSL/TLS. 391 | * **version** - protocol version, e.g. 1.2, 1.1 etc 392 | * **v2SuitesDisabled** - some servers have SSLv2 protocol enabled, but with all SSLv2 cipher suites disabled. In that case, this field is set to true. 393 | * **q** - 0 if the protocol is insecure, null otherwise 394 | 395 | ### ProtocolSuites ### 396 | 397 | * **protocol** - protocol version. 398 | * **list[]** - list of [Suite objects](#suite) 399 | * **preference** - true if the server actively selects cipher suites; if null, we were not able to determine if the server has a preference 400 | * **chaCha20Preference** - true if the server takes into account client preferences when deciding if to use ChaCha20 suites. null, we were not able to determine if the server has a chacha preference. 401 | 402 | ### Suite ### 403 | 404 | * **id** - suite RFC ID (e.g., 5) 405 | * **name** - suite name (e.g., TLS_RSA_WITH_RC4_128_SHA) 406 | * **cipherStrength** - suite strength (e.g., 128) 407 | * **kxType** - key exchange type (e.g., ECDH) 408 | * **kxStrength** - key exchange strength, in RSA-equivalent bits 409 | * **dhP** - DH params, p component 410 | * **dhG** - DH params, g component 411 | * **dhYs** - DH params, Ys component 412 | * **namedGroupBits** - EC bits 413 | * **namedGroupId** - EC curve ID 414 | * **namedGroupName** - EC curve name 415 | * **q** - flag for suite insecure or weak. Not present if suite is strong or good 416 | * 0 - insecure 417 | * 1 - weak 418 | 419 | 420 | ### NamedGroups ### 421 | 422 | * **list** - an array of [NamedGroup](#namedgroup) objects 423 | * **preference** - true if the server has preferred curves that it uses first 424 | 425 | 426 | 427 | ### NamedGroup ### 428 | 429 | * **Id** - named curve ID 430 | * **Name** - named curve name 431 | * **bits** - named curve strength in EC bits 432 | 433 | 434 | ### SimDetails ### 435 | 436 | * **results[]** - instances of [Simulation](#simulation). 437 | 438 | ### Simulation ### 439 | 440 | * **client** - instance of [SimClient](#simclient). 441 | * **errorCode** - zero if handshake was successful, 1 if it was not. 442 | * **errorMessage** - error message if simulation has failed. 443 | * **attempts** - always 1 with the current implementation. 444 | * **certChainId** - id of the certificate chain. 445 | * **protocolId** - negotiated protocol ID. 446 | * **suiteId** - negotiated suite ID. 447 | * **suiteName** - negotiated suite Name. 448 | * **kxType** - negotiated key exchange, for example "ECDH". 449 | * **kxStrength** - negotiated key exchange strength, in RSA-equivalent bits. 450 | * **dhBits** - strength of DH params (e.g., 1024) 451 | * **dhP** - DH params, p component 452 | * **dhG** - DH params, g component 453 | * **dhYs** - DH params, Ys component 454 | * **namedGroupBits** - when ECDHE is negotiated, length of EC parameters. 455 | * **namedGroupId** - when ECDHE is negotiated, EC curve ID. 456 | * **namedGroupName** - when ECDHE is negotiated, EC curve nanme (e.g., "secp256r1"). 457 | * **keyAlg** - connection certificate key algorithsm (e.g., "RSA"). 458 | * **keySize** - connection certificate key size (e.g., 2048). 459 | * **sigAlg** - connection certificate signature algorithm (e.g, "SHA256withRSA"). 460 | 461 | ### SimClient ### 462 | 463 | * **id** - unique client ID. 464 | * **name** - name of the client (e.g., Chrome). 465 | * **platform** - name of the platform (e.g., XP SP3). 466 | * **version** - version of the software being simulated (e.g., 49) 467 | * **isReference** - true if the browser is considered representative of modern browsers, false otherwise. This flag does not correlate to client's capabilities, but is used by SSL Labs to determine if a particular configuration is effective. For example, to track Forward Secrecy support, we mark several representative browsers as "modern" and then test to see if they succeed in negotiating a FS suite. Just as an illustration, modern browsers are currently Chrome, Firefox (not ESR versions), IE/Win7, and Safari. 468 | 469 | ### HstsPolicy ### 470 | 471 | * **LONG_MAX_AGE** - this constant contains what SSL Labs considers to be sufficiently large max-age value 472 | * **header** - the contents of the HSTS response header, if present 473 | * **status** - HSTS status: 474 | * unknown - either before the server is checked or when its HTTP response headers are not available 475 | * absent - header not present 476 | * present - header present and syntatically correct 477 | * invalid - header present, but couldn't be parsed 478 | * disabled - header present and syntatically correct, but HSTS is disabled 479 | * **error** - error message when error is encountered, null otherwise 480 | * **maxAge** - the max-age value specified in the policy; null if policy is missing or invalid or on parsing error; the maximum value currently supported is 9223372036854775807 481 | * **includeSubDomains** - true if the includeSubDomains directive is set; null otherwise 482 | * **preload** - true if the preload directive is set; null otherwise 483 | * **directives[][]** - list of raw policy directives 484 | 485 | ### HstsPreload ### 486 | 487 | The HstsPreload object contains preload HSTS status of one source for the current hostname. Preload checks are done for the current hostname, not for a domain name. For example, a hostname "www.example.com" tested in SSL Labs would come back as "present" if there is an entry for "example.com" with includeSubDomains enabled or if there is an explicit entry for "www.example.com". 488 | 489 | * **source** - source name 490 | * **hostname** - name of the host 491 | * **status** - preload status: 492 | * error 493 | * unknown - either before the preload status is checked, or if the information is not available for some reason. 494 | * absent 495 | * present 496 | * **error** - error message, when status is "error" 497 | * **sourceTime** - time, as a Unix timestamp, when the preload database was retrieved 498 | 499 | ### HpkpPolicy ### 500 | 501 | * **header** - the contents of the HPKP response header, if present 502 | * **status** - HPKP status: 503 | * unknown - either before the server is checked or when its HTTP response headers are not available 504 | * absent - header not present 505 | * invalid - header present, but couldn't be parsed 506 | * disabled - header present and syntatically correct, but HPKP is disabled 507 | * incomplete - header present and syntatically correct, incorrectly used 508 | * partial - header present and synatatically correct, but not all paths pinned 509 | * valid - header present, syntatically correct, and correctly used 510 | * **error** - error message, when the policy is invalid 511 | * **maxAge** - the max-age value from the policy 512 | * **includeSubDomains** - true if the includeSubDomains directive is set; null otherwise 513 | * **reportUri** - the report-uri value from the policy 514 | * **pins[]** - list of all pins used by the policy 515 | * **matchedPins[]** - list of pins that match the current configuration; each list entry contains an object with two fields, `hashFunction` and `value` (hex-encoded) 516 | * **directives[][]** - list of raw policy directives (name-value pairs) 517 | 518 | ### staticPkpPolicy ### 519 | 520 | * **status** - SPKP status: 521 | * unknown - either before the server is checked or when its preload list not available 522 | * absent - static pinning not present 523 | * invalid - static pinning present, but couldn't be parsed 524 | * incomplete - static pinning present but doesn't match configuration 525 | * partial - static pinning present but not all trust paths pinned 526 | * forbidden - static pinning present, forbidden pinns matched 527 | * valid - static pinning present, syntatically correct, and correctly used 528 | * **error** - error message, when the policy is invalid 529 | * **includeSubDomains** - true if the includeSubDomains directive is set else false 530 | * **reportUri** - the report-uri value from the policy 531 | * **pins[]** - list of all pins used by the policy 532 | * **matchedPins[]** - list of pins that match the current configuration; each list entry contains an object with two fields, `hashFunction` and `value` (hex-encoded) 533 | * **forbiddenPins[]** - list of all forbidden pins used by policy; 534 | * **matchedForbiddenPins[]** - list of forbidden pins that match the current configuration; each list entry contains an object with two fields, `hashFunction` and `value` (hex-encoded) 535 | 536 | ### HttpTransaction ### 537 | 538 | * **requestUrl** - request URL 539 | * **statusCode** - response status code 540 | * **requestLine** - the entire request line as a single field 541 | * **requestHeaders[]** - an array of request HTTP headers, each with name and value 542 | * **responseLine** - the entire response line as a single field 543 | * **responseHeadersRaw[]** - all response headers as a single field (useful if the headers are malformed) 544 | * **responseHeaders[]** - an array of response HTTP headers, each with name and value 545 | * **fragileServer** - true if the server crashes when inspected by SSL Labs (in which case the full test is refused) 546 | 547 | ### DrownHosts ### 548 | 549 | * **ip** - Ip address of server that shares same RSA-Key/hostname in its certificate 550 | * **export** - true if export cipher suites detected 551 | * **port** - port number of the server 552 | * **special** - true if vulnerable OpenSSL version detected 553 | * **sslv2** - true if SSL v2 is supported 554 | * **status** - drown host status: 555 | * error - error occurred in test 556 | * unknown - before the status is checked 557 | * not_checked - not checked if already vulnerable server found 558 | * not_checked_same_host - Not checked (same host) 559 | * handshake_failure - when SSL v2 not supported by server 560 | * sslv2 - SSL v2 supported but not same rsa key 561 | * key_match - vulnerable (same key with SSL v2) 562 | * hostname_match - vulnerable (same hostname with SSL v2) 563 | 564 | 565 | ### Cert ### 566 | 567 | * **id** - certificate ID 568 | * **subject** - certificate subject 569 | * **serialNumber** - certificate serial number (hex-encoded) 570 | * **commonNames[]** - common names extracted from the subject 571 | * **altNames[]** - alternative names 572 | * **notBefore** - timestamp before which the certificate is not valid (Unix Timestamp) 573 | * **notAfter** - timestamp after which the certificate is not valid (Unix Timestamp) 574 | * **issuerSubject** - issuer subject 575 | * **sigAlg** - certificate signature algorithm 576 | * **revocationInfo** - a number that represents revocation information present in the certificate: 577 | * bit 0 (1) - CRL information available 578 | * bit 1 (2) - OCSP information available 579 | * **crlURIs[]** - CRL URIs extracted from the certificate 580 | * **ocspURIs[]** - OCSP URIs extracted from the certificate 581 | * **revocationStatus** - a number that describes the revocation status of the certificate: 582 | * 0 - not checked 583 | * 1 - certificate revoked 584 | * 2 - certificate not revoked 585 | * 3 - revocation check error 586 | * 4 - no revocation information 587 | * 5 - internal error 588 | * **crlRevocationStatus** - same as revocationStatus, but only for the CRL information (if any). 589 | * **ocspRevocationStatus** - same as revocationStatus, but only for the OCSP information (if any). 590 | * **dnsCaa** - true if CAA is supported else false. 591 | * **caaPolicy** - [CAA Policy](#caapolicy), Null if CAA is not supported 592 | * **mustStaple** - true if stapling is supported else false 593 | * **sgc** - Server Gated Cryptography support; integer: 594 | * bit 1 (1) - Netscape SGC 595 | * bit 2 (2) - Microsoft SGC 596 | * **validationType** - E for Extended Validation certificates; may be null if unable to determine 597 | * **issues** - list of certificate issues, one bit per issue: 598 | * bit 0 (1) - no chain of trust 599 | * bit 1 (2) - not before 600 | * bit 2 (4) - not after 601 | * bit 3 (8) - hostname mismatch 602 | * bit 4 (16) - revoked 603 | * bit 5 (32) - bad common name 604 | * bit 6 (64) - self-signed 605 | * bit 7 (128) - blacklisted 606 | * bit 8 (256) - insecure signature 607 | * bit 9 (512) - insecure key 608 | * **sct** - true if the certificate contains an embedded SCT; false otherwise. 609 | * **sha1Hash** - sha1 hash of the certificate 610 | * **sha256Hash** - sha256 hash of the certificate 611 | * **pinSha256** - sha256 hash of the public key 612 | * **keyAlg** - key algorithm. 613 | * **keySize** - key size, in bits appropriate for the key algorithm. 614 | * **keyStrength** - key strength, in equivalent RSA bits 615 | * **keyKnownDebianInsecure** - true if debian flaw is found, else false 616 | * **raw** - PEM-encoded certificate 617 | 618 | ### CaaPolicy #### 619 | 620 | * **policyHostname** - hostname where policy is located 621 | * **caaRecords[]** - list of Supported [CAARecord](#caarecord) 622 | 623 | ### CaaRecord #### 624 | 625 | * **tag** - a property of the CAA record 626 | * **value** - corresponding value of a CAA property 627 | * **flags** - corresponding flags of CAA property (8 bit) 628 | 629 | ### StatusCodes ### 630 | 631 | * **statusDetails** - a map containing all status details codes and the corresponding English translations. Please note that, once in use, the codes will not change, whereas the translations may change at any time. 632 | -------------------------------------------------------------------------------- /ssllabs-api-docs.md: -------------------------------------------------------------------------------- 1 | The documentation for the SSL Labs API v4: 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ssllabs-scan-v4-register.go: -------------------------------------------------------------------------------- 1 | //go:build go1.3 2 | 3 | /* 4 | * Licensed to Qualys, Inc. (QUALYS) under one or more 5 | * contributor license agreements. See the NOTICE file distributed with 6 | * this work for additional information regarding copyright ownership. 7 | * QUALYS licenses this file to You under the Apache License, Version 2.0 8 | * (the "License"); you may not use this file except in compliance with 9 | * the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // work in progress 21 | package main 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "flag" 27 | "fmt" 28 | "io" 29 | "net/http" 30 | ) 31 | 32 | type RegisterRequest struct { 33 | FirstName string `json:"firstName"` 34 | LastName string `json:"lastName"` 35 | Email string `json:"email"` 36 | Organization string `json:"organization"` 37 | } 38 | 39 | type ErrorResponse struct { 40 | Errors []struct { 41 | Field string `json:"field"` 42 | Message string `json:"message"` 43 | } `json:"errors"` 44 | } 45 | 46 | func main() { 47 | // Define command-line flags 48 | firstName := flag.String("firstName", "", "First name") 49 | lastName := flag.String("lastName", "", "Last name") 50 | email := flag.String("email", "", "Email") 51 | organization := flag.String("organization", "", "Organization") 52 | registerApiUrl := flag.String("registerApiUrl", "https://api.ssllabs.com/api/v4/register", "API endpoint URL") 53 | flag.Parse() 54 | 55 | // Validate required flags 56 | if *firstName == "" || *lastName == "" || *email == "" || *organization == "" { 57 | fmt.Println("All flags (firstName, lastName, email, organization) are required.") 58 | return 59 | } 60 | 61 | // Create RegisterRequest instance 62 | requestData := RegisterRequest{ 63 | FirstName: *firstName, 64 | LastName: *lastName, 65 | Email: *email, 66 | Organization: *organization, 67 | } 68 | 69 | // Convert data to JSON 70 | jsonData, err := json.Marshal(requestData) 71 | if err != nil { 72 | fmt.Println("Error encoding JSON:", err) 73 | return 74 | } 75 | 76 | // Make the HTTP POST request 77 | resp, err := http.Post(*registerApiUrl, "application/json", bytes.NewBuffer(jsonData)) 78 | if err != nil { 79 | fmt.Println("Error making HTTP request:", err) 80 | return 81 | } 82 | defer resp.Body.Close() 83 | body, err := io.ReadAll(resp.Body) 84 | 85 | // Check the response status code 86 | if resp.StatusCode != http.StatusOK { 87 | // Handle error response 88 | var errorResponse ErrorResponse 89 | err = json.Unmarshal(body, &errorResponse) 90 | if err != nil { 91 | fmt.Println("Error decoding error response:", err) 92 | return 93 | } 94 | 95 | fmt.Println("API Error Response:") 96 | for _, e := range errorResponse.Errors { 97 | fmt.Printf("Email: %s, Field: %s, Message: %s\n", requestData.Email, e.Field, e.Message) 98 | } 99 | return 100 | } 101 | 102 | // Print the response body 103 | var responseMap map[string]interface{} 104 | err = json.Unmarshal(body, &responseMap) 105 | if err != nil { 106 | fmt.Println("Error decoding response:", err) 107 | return 108 | } 109 | 110 | fmt.Printf("API Response: Status - %s, Message - %s\n", responseMap["status"], responseMap["message"]) 111 | } 112 | -------------------------------------------------------------------------------- /ssllabs-scan-v4.go: -------------------------------------------------------------------------------- 1 | //go:build go1.3 2 | 3 | /* 4 | * Licensed to Qualys, Inc. (QUALYS) under one or more 5 | * contributor license agreements. See the NOTICE file distributed with 6 | * this work for additional information regarding copyright ownership. 7 | * QUALYS licenses this file to You under the Apache License, Version 2.0 8 | * (the "License"); you may not use this file except in compliance with 9 | * the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | // work in progress 21 | package main 22 | 23 | import ( 24 | "crypto/tls" 25 | "io" 26 | ) 27 | import "errors" 28 | import "encoding/json" 29 | import "flag" 30 | import "fmt" 31 | import "bufio" 32 | import "os" 33 | import "log" 34 | import "math/rand" 35 | import "net" 36 | import "net/http" 37 | import "net/url" 38 | import "strconv" 39 | import "strings" 40 | import "sync/atomic" 41 | import "time" 42 | import "sort" 43 | 44 | const ( 45 | LOG_NONE = -1 46 | LOG_EMERG = 0 47 | LOG_ALERT = 1 48 | LOG_CRITICAL = 2 49 | LOG_ERROR = 3 50 | LOG_WARNING = 4 51 | LOG_NOTICE = 5 52 | LOG_INFO = 6 53 | LOG_DEBUG = 7 54 | LOG_TRACE = 8 55 | ) 56 | 57 | var USER_AGENT = "ssllabs-scan v1.5.0 (dev $Id$)" 58 | 59 | var logLevel = LOG_NOTICE 60 | 61 | // How many assessment do we have in progress? 62 | var activeAssessments = 0 63 | 64 | // How many assessments does the server think we have in progress? 65 | var currentAssessments = -1 66 | 67 | // The maximum number of assessments we can have in progress at any one time. 68 | var maxAssessments = -1 69 | 70 | var requestCounter uint64 = 0 71 | 72 | var apiLocation = "https://api.ssllabs.com/api/v4" 73 | 74 | var globalNewAssessmentCoolOff int64 = 1100 75 | 76 | var globalIgnoreMismatch = false 77 | 78 | var globalStartNew = true 79 | 80 | var globalFromCache = false 81 | 82 | var globalMaxAge = 0 83 | 84 | var globalInsecure = false 85 | 86 | var globalEmailAddress = "" 87 | 88 | var httpClient *http.Client 89 | 90 | type LabsError struct { 91 | Field string 92 | Message string 93 | } 94 | 95 | type LabsErrorResponse struct { 96 | ResponseErrors []LabsError `json:"errors"` 97 | } 98 | 99 | func (e LabsErrorResponse) Error() string { 100 | msg, err := json.Marshal(e) 101 | if err != nil { 102 | return err.Error() 103 | } else { 104 | return string(msg) 105 | } 106 | } 107 | 108 | type LabsKey struct { 109 | Size int 110 | Strength int 111 | Alg string 112 | DebianFlaw bool 113 | Q int 114 | } 115 | 116 | type LabsCaaRecord struct { 117 | Tag string 118 | Value string 119 | Flags int 120 | } 121 | 122 | type LabsCaaPolicy struct { 123 | PolicyHostname string 124 | CaaRecords []LabsCaaRecord 125 | } 126 | 127 | type LabsCert struct { 128 | Id string 129 | Subject string 130 | CommonNames []string 131 | AltNames []string 132 | NotBefore int64 133 | NotAfter int64 134 | IssuerSubject string 135 | SigAlg string 136 | RevocationInfo int 137 | CrlURIs []string 138 | OcspURIs []string 139 | RevocationStatus int 140 | CrlRevocationStatus int 141 | OcspRevocationStatus int 142 | DnsCaa bool 143 | Caapolicy LabsCaaPolicy 144 | MustStaple bool 145 | Sgc int 146 | ValidationType string 147 | Issues int 148 | Sct bool 149 | Sha1Hash string 150 | PinSha256 string 151 | KeyAlg string 152 | KeySize int 153 | KeyStrength int 154 | KeyKnownDebianInsecure bool 155 | Raw string 156 | } 157 | 158 | type LabsChainCert struct { 159 | Subject string 160 | Label string 161 | NotBefore int64 162 | NotAfter int64 163 | IssuerSubject string 164 | IssuerLabel string 165 | SigAlg string 166 | Issues int 167 | KeyAlg string 168 | KeySize int 169 | KeyStrength int 170 | RevocationStatus int 171 | CrlRevocationStatus int 172 | OcspRevocationStatus int 173 | Raw string 174 | } 175 | 176 | type LabsChain struct { 177 | Certs []LabsChainCert 178 | Issues int 179 | } 180 | 181 | type LabsProtocol struct { 182 | Id int 183 | Name string 184 | Version string 185 | V2SuitesDisabled bool 186 | Q int 187 | } 188 | 189 | type LabsSimClient struct { 190 | Id int 191 | Name string 192 | Platform string 193 | Version string 194 | IsReference bool 195 | } 196 | 197 | type LabsSimulation struct { 198 | Client LabsSimClient 199 | ErrorCode int 200 | ErrorMessage string 201 | Attempts int 202 | CertChainId string 203 | ProtocolId int 204 | SuiteId int 205 | SuiteName string 206 | KxType string 207 | KxStrength int 208 | DhBits int 209 | DhP int 210 | DhG int 211 | DhYs int 212 | NamedGroupBits int 213 | NamedGroupId int 214 | NamedGroupName string 215 | AlertType int 216 | AlertCode int 217 | KeyAlg string 218 | KeySize int 219 | SigAlg string 220 | } 221 | 222 | type LabsSimDetails struct { 223 | Results []LabsSimulation 224 | } 225 | 226 | type LabsSuite struct { 227 | Id int 228 | Name string 229 | CipherStrength int 230 | KxType string 231 | KxStrength int 232 | DhBits int 233 | DhP int 234 | DhG int 235 | DhYs int 236 | NamedGroupBits int 237 | NamedGroupId int 238 | NamedGroudName string 239 | Q int 240 | } 241 | 242 | type LabsSuites struct { 243 | Protocol int 244 | List []LabsSuite 245 | Preference bool 246 | } 247 | 248 | type LabsHstsPolicy struct { 249 | LONG_MAX_AGE int64 250 | Header string 251 | Status string 252 | Error string 253 | MaxAge int64 254 | IncludeSubDomains bool 255 | Preload bool 256 | Directives map[string]string 257 | } 258 | 259 | type LabsHstsPreload struct { 260 | Source string 261 | HostName string 262 | Status string 263 | Error string 264 | SourceTime int64 265 | } 266 | 267 | type LabsHpkpPin struct { 268 | HashFunction string 269 | Value string 270 | } 271 | 272 | type LabsHpkpDirective struct { 273 | Name string 274 | Value string 275 | } 276 | 277 | type LabsHpkpPolicy struct { 278 | Header string 279 | Status string 280 | Error string 281 | MaxAge int64 282 | IncludeSubDomains bool 283 | ReportUri string 284 | Pins []LabsHpkpPin 285 | MatchedPins []LabsHpkpPin 286 | Directives []LabsHpkpDirective 287 | } 288 | 289 | type LabsDrownHost struct { 290 | Ip string 291 | Export bool 292 | Port int 293 | Special bool 294 | Sslv2 bool 295 | Status string 296 | } 297 | 298 | type LabsCertChain struct { 299 | Id string 300 | CertIds []string 301 | Trustpath []LabsTrustPath 302 | Issues int 303 | NoSni bool 304 | } 305 | 306 | type LabsTrustPath struct { 307 | CertIds []string 308 | Trust []LabsTrust 309 | IsPinned bool 310 | MatchedPins int 311 | UnMatchedPins int 312 | } 313 | 314 | type LabsTrust struct { 315 | RootStore string 316 | IsTrusted bool 317 | TrustErrorMessage string 318 | } 319 | 320 | type LabsNamedGroups struct { 321 | List []LabsNamedGroup 322 | Preference bool 323 | } 324 | 325 | type LabsNamedGroup struct { 326 | Id int 327 | Name string 328 | Bits int 329 | } 330 | 331 | type LabsHttpTransaction struct { 332 | RequestUrl string 333 | StatusCode int 334 | RequestLine string 335 | RequestHeaders []string 336 | ResponseLine string 337 | ResponseRawHeader []string 338 | ResponseHeader []LabsHttpHeader 339 | FragileServer bool 340 | } 341 | 342 | type LabsHttpHeader struct { 343 | Name string 344 | Value string 345 | } 346 | 347 | type LabsEndpointDetails struct { 348 | HostStartTime int64 349 | CertChains []LabsCertChain 350 | Protocols []LabsProtocol 351 | Suites []LabsSuites 352 | NoSniSuites LabsSuites 353 | NamedGroups LabsNamedGroups 354 | ServerSignature string 355 | PrefixDelegation bool 356 | NonPrefixDelegation bool 357 | VulnBeast bool 358 | RenegSupport int 359 | SessionResumption int 360 | CompressionMethods int 361 | SupportsNpn bool 362 | NpnProtocols string 363 | SupportsAlpn bool 364 | AlpnProtocols string 365 | SessionTickets int 366 | OcspStapling bool 367 | StaplingRevocationStatus int 368 | StaplingRevocationErrorMessage string 369 | SniRequired bool 370 | HttpStatusCode int 371 | HttpForwarding string 372 | SupportsRc4 bool 373 | Rc4WithModern bool 374 | Rc4Only bool 375 | ForwardSecrecy int 376 | ProtocolIntolerance int 377 | MiscIntolerance int 378 | Sims LabsSimDetails 379 | Heartbleed bool 380 | Heartbeat bool 381 | OpenSslCcs int 382 | OpenSSLLuckyMinus20 int 383 | Ticketbleed int 384 | Bleichenbacher int 385 | Poodle bool 386 | PoodleTLS int 387 | FallbackScsv bool 388 | Freak bool 389 | HasSct int 390 | DhPrimes []string 391 | DhUsesKnownPrimes int 392 | DhYsReuse bool 393 | EcdhParameterReuse bool 394 | Logjam bool 395 | ChaCha20Preference bool 396 | HstsPolicy LabsHstsPolicy 397 | HstsPreloads []LabsHstsPreload 398 | HpkpPolicy LabsHpkpPolicy 399 | HpkpRoPolicy LabsHpkpPolicy 400 | HttpTransactions []LabsHttpTransaction 401 | DrownHosts []LabsDrownHost 402 | DrownErrors bool 403 | DrownVulnerable bool 404 | } 405 | 406 | type LabsEndpoint struct { 407 | IpAddress string 408 | ServerName string 409 | StatusMessage string 410 | StatusDetailsMessage string 411 | Grade string 412 | GradeTrustIgnored string 413 | FutureGrade string 414 | HasWarnings bool 415 | IsExceptional bool 416 | Progress int 417 | Duration int 418 | Eta int 419 | Delegation int 420 | Details LabsEndpointDetails 421 | } 422 | 423 | type LabsReport struct { 424 | Host string 425 | Port int 426 | Protocol string 427 | IsPublic bool 428 | Status string 429 | StatusMessage string 430 | StartTime int64 431 | TestTime int64 432 | EngineVersion string 433 | CriteriaVersion string 434 | CacheExpiryTime int64 435 | CertHostnames []string 436 | Endpoints []LabsEndpoint 437 | Certs []LabsCert 438 | rawJSON string 439 | } 440 | 441 | type LabsResults struct { 442 | reports []LabsReport 443 | responses []string 444 | } 445 | 446 | type LabsInfo struct { 447 | EngineVersion string 448 | CriteriaVersion string 449 | MaxAssessments int 450 | CurrentAssessments int 451 | NewAssessmentCoolOff int64 452 | Messages []string 453 | } 454 | 455 | func invokeGetRepeatedly(url string) (*http.Response, []byte, error) { 456 | retryCount := 0 457 | 458 | for { 459 | var reqId = atomic.AddUint64(&requestCounter, 1) 460 | 461 | if logLevel >= LOG_DEBUG { 462 | log.Printf("[DEBUG] Request #%v: %v", reqId, url) 463 | } 464 | 465 | req, err := http.NewRequest("GET", url, nil) 466 | if err != nil { 467 | return nil, nil, err 468 | } 469 | 470 | req.Header.Add("User-Agent", USER_AGENT) 471 | req.Header.Add("email", globalEmailAddress) 472 | 473 | resp, err := httpClient.Do(req) 474 | if err == nil { 475 | if logLevel >= LOG_DEBUG { 476 | log.Printf("[DEBUG] Response #%v status: %v %v", reqId, resp.Proto, resp.Status) 477 | } 478 | 479 | if logLevel >= LOG_TRACE { 480 | for key, values := range resp.Header { 481 | for _, value := range values { 482 | log.Printf("[TRACE] %v: %v\n", key, value) 483 | } 484 | } 485 | } 486 | 487 | if logLevel >= LOG_NOTICE { 488 | for key, values := range resp.Header { 489 | if strings.ToLower(key) == "x-message" { 490 | for _, value := range values { 491 | log.Printf("[NOTICE] Server message: %v\n", value) 492 | } 493 | } 494 | } 495 | } 496 | 497 | // Update current assessments. 498 | 499 | headerValue := resp.Header.Get("X-Current-Assessments") 500 | if headerValue != "" { 501 | i, err := strconv.Atoi(headerValue) 502 | if err == nil { 503 | if currentAssessments != i { 504 | currentAssessments = i 505 | 506 | if logLevel >= LOG_DEBUG { 507 | log.Printf("[DEBUG] Server set current assessments to %v", headerValue) 508 | } 509 | } 510 | } else { 511 | if logLevel >= LOG_WARNING { 512 | log.Printf("[WARNING] Ignoring invalid X-Current-Assessments value (%v): %v", headerValue, err) 513 | } 514 | } 515 | } 516 | 517 | // Update maximum assessments. 518 | 519 | headerValue = resp.Header.Get("X-Max-Assessments") 520 | if headerValue != "" { 521 | i, err := strconv.Atoi(headerValue) 522 | if err == nil { 523 | if maxAssessments != i { 524 | maxAssessments = i 525 | 526 | if maxAssessments <= 0 { 527 | log.Fatalf("[ERROR] Server doesn't allow further API requests") 528 | } 529 | 530 | if logLevel >= LOG_DEBUG { 531 | log.Printf("[DEBUG] Server set maximum assessments to %v", headerValue) 532 | } 533 | } 534 | } else { 535 | if logLevel >= LOG_WARNING { 536 | log.Printf("[WARNING] Ignoring invalid X-Max-Assessments value (%v): %v", headerValue, err) 537 | } 538 | } 539 | } 540 | 541 | // Retrieve the response body. 542 | 543 | defer resp.Body.Close() 544 | 545 | body, err := io.ReadAll(resp.Body) 546 | if err != nil { 547 | return nil, nil, err 548 | } 549 | 550 | if logLevel >= LOG_TRACE { 551 | log.Printf("[TRACE] Response #%v body:\n%v", reqId, string(body)) 552 | } 553 | 554 | return resp, body, nil 555 | } else { 556 | if strings.Contains(err.Error(), "EOF") { 557 | // Server closed a persistent connection on us, which 558 | // Go doesn't seem to be handling well. So we'll try one 559 | // more time. 560 | if retryCount > 5 { 561 | log.Fatalf("[ERROR] Too many HTTP requests (5) failed with EOF (ref#2)") 562 | } 563 | 564 | if logLevel >= LOG_DEBUG { 565 | log.Printf("[DEBUG] HTTP request failed with EOF (ref#2)") 566 | } 567 | } else { 568 | log.Fatalf("[ERROR] HTTP request failed: %v (ref#2)", err.Error()) 569 | } 570 | 571 | retryCount++ 572 | } 573 | } 574 | } 575 | 576 | func invokeApi(command string) (*http.Response, []byte, error) { 577 | var url = apiLocation + "/" + command 578 | 579 | for { 580 | resp, body, err := invokeGetRepeatedly(url) 581 | if err != nil { 582 | return nil, nil, err 583 | } 584 | 585 | if resp.StatusCode == 400 { 586 | // 400 status code can be an invalid email address error check the error field to confirm 587 | apiError, err := getLabsErrorResponse(body) 588 | if err != nil { 589 | return nil, nil, err 590 | } 591 | apiResponseError := apiError.ResponseErrors[0] 592 | if apiResponseError.Field == "email" { 593 | log.Fatalf("[ERROR] %s", apiResponseError.Message) 594 | } 595 | } 596 | 597 | // Status codes 429, 503, and 529 essentially mean try later. Thus, 598 | // if we encounter them, we sleep for a while and try again. 599 | if resp.StatusCode == 429 { 600 | return resp, body, errors.New("Assessment failed: 429") 601 | } else if (resp.StatusCode == 503) || (resp.StatusCode == 529) { 602 | // In case of the overloaded server, randomize the sleep time so 603 | // that some clients reconnect earlier and some later. 604 | 605 | sleepTime := 15 + rand.Int31n(15) 606 | 607 | if logLevel >= LOG_NOTICE { 608 | log.Printf("[NOTICE] Sleeping for %v minutes after a %v response", sleepTime, resp.StatusCode) 609 | } 610 | 611 | time.Sleep(time.Duration(sleepTime) * time.Minute) 612 | } else if (resp.StatusCode != 200) && (resp.StatusCode != 400) { 613 | log.Fatalf("[ERROR] Unexpected response status code %v", resp.StatusCode) 614 | } else { 615 | return resp, body, nil 616 | } 617 | } 618 | } 619 | 620 | func invokeInfo() (*LabsInfo, error) { 621 | var command = "info" 622 | 623 | _, body, err := invokeApi(command) 624 | if err != nil { 625 | return nil, err 626 | } 627 | 628 | var labsInfo LabsInfo 629 | err = json.Unmarshal(body, &labsInfo) 630 | if err != nil { 631 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 632 | return nil, err 633 | } 634 | 635 | return &labsInfo, nil 636 | } 637 | 638 | func invokeAnalyze(host string, startNew bool, fromCache bool) (*LabsReport, error) { 639 | var command = "analyze?host=" + host + "&all=done" 640 | 641 | if fromCache { 642 | command = command + "&fromCache=on" 643 | 644 | if globalMaxAge != 0 { 645 | command = command + "&maxAge=" + strconv.Itoa(globalMaxAge) 646 | } 647 | } else if startNew { 648 | command = command + "&startNew=on" 649 | } 650 | 651 | if globalIgnoreMismatch { 652 | command = command + "&ignoreMismatch=on" 653 | } 654 | 655 | resp, body, err := invokeApi(command) 656 | if err != nil { 657 | return nil, err 658 | } 659 | 660 | // Use the status code to determine if the response is an error. 661 | if resp.StatusCode == 400 { 662 | // Parameter validation error. 663 | apiError, err := getLabsErrorResponse(body) 664 | if err != nil { 665 | return nil, err 666 | } 667 | 668 | return nil, apiError 669 | } else { 670 | // We should have a proper response. 671 | 672 | var analyzeResponse LabsReport 673 | err = json.Unmarshal(body, &analyzeResponse) 674 | if err != nil { 675 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 676 | return nil, err 677 | } 678 | 679 | // Add the JSON body to the response 680 | analyzeResponse.rawJSON = string(body) 681 | 682 | return &analyzeResponse, nil 683 | } 684 | } 685 | 686 | func getLabsErrorResponse(body []byte) (*LabsErrorResponse, error) { 687 | var apiError LabsErrorResponse 688 | var err = json.Unmarshal(body, &apiError) 689 | if err != nil { 690 | log.Printf("[ERROR] JSON unmarshal error: %v", err) 691 | return nil, err 692 | } 693 | return &apiError, err 694 | } 695 | 696 | type Event struct { 697 | host string 698 | eventType int 699 | report *LabsReport 700 | } 701 | 702 | const ( 703 | ASSESSMENT_FAILED = -1 704 | ASSESSMENT_STARTING = 0 705 | ASSESSMENT_COMPLETE = 1 706 | ) 707 | 708 | func NewAssessment(host string, eventChannel chan Event) { 709 | eventChannel <- Event{host, ASSESSMENT_STARTING, nil} 710 | 711 | var report *LabsReport 712 | var startTime int64 = -1 713 | var startNew = globalStartNew 714 | 715 | for { 716 | myResponse, err := invokeAnalyze(host, startNew, globalFromCache) 717 | if err != nil { 718 | eventChannel <- Event{host, ASSESSMENT_FAILED, nil} 719 | return 720 | } 721 | 722 | if startTime == -1 { 723 | startTime = myResponse.StartTime 724 | startNew = false 725 | } else { 726 | // Abort this assessment if the time we receive in a follow-up check 727 | // is older than the time we got when we started the request. The 728 | // upstream code should then retry the hostname in order to get 729 | // consistent results. 730 | if myResponse.StartTime > startTime { 731 | eventChannel <- Event{host, ASSESSMENT_FAILED, nil} 732 | return 733 | } else { 734 | startTime = myResponse.StartTime 735 | } 736 | } 737 | 738 | if (myResponse.Status == "READY") || (myResponse.Status == "ERROR") { 739 | report = myResponse 740 | break 741 | } 742 | 743 | time.Sleep(5 * time.Second) 744 | } 745 | 746 | eventChannel <- Event{host, ASSESSMENT_COMPLETE, report} 747 | } 748 | 749 | type HostProvider struct { 750 | hostnames []string 751 | StartingLen int 752 | } 753 | 754 | func NewHostProvider(hs []string) *HostProvider { 755 | hostnames := make([]string, len(hs)) 756 | copy(hostnames, hs) 757 | hostProvider := HostProvider{hostnames, len(hs)} 758 | return &hostProvider 759 | } 760 | 761 | func (hp *HostProvider) next() (string, bool) { 762 | if len(hp.hostnames) == 0 { 763 | return "", false 764 | } 765 | 766 | var e string 767 | e, hp.hostnames = hp.hostnames[0], hp.hostnames[1:] 768 | 769 | return e, true 770 | } 771 | 772 | func (hp *HostProvider) retry(host string) { 773 | hp.hostnames = append(hp.hostnames, host) 774 | } 775 | 776 | type Manager struct { 777 | hostProvider *HostProvider 778 | FrontendEventChannel chan Event 779 | BackendEventChannel chan Event 780 | results *LabsResults 781 | } 782 | 783 | func NewManager(hostProvider *HostProvider) *Manager { 784 | manager := Manager{ 785 | hostProvider: hostProvider, 786 | FrontendEventChannel: make(chan Event), 787 | BackendEventChannel: make(chan Event), 788 | results: &LabsResults{reports: make([]LabsReport, 0)}, 789 | } 790 | 791 | go manager.run() 792 | 793 | return &manager 794 | } 795 | 796 | func (manager *Manager) startAssessment(h string) { 797 | go NewAssessment(h, manager.BackendEventChannel) 798 | activeAssessments++ 799 | } 800 | 801 | func (manager *Manager) run() { 802 | transport := &http.Transport{ 803 | TLSClientConfig: &tls.Config{InsecureSkipVerify: globalInsecure}, 804 | DisableKeepAlives: false, 805 | Proxy: http.ProxyFromEnvironment, 806 | } 807 | 808 | httpClient = &http.Client{Transport: transport} 809 | 810 | // Ping SSL Labs to determine how many concurrent 811 | // assessments we're allowed to use. Print the API version 812 | // information and the limits. 813 | 814 | labsInfo, err := invokeInfo() 815 | if err != nil { 816 | // TODO Signal error so that we return the correct exit code 817 | close(manager.FrontendEventChannel) 818 | } 819 | 820 | if logLevel >= LOG_INFO { 821 | log.Printf("[INFO] SSL Labs v%v (criteria version %v)", labsInfo.EngineVersion, labsInfo.CriteriaVersion) 822 | } 823 | 824 | if logLevel >= LOG_NOTICE { 825 | for _, message := range labsInfo.Messages { 826 | log.Printf("[NOTICE] Server message: %v", message) 827 | } 828 | } 829 | 830 | maxAssessments = labsInfo.MaxAssessments 831 | 832 | if maxAssessments <= 0 { 833 | if logLevel >= LOG_WARNING { 834 | log.Printf("[WARNING] You're not allowed to request new assessments") 835 | } 836 | } 837 | 838 | moreAssessments := true 839 | 840 | if labsInfo.NewAssessmentCoolOff >= 1000 { 841 | globalNewAssessmentCoolOff = 100 + labsInfo.NewAssessmentCoolOff 842 | } else { 843 | if logLevel >= LOG_WARNING { 844 | log.Printf("[WARNING] Info.NewAssessmentCoolOff too small: %v", labsInfo.NewAssessmentCoolOff) 845 | } 846 | } 847 | 848 | for { 849 | select { 850 | // Handle assessment events (e.g., starting and finishing). 851 | case e := <-manager.BackendEventChannel: 852 | if e.eventType == ASSESSMENT_FAILED { 853 | activeAssessments-- 854 | manager.hostProvider.retry(e.host) 855 | } 856 | 857 | if e.eventType == ASSESSMENT_STARTING { 858 | if logLevel >= LOG_INFO { 859 | log.Printf("[INFO] Assessment starting: %v", e.host) 860 | } 861 | } 862 | 863 | if e.eventType == ASSESSMENT_COMPLETE { 864 | if logLevel >= LOG_INFO { 865 | msg := "" 866 | 867 | if len(e.report.Endpoints) == 0 { 868 | msg = fmt.Sprintf("[WARN] Assessment failed: %v (%v)", e.host, e.report.StatusMessage) 869 | } else if len(e.report.Endpoints) > 1 { 870 | msg = fmt.Sprintf("[INFO] Assessment complete: %v (%v hosts in %v seconds)", 871 | e.host, len(e.report.Endpoints), (e.report.TestTime-e.report.StartTime)/1000) 872 | } else { 873 | msg = fmt.Sprintf("[INFO] Assessment complete: %v (%v host in %v seconds)", 874 | e.host, len(e.report.Endpoints), (e.report.TestTime-e.report.StartTime)/1000) 875 | } 876 | 877 | for _, endpoint := range e.report.Endpoints { 878 | if endpoint.Grade != "" { 879 | msg = msg + "\n " + endpoint.IpAddress + ": " + endpoint.Grade 880 | if endpoint.FutureGrade != "" { 881 | msg = msg + " -> " + endpoint.FutureGrade 882 | } 883 | } else { 884 | msg = msg + "\n " + endpoint.IpAddress + ": Err: " + endpoint.StatusMessage 885 | } 886 | } 887 | 888 | log.Println(msg) 889 | } 890 | 891 | activeAssessments-- 892 | 893 | manager.results.reports = append(manager.results.reports, *e.report) 894 | manager.results.responses = append(manager.results.responses, e.report.rawJSON) 895 | 896 | if logLevel >= LOG_DEBUG { 897 | log.Printf("[DEBUG] Active assessments: %v (more: %v)", activeAssessments, moreAssessments) 898 | } 899 | } 900 | 901 | // Are we done? 902 | if (activeAssessments == 0) && (moreAssessments == false) { 903 | close(manager.FrontendEventChannel) 904 | return 905 | } 906 | 907 | break 908 | 909 | // Once a second, start a new assessment, provided there are 910 | // hostnames left and we're not over the concurrent assessment limit. 911 | default: 912 | if manager.hostProvider.StartingLen > 0 { 913 | <-time.NewTimer(time.Duration(globalNewAssessmentCoolOff) * time.Millisecond).C 914 | } 915 | 916 | if moreAssessments { 917 | if currentAssessments < maxAssessments { 918 | host, hasNext := manager.hostProvider.next() 919 | if hasNext { 920 | manager.startAssessment(host) 921 | } else { 922 | // We've run out of hostnames and now just need 923 | // to wait for all the assessments to complete. 924 | moreAssessments = false 925 | 926 | if activeAssessments == 0 { 927 | close(manager.FrontendEventChannel) 928 | return 929 | } 930 | } 931 | } 932 | } 933 | break 934 | } 935 | } 936 | } 937 | 938 | func parseLogLevel(level string) int { 939 | switch { 940 | case level == "error": 941 | return LOG_ERROR 942 | case level == "notice": 943 | return LOG_NOTICE 944 | case level == "info": 945 | return LOG_INFO 946 | case level == "debug": 947 | return LOG_DEBUG 948 | case level == "trace": 949 | return LOG_TRACE 950 | } 951 | 952 | log.Fatalf("[ERROR] Unrecognized log level: %v", level) 953 | return -1 954 | } 955 | 956 | func flattenJSON(inputJSON map[string]interface{}, rootKey string, flattened *map[string]interface{}) { 957 | var keysep = "." // Char to separate keys 958 | var Q = "\"" // Char to envelope strings 959 | 960 | for rkey, value := range inputJSON { 961 | key := rootKey + rkey 962 | if _, ok := value.(string); ok { 963 | (*flattened)[key] = Q + value.(string) + Q 964 | } else if _, ok := value.(float64); ok { 965 | (*flattened)[key] = fmt.Sprintf("%.f", value) 966 | } else if _, ok := value.(bool); ok { 967 | (*flattened)[key] = value.(bool) 968 | } else if _, ok := value.([]interface{}); ok { 969 | for i := 0; i < len(value.([]interface{})); i++ { 970 | aKey := key + keysep + strconv.Itoa(i) 971 | if _, ok := value.([]interface{})[i].(string); ok { 972 | (*flattened)[aKey] = Q + value.([]interface{})[i].(string) + Q 973 | } else if _, ok := value.([]interface{})[i].(float64); ok { 974 | (*flattened)[aKey] = value.([]interface{})[i].(float64) 975 | } else if _, ok := value.([]interface{})[i].(bool); ok { 976 | (*flattened)[aKey] = value.([]interface{})[i].(bool) 977 | } else { 978 | flattenJSON(value.([]interface{})[i].(map[string]interface{}), key+keysep+strconv.Itoa(i)+keysep, flattened) 979 | } 980 | } 981 | } else if value == nil { 982 | (*flattened)[key] = nil 983 | } else { 984 | flattenJSON(value.(map[string]interface{}), key+keysep, flattened) 985 | } 986 | } 987 | } 988 | 989 | func flattenAndFormatJSON(inputJSON []byte) *[]string { 990 | var flattened = make(map[string]interface{}) 991 | 992 | mappedJSON := map[string]interface{}{} 993 | err := json.Unmarshal(inputJSON, &mappedJSON) 994 | if err != nil { 995 | log.Fatalf("[ERROR] Reconsitution of JSON failed: %v", err) 996 | } 997 | 998 | // Flatten the JSON structure, recursively 999 | flattenJSON(mappedJSON, "", &flattened) 1000 | 1001 | // Make a sorted index, so we can print keys in order 1002 | kIndex := make([]string, len(flattened)) 1003 | ki := 0 1004 | for key, _ := range flattened { 1005 | kIndex[ki] = key 1006 | ki++ 1007 | } 1008 | sort.Strings(kIndex) 1009 | 1010 | // Ordered flattened data 1011 | var flatStrings []string 1012 | for _, value := range kIndex { 1013 | flatStrings = append(flatStrings, fmt.Sprintf("\"%v\": %v\n", value, flattened[value])) 1014 | } 1015 | return &flatStrings 1016 | } 1017 | 1018 | func readLines(path *string) ([]string, error) { 1019 | file, err := os.Open(*path) 1020 | if err != nil { 1021 | return nil, err 1022 | } 1023 | defer file.Close() 1024 | 1025 | var lines []string 1026 | scanner := bufio.NewScanner(file) 1027 | for scanner.Scan() { 1028 | var line = strings.TrimSpace(scanner.Text()) 1029 | if (!strings.HasPrefix(line, "#")) && (line != "") { 1030 | lines = append(lines, line) 1031 | } 1032 | } 1033 | return lines, scanner.Err() 1034 | } 1035 | 1036 | func validateURL(URL string) bool { 1037 | _, err := url.Parse(URL) 1038 | if err != nil { 1039 | return false 1040 | } else { 1041 | return true 1042 | } 1043 | } 1044 | 1045 | func validateHostname(hostname string) bool { 1046 | addrs, err := net.LookupHost(hostname) 1047 | 1048 | // In some cases there is no error 1049 | // but there are also no addresses 1050 | if err != nil || len(addrs) < 1 { 1051 | return false 1052 | } else { 1053 | return true 1054 | } 1055 | } 1056 | 1057 | func main() { 1058 | var conf_api = flag.String("api", "BUILTIN", "API entry point, for example https://www.example.com/api/") 1059 | var conf_grade = flag.Bool("grade", false, "Output only the hostname: grade") 1060 | var conf_hostcheck = flag.Bool("hostcheck", false, "If true, host resolution failure will result in a fatal error.") 1061 | var conf_hostfile = flag.String("hostfile", "", "File containing hosts to scan (one per line)") 1062 | var conf_email = flag.String("email", "", "Registered organization email address on SSLLabs") 1063 | var conf_ignore_mismatch = flag.Bool("ignore-mismatch", false, "If true, certificate hostname mismatch does not stop assessment.") 1064 | var conf_insecure = flag.Bool("insecure", false, "Skip certificate validation. For use in development only. Do not use.") 1065 | var conf_json_flat = flag.Bool("json-flat", false, "Output results in flattened JSON format") 1066 | var conf_quiet = flag.Bool("quiet", false, "Disable status messages (logging)") 1067 | var conf_usecache = flag.Bool("usecache", false, "If true, accept cached results (if available), else force live scan.") 1068 | var conf_maxage = flag.Int("maxage", 0, "Maximum acceptable age of cached results, in hours. A zero value is ignored.") 1069 | var conf_verbosity = flag.String("verbosity", "info", "Configure log verbosity: error, notice, info, debug, or trace.") 1070 | var conf_version = flag.Bool("version", false, "Print version and API location information and exit") 1071 | 1072 | flag.Parse() 1073 | 1074 | if *conf_email == "" { 1075 | // email is required 1076 | log.Fatalf("[ERROR] Email address cannot be empty. Please use --email flag and pass your registered organization email") 1077 | } else { 1078 | globalEmailAddress = *conf_email 1079 | } 1080 | 1081 | if *conf_version { 1082 | fmt.Println(USER_AGENT) 1083 | fmt.Println("API location: " + apiLocation) 1084 | return 1085 | } 1086 | 1087 | globalIgnoreMismatch = *conf_ignore_mismatch 1088 | 1089 | if *conf_quiet { 1090 | logLevel = LOG_NONE 1091 | } else { 1092 | logLevel = parseLogLevel(strings.ToLower(*conf_verbosity)) 1093 | } 1094 | 1095 | // We prefer cached results 1096 | if *conf_usecache { 1097 | globalFromCache = true 1098 | globalStartNew = false 1099 | } 1100 | 1101 | if *conf_maxage != 0 { 1102 | globalMaxAge = *conf_maxage 1103 | } 1104 | 1105 | // Verify that the API entry point is a URL. 1106 | if *conf_api != "BUILTIN" { 1107 | apiLocation = *conf_api 1108 | } 1109 | 1110 | if validateURL(apiLocation) == false { 1111 | log.Fatalf("[ERROR] Invalid API URL: %v", apiLocation) 1112 | } 1113 | 1114 | var hostnames []string 1115 | 1116 | if *conf_hostfile != "" { 1117 | // Open file, and read it 1118 | var err error 1119 | hostnames, err = readLines(conf_hostfile) 1120 | if err != nil { 1121 | log.Fatalf("[ERROR] Reading from specified hostfile failed: %v", err) 1122 | } 1123 | 1124 | } else { 1125 | // Read hostnames from the rest of the args 1126 | hostnames = flag.Args() 1127 | } 1128 | 1129 | if *conf_hostcheck { 1130 | // Validate all hostnames before we attempt to test them. At least 1131 | // one hostname is required. 1132 | for _, host := range hostnames { 1133 | if validateHostname(host) == false { 1134 | log.Fatalf("[ERROR] Invalid hostname: %v", host) 1135 | } 1136 | } 1137 | } 1138 | 1139 | if *conf_insecure { 1140 | globalInsecure = *conf_insecure 1141 | } 1142 | 1143 | hp := NewHostProvider(hostnames) 1144 | manager := NewManager(hp) 1145 | 1146 | // Respond to events until all the work is done. 1147 | for { 1148 | _, running := <-manager.FrontendEventChannel 1149 | if running == false { 1150 | var err error 1151 | 1152 | if hp.StartingLen == 0 { 1153 | return 1154 | } 1155 | 1156 | if *conf_grade { 1157 | for i := range manager.results.responses { 1158 | results := []byte(manager.results.responses[i]) 1159 | 1160 | // Fill LabsReport with json response received i.e results 1161 | var labsReport LabsReport 1162 | err = json.Unmarshal(results, &labsReport) 1163 | // Check for error while unmarshalling. If yes then display error messsage and terminate the program 1164 | if err != nil { 1165 | log.Fatalf("[ERROR] JSON unmarshal error: %v", err) 1166 | } 1167 | 1168 | // Printing the Hostname and IpAddress with grades 1169 | fmt.Println() 1170 | if !strings.EqualFold(labsReport.StatusMessage, "ERROR") { 1171 | fmt.Printf("HostName:\"%v\"\n", labsReport.Host) 1172 | for _, endpoints := range labsReport.Endpoints { 1173 | if endpoints.FutureGrade != "" { 1174 | fmt.Printf("\"%v\":\"%v\"->\"%v\"\n", endpoints.IpAddress, endpoints.Grade, endpoints.FutureGrade) 1175 | } else { 1176 | if endpoints.Grade != "" { 1177 | fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.Grade) 1178 | } else { 1179 | // When no grade is seen print Status Message 1180 | fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.StatusMessage) 1181 | } 1182 | } 1183 | } 1184 | } 1185 | } 1186 | } else if *conf_json_flat { 1187 | // Flat JSON and RAW 1188 | 1189 | for i := range manager.results.responses { 1190 | results := []byte(manager.results.responses[i]) 1191 | 1192 | flattened := flattenAndFormatJSON(results) 1193 | 1194 | // Print the flattened data 1195 | fmt.Println(*flattened) 1196 | } 1197 | } else { 1198 | // Raw (non-Go-mangled) JSON output 1199 | 1200 | fmt.Println("[") 1201 | for i := range manager.results.responses { 1202 | results := manager.results.responses[i] 1203 | 1204 | if i > 0 { 1205 | fmt.Println(",") 1206 | } 1207 | fmt.Println(results) 1208 | 1209 | } 1210 | fmt.Println("]") 1211 | } 1212 | 1213 | if err != nil { 1214 | log.Fatalf("[ERROR] Output to JSON failed: %v", err) 1215 | } 1216 | 1217 | if logLevel >= LOG_INFO { 1218 | log.Println("[INFO] All assessments complete; shutting down") 1219 | } 1220 | 1221 | return 1222 | } 1223 | } 1224 | } 1225 | --------------------------------------------------------------------------------