diff --git a/cmd/ssllabs-scan/ssllabs-scan-v3.go b/cmd/ssllabs-scan/ssllabs-scan-v3.go new file mode 100644 index 0000000..527f553 --- /dev/null +++ b/cmd/ssllabs-scan/ssllabs-scan-v3.go @@ -0,0 +1,277 @@ +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "net/url" + "os" + "sort" + "strconv" + "strings" + + scan "github.com/prasincs/ssllabs-scan" +) + +var USER_AGENT = "ssllabs-scan v1.4.0 (stable $Id$)" + +var logLevel = scan.LOG_NOTICE + +func flattenJSON(inputJSON map[string]interface{}, rootKey string, flattened *map[string]interface{}) { + var keysep = "." // Char to separate keys + var Q = "\"" // Char to envelope strings + + for rkey, value := range inputJSON { + key := rootKey + rkey + if _, ok := value.(string); ok { + (*flattened)[key] = Q + value.(string) + Q + } else if _, ok := value.(float64); ok { + (*flattened)[key] = fmt.Sprintf("%.f", value) + } else if _, ok := value.(bool); ok { + (*flattened)[key] = value.(bool) + } else if _, ok := value.([]interface{}); ok { + for i := 0; i < len(value.([]interface{})); i++ { + aKey := key + keysep + strconv.Itoa(i) + if _, ok := value.([]interface{})[i].(string); ok { + (*flattened)[aKey] = Q + value.([]interface{})[i].(string) + Q + } else if _, ok := value.([]interface{})[i].(float64); ok { + (*flattened)[aKey] = value.([]interface{})[i].(float64) + } else if _, ok := value.([]interface{})[i].(bool); ok { + (*flattened)[aKey] = value.([]interface{})[i].(bool) + } else { + flattenJSON(value.([]interface{})[i].(map[string]interface{}), key+keysep+strconv.Itoa(i)+keysep, flattened) + } + } + } else if value == nil { + (*flattened)[key] = nil + } else { + flattenJSON(value.(map[string]interface{}), key+keysep, flattened) + } + } +} + +func flattenAndFormatJSON(inputJSON []byte) *[]string { + var flattened = make(map[string]interface{}) + + mappedJSON := map[string]interface{}{} + err := json.Unmarshal(inputJSON, &mappedJSON) + if err != nil { + log.Fatalf("[ERROR] Reconstitution of JSON failed: %v", err) + } + + // Flatten the JSON structure, recursively + flattenJSON(mappedJSON, "", &flattened) + + // Make a sorted index, so we can print keys in order + kIndex := make([]string, len(flattened)) + ki := 0 + for key, _ := range flattened { + kIndex[ki] = key + ki++ + } + sort.Strings(kIndex) + + // Ordered flattened data + var flatStrings []string + for _, value := range kIndex { + flatStrings = append(flatStrings, fmt.Sprintf("\"%v\": %v\n", value, flattened[value])) + } + return &flatStrings +} + +func readLines(path *string) ([]string, error) { + file, err := os.Open(*path) + if err != nil { + return nil, err + } + defer file.Close() + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + var line = strings.TrimSpace(scanner.Text()) + if (!strings.HasPrefix(line, "#")) && (line != "") { + lines = append(lines, line) + } + } + return lines, scanner.Err() +} + +func validateURL(URL string) bool { + _, err := url.Parse(URL) + if err != nil { + return false + } else { + return true + } +} + +func validateHostname(hostname string) bool { + addrs, err := net.LookupHost(hostname) + + // In some cases there is no error + // but there are also no addresses + if err != nil || len(addrs) < 1 { + return false + } else { + return true + } +} + +func main() { + var conf_api = flag.String("api", "BUILTIN", "API entry point, for example https://www.example.com/api/") + var conf_grade = flag.Bool("grade", false, "Output only the hostname: grade") + var conf_hostcheck = flag.Bool("hostcheck", false, "If true, host resolution failure will result in a fatal error.") + var conf_hostfile = flag.String("hostfile", "", "File containing hosts to scan (one per line)") + var conf_ignore_mismatch = flag.Bool("ignore-mismatch", false, "If true, certificate hostname mismatch does not stop assessment.") + var conf_insecure = flag.Bool("insecure", false, "Skip certificate validation. For use in development only. Do not use.") + var conf_json_flat = flag.Bool("json-flat", false, "Output results in flattened JSON format") + var conf_quiet = flag.Bool("quiet", false, "Disable status messages (logging)") + var conf_usecache = flag.Bool("usecache", false, "If true, accept cached results (if available), else force live scan.") + var conf_maxage = flag.Int("maxage", 0, "Maximum acceptable age of cached results, in hours. A zero value is ignored.") + var conf_verbosity = flag.String("verbosity", "info", "Configure log verbosity: error, notice, info, debug, or trace.") + var conf_version = flag.Bool("version", false, "Print version and API location information and exit") + + flag.Parse() + + if *conf_version { + fmt.Println(USER_AGENT) + fmt.Println("API location: " + *conf_api) + return + } + + scan.IgnoreMismatch(*conf_ignore_mismatch) + + if *conf_quiet { + logLevel = scan.LOG_NONE + } else { + logLevel = scan.ParseLogLevel(strings.ToLower(*conf_verbosity)) + } + + // We prefer cached results + scan.UseCache(*conf_usecache) + + if *conf_maxage != 0 { + scan.SetMaxAge(*conf_maxage) + } + + // Verify that the API entry point is a URL. + if *conf_api != "BUILTIN" { + if validateURL(*conf_api) == false { + log.Fatalf("[ERROR] Invalid API URL: %v", *conf_api) + } + scan.SetAPILocation(*conf_api) + } + + var hostnames []string + + if *conf_hostfile != "" { + // Open file, and read it + var err error + hostnames, err = readLines(conf_hostfile) + if err != nil { + log.Fatalf("[ERROR] Reading from specified hostfile failed: %v", err) + } + + } else { + // Read hostnames from the rest of the args + hostnames = flag.Args() + } + + if *conf_hostcheck { + // Validate all hostnames before we attempt to test them. At least + // one hostname is required. + for _, host := range hostnames { + if validateHostname(host) == false { + log.Fatalf("[ERROR] Invalid hostname: %v", host) + } + } + } + + scan.AllowInsecure(*conf_insecure) + + hp := scan.NewHostProvider(hostnames) + manager := scan.NewManager(hp) + + // Respond to events until all the work is done. + for { + _, running := <-manager.FrontendEventChannel + if running == false { + var err error + + if hp.StartingLen == 0 { + return + } + + if *conf_grade { + for i := range manager.Results.Responses { + results := []byte(manager.Results.Responses[i]) + + // Fill LabsReport with json response received i.e results + var labsReport scan.LabsReport + err = json.Unmarshal(results, &labsReport) + // Check for error while unmarshalling. If yes then display error messsage and terminate the program + if err != nil { + log.Fatalf("[ERROR] JSON unmarshal error: %v", err) + } + + // Printing the Hostname and IpAddress with grades + fmt.Println() + if !strings.EqualFold(labsReport.StatusMessage, "ERROR") { + fmt.Printf("HostName:\"%v\"\n", labsReport.Host) + for _, endpoints := range labsReport.Endpoints { + if endpoints.FutureGrade != "" { + fmt.Printf("\"%v\":\"%v\"->\"%v\"\n", endpoints.IpAddress, endpoints.Grade, endpoints.FutureGrade) + } else { + if endpoints.Grade != "" { + fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.Grade) + } else { + // When no grade is seen print Status Message + fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.StatusMessage) + } + } + } + } + } + } else if *conf_json_flat { + // Flat JSON and RAW + + for i := range manager.Results.Responses { + results := []byte(manager.Results.Responses[i]) + + flattened := flattenAndFormatJSON(results) + + // Print the flattened data + fmt.Println(*flattened) + } + } else { + // Raw (non-Go-mangled) JSON output + + fmt.Println("[") + for i := range manager.Results.Responses { + results := manager.Results.Responses[i] + + if i > 0 { + fmt.Println(",") + } + fmt.Println(results) + + } + fmt.Println("]") + } + + if err != nil { + log.Fatalf("[ERROR] Output to JSON failed: %v", err) + } + + if logLevel >= scan.LOG_INFO { + log.Println("[INFO] All assessments complete; shutting down") + } + + return + } + } +} diff --git a/ssllabs-scan-v3.go b/ssllabs-scan-v3.go index 6b68a21..20f51cf 100644 --- a/ssllabs-scan-v3.go +++ b/ssllabs-scan-v3.go @@ -18,21 +18,21 @@ */ // work in progress -package main +package scan import "crypto/tls" import "errors" import "encoding/json" -import "flag" + import "fmt" import "io/ioutil" import "bufio" import "os" import "log" import "math/rand" -import "net" + import "net/http" -import "net/url" + import "strconv" import "strings" import "sync/atomic" @@ -376,7 +376,7 @@ type LabsEndpointDetails struct { Heartbeat bool OpenSslCcs int OpenSSLLuckyMinus20 int - Ticketbleed int + Ticketbleed int Poodle bool PoodleTLS int FallbackScsv bool @@ -434,8 +434,8 @@ type LabsReport struct { } type LabsResults struct { - reports []LabsReport - responses []string + Reports []LabsReport + Responses []string } type LabsInfo struct { @@ -752,7 +752,7 @@ type Manager struct { hostProvider *HostProvider FrontendEventChannel chan Event BackendEventChannel chan Event - results *LabsResults + Results *LabsResults } func NewManager(hostProvider *HostProvider) *Manager { @@ -760,7 +760,7 @@ func NewManager(hostProvider *HostProvider) *Manager { hostProvider: hostProvider, FrontendEventChannel: make(chan Event), BackendEventChannel: make(chan Event), - results: &LabsResults{reports: make([]LabsReport, 0)}, + Results: &LabsResults{Reports: make([]LabsReport, 0)}, } go manager.run() @@ -865,8 +865,8 @@ func (manager *Manager) run() { activeAssessments-- - manager.results.reports = append(manager.results.reports, *e.report) - manager.results.responses = append(manager.results.responses, e.report.rawJSON) + manager.Results.Reports = append(manager.Results.Reports, *e.report) + manager.Results.Responses = append(manager.Results.Responses, e.report.rawJSON) if logLevel >= LOG_DEBUG { log.Printf("[DEBUG] Active assessments: %v (more: %v)", activeAssessments, moreAssessments) @@ -910,7 +910,7 @@ func (manager *Manager) run() { } } -func parseLogLevel(level string) int { +func ParseLogLevel(level string) int { switch { case level == "error": return LOG_ERROR @@ -1008,184 +1008,28 @@ func readLines(path *string) ([]string, error) { return lines, scanner.Err() } -func validateURL(URL string) bool { - _, err := url.Parse(URL) - if err != nil { - return false - } else { - return true - } +func IgnoreMismatch(ignore bool) { + globalIgnoreMismatch = ignore } -func validateHostname(hostname string) bool { - addrs, err := net.LookupHost(hostname) - - // In some cases there is no error - // but there are also no addresses - if err != nil || len(addrs) < 1 { - return false - } else { - return true - } -} - -func main() { - var conf_api = flag.String("api", "BUILTIN", "API entry point, for example https://www.example.com/api/") - var conf_grade = flag.Bool("grade", false, "Output only the hostname: grade") - var conf_hostcheck = flag.Bool("hostcheck", false, "If true, host resolution failure will result in a fatal error.") - var conf_hostfile = flag.String("hostfile", "", "File containing hosts to scan (one per line)") - var conf_ignore_mismatch = flag.Bool("ignore-mismatch", false, "If true, certificate hostname mismatch does not stop assessment.") - var conf_insecure = flag.Bool("insecure", false, "Skip certificate validation. For use in development only. Do not use.") - var conf_json_flat = flag.Bool("json-flat", false, "Output results in flattened JSON format") - var conf_quiet = flag.Bool("quiet", false, "Disable status messages (logging)") - var conf_usecache = flag.Bool("usecache", false, "If true, accept cached results (if available), else force live scan.") - var conf_maxage = flag.Int("maxage", 0, "Maximum acceptable age of cached results, in hours. A zero value is ignored.") - var conf_verbosity = flag.String("verbosity", "info", "Configure log verbosity: error, notice, info, debug, or trace.") - var conf_version = flag.Bool("version", false, "Print version and API location information and exit") - - flag.Parse() - - if *conf_version { - fmt.Println(USER_AGENT) - fmt.Println("API location: " + apiLocation) - return - } - - globalIgnoreMismatch = *conf_ignore_mismatch - - if *conf_quiet { - logLevel = LOG_NONE - } else { - logLevel = parseLogLevel(strings.ToLower(*conf_verbosity)) - } - - // We prefer cached results - if *conf_usecache { +func UseCache(useCache bool) { + if useCache { globalFromCache = true globalStartNew = false - } - - if *conf_maxage != 0 { - globalMaxAge = *conf_maxage - } - - // Verify that the API entry point is a URL. - if *conf_api != "BUILTIN" { - apiLocation = *conf_api - } - - if validateURL(apiLocation) == false { - log.Fatalf("[ERROR] Invalid API URL: %v", apiLocation) - } - - var hostnames []string - - if *conf_hostfile != "" { - // Open file, and read it - var err error - hostnames, err = readLines(conf_hostfile) - if err != nil { - log.Fatalf("[ERROR] Reading from specified hostfile failed: %v", err) - } - } else { - // Read hostnames from the rest of the args - hostnames = flag.Args() - } - - if *conf_hostcheck { - // Validate all hostnames before we attempt to test them. At least - // one hostname is required. - for _, host := range hostnames { - if validateHostname(host) == false { - log.Fatalf("[ERROR] Invalid hostname: %v", host) - } - } - } - - if *conf_insecure { - globalInsecure = *conf_insecure + globalFromCache = false + globalStartNew = true } +} - hp := NewHostProvider(hostnames) - manager := NewManager(hp) - - // Respond to events until all the work is done. - for { - _, running := <-manager.FrontendEventChannel - if running == false { - var err error - - if hp.StartingLen == 0 { - return - } - - if *conf_grade { - for i := range manager.results.responses { - results := []byte(manager.results.responses[i]) - - // Fill LabsReport with json response received i.e results - var labsReport LabsReport - err = json.Unmarshal(results, &labsReport) - // Check for error while unmarshalling. If yes then display error messsage and terminate the program - if err != nil { - log.Fatalf("[ERROR] JSON unmarshal error: %v", err) - } - - // Printing the Hostname and IpAddress with grades - fmt.Println() - if !strings.EqualFold(labsReport.StatusMessage, "ERROR") { - fmt.Printf("HostName:\"%v\"\n", labsReport.Host) - for _, endpoints := range labsReport.Endpoints { - if endpoints.FutureGrade != "" { - fmt.Printf("\"%v\":\"%v\"->\"%v\"\n", endpoints.IpAddress, endpoints.Grade, endpoints.FutureGrade) - } else { - if endpoints.Grade != "" { - fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.Grade) - } else { - // When no grade is seen print Status Message - fmt.Printf("\"%v\":\"%v\"\n", endpoints.IpAddress, endpoints.StatusMessage) - } - } - } - } - } - } else if *conf_json_flat { - // Flat JSON and RAW - - for i := range manager.results.responses { - results := []byte(manager.results.responses[i]) - - flattened := flattenAndFormatJSON(results) - - // Print the flattened data - fmt.Println(*flattened) - } - } else { - // Raw (non-Go-mangled) JSON output - - fmt.Println("[") - for i := range manager.results.responses { - results := manager.results.responses[i] - - if i > 0 { - fmt.Println(",") - } - fmt.Println(results) - - } - fmt.Println("]") - } - - if err != nil { - log.Fatalf("[ERROR] Output to JSON failed: %v", err) - } +func SetMaxAge(maxAge int) { + globalMaxAge = maxAge +} - if logLevel >= LOG_INFO { - log.Println("[INFO] All assessments complete; shutting down") - } +func SetAPILocation(apiUrl string) { + apiLocation = apiUrl +} - return - } - } +func AllowInsecure(allowInsecure bool) { + globalInsecure = allowInsecure }