282 lines
6.9 KiB
Go
282 lines
6.9 KiB
Go
package scan
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"net"
|
|
"net/http"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cloudflare/cfssl/helpers"
|
|
"github.com/cloudflare/cfssl/log"
|
|
"github.com/cloudflare/cfssl/scan/crypto/tls"
|
|
)
|
|
|
|
var (
|
|
// Network is the default network to use.
|
|
Network = "tcp"
|
|
// Dialer is the default dialer to use, with a 1s timeout.
|
|
Dialer = &net.Dialer{Timeout: time.Second}
|
|
// Client is the default HTTP Client.
|
|
Client = &http.Client{Transport: &http.Transport{Dial: Dialer.Dial}}
|
|
// RootCAs defines the default root certificate authorities to be used for scan.
|
|
RootCAs *x509.CertPool
|
|
)
|
|
|
|
// Grade gives a subjective rating of the host's success in a scan.
|
|
type Grade int
|
|
|
|
const (
|
|
// Bad describes a host with serious misconfiguration or vulnerability.
|
|
Bad Grade = iota
|
|
// Warning describes a host with non-ideal configuration that maintains support for Warning clients.
|
|
Warning
|
|
// Good describes host performing the expected state-of-the-art.
|
|
Good
|
|
// Skipped descibes the "grade" of a scan that has been skipped.
|
|
Skipped
|
|
)
|
|
|
|
// String gives the name of the Grade as a string.
|
|
func (g Grade) String() string {
|
|
switch g {
|
|
case Bad:
|
|
return "Bad"
|
|
case Warning:
|
|
return "Warning"
|
|
case Good:
|
|
return "Good"
|
|
case Skipped:
|
|
return "Skipped"
|
|
default:
|
|
return "Invalid"
|
|
}
|
|
}
|
|
|
|
// Output is the result of a scan, to be stored for potential use by later Scanners.
|
|
type Output interface{}
|
|
|
|
// multiscan scans all DNS addresses returned for the host, returning the lowest grade
|
|
// and the concatenation of all the output.
|
|
func multiscan(host string, scan func(string) (Grade, Output, error)) (grade Grade, output Output, err error) {
|
|
domain, port, _ := net.SplitHostPort(host)
|
|
var addrs []string
|
|
addrs, err = net.LookupHost(domain)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
grade = Good
|
|
out := make(map[string]Output)
|
|
|
|
for _, addr := range addrs {
|
|
var g Grade
|
|
var o Output
|
|
|
|
g, o, err = scan(net.JoinHostPort(addr, port))
|
|
if err != nil {
|
|
grade = Bad
|
|
return
|
|
}
|
|
|
|
if g < grade {
|
|
grade = g
|
|
}
|
|
|
|
out[addr] = o
|
|
}
|
|
output = out
|
|
return
|
|
}
|
|
|
|
// Scanner describes a type of scan to perform on a host.
|
|
type Scanner struct {
|
|
// Description describes the nature of the scan to be performed.
|
|
Description string `json:"description"`
|
|
// scan is the function that scans the given host and provides a Grade and Output.
|
|
scan func(string, string) (Grade, Output, error)
|
|
}
|
|
|
|
// Scan performs the scan to be performed on the given host and stores its result.
|
|
func (s *Scanner) Scan(addr, hostname string) (Grade, Output, error) {
|
|
grade, output, err := s.scan(addr, hostname)
|
|
if err != nil {
|
|
log.Debugf("scan: %v", err)
|
|
return grade, output, err
|
|
}
|
|
return grade, output, err
|
|
}
|
|
|
|
// Family defines a set of related scans meant to be run together in sequence.
|
|
type Family struct {
|
|
// Description gives a short description of the scans performed scan/scan_common.goon the host.
|
|
Description string `json:"description"`
|
|
// Scanners is a list of scanners that are to be run in sequence.
|
|
Scanners map[string]*Scanner `json:"scanners"`
|
|
}
|
|
|
|
// FamilySet contains a set of Families to run Scans from.
|
|
type FamilySet map[string]*Family
|
|
|
|
// Default contains each scan Family that is defined
|
|
var Default = FamilySet{
|
|
"Connectivity": Connectivity,
|
|
"TLSHandshake": TLSHandshake,
|
|
"TLSSession": TLSSession,
|
|
"PKI": PKI,
|
|
"Broad": Broad,
|
|
}
|
|
|
|
// ScannerResult contains the result for a single scan.
|
|
type ScannerResult struct {
|
|
Grade string `json:"grade"`
|
|
Output Output `json:"output,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// FamilyResult contains a scan response for a single Family
|
|
type FamilyResult map[string]ScannerResult
|
|
|
|
// A Result contains a ScannerResult along with it's scanner and family names.
|
|
type Result struct {
|
|
Family, Scanner string
|
|
ScannerResult
|
|
}
|
|
|
|
type context struct {
|
|
sync.WaitGroup
|
|
addr, hostname string
|
|
familyRegexp, scannerRegexp *regexp.Regexp
|
|
resultChan chan *Result
|
|
}
|
|
|
|
func newContext(addr, hostname string, familyRegexp, scannerRegexp *regexp.Regexp, numFamilies int) *context {
|
|
ctx := &context{
|
|
addr: addr,
|
|
hostname: hostname,
|
|
familyRegexp: familyRegexp,
|
|
scannerRegexp: scannerRegexp,
|
|
resultChan: make(chan *Result),
|
|
}
|
|
ctx.Add(numFamilies)
|
|
|
|
go func() {
|
|
ctx.Wait()
|
|
close(ctx.resultChan)
|
|
}()
|
|
|
|
return ctx
|
|
}
|
|
|
|
type familyContext struct {
|
|
sync.WaitGroup
|
|
ctx *context
|
|
}
|
|
|
|
func (ctx *context) newfamilyContext(numScanners int) *familyContext {
|
|
familyCtx := &familyContext{ctx: ctx}
|
|
familyCtx.Add(numScanners)
|
|
|
|
go func() {
|
|
familyCtx.Wait()
|
|
familyCtx.ctx.Done()
|
|
}()
|
|
|
|
return familyCtx
|
|
}
|
|
|
|
func (ctx *context) copyResults(timeout time.Duration) map[string]FamilyResult {
|
|
results := make(map[string]FamilyResult)
|
|
for {
|
|
var result *Result
|
|
select {
|
|
case <-time.After(timeout):
|
|
log.Warningf("Scan timed out after %v", timeout)
|
|
return results
|
|
case result = <-ctx.resultChan:
|
|
if result == nil {
|
|
return results
|
|
}
|
|
}
|
|
|
|
if results[result.Family] == nil {
|
|
results[result.Family] = make(FamilyResult)
|
|
}
|
|
results[result.Family][result.Scanner] = result.ScannerResult
|
|
}
|
|
}
|
|
|
|
func (familyCtx *familyContext) runScanner(familyName, scannerName string, scanner *Scanner) {
|
|
if familyCtx.ctx.familyRegexp.MatchString(familyName) && familyCtx.ctx.scannerRegexp.MatchString(scannerName) {
|
|
grade, output, err := scanner.Scan(familyCtx.ctx.addr, familyCtx.ctx.hostname)
|
|
result := &Result{
|
|
familyName,
|
|
scannerName,
|
|
ScannerResult{
|
|
Grade: grade.String(),
|
|
Output: output,
|
|
},
|
|
}
|
|
if err != nil {
|
|
result.Error = err.Error()
|
|
}
|
|
familyCtx.ctx.resultChan <- result
|
|
}
|
|
familyCtx.Done()
|
|
}
|
|
|
|
// RunScans iterates over AllScans, running each scan that matches the family
|
|
// and scanner regular expressions concurrently.
|
|
func (fs FamilySet) RunScans(host, ip, family, scanner string, timeout time.Duration) (map[string]FamilyResult, error) {
|
|
hostname, port, err := net.SplitHostPort(host)
|
|
if err != nil {
|
|
hostname = host
|
|
port = "443"
|
|
}
|
|
|
|
var addr string
|
|
if net.ParseIP(ip) != nil {
|
|
addr = net.JoinHostPort(ip, port)
|
|
} else {
|
|
addr = net.JoinHostPort(hostname, port)
|
|
}
|
|
|
|
familyRegexp, err := regexp.Compile(family)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scannerRegexp, err := regexp.Compile(scanner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx := newContext(addr, hostname, familyRegexp, scannerRegexp, len(fs))
|
|
for familyName, family := range fs {
|
|
familyCtx := ctx.newfamilyContext(len(family.Scanners))
|
|
for scannerName, scanner := range family.Scanners {
|
|
go familyCtx.runScanner(familyName, scannerName, scanner)
|
|
}
|
|
}
|
|
|
|
return ctx.copyResults(timeout), nil
|
|
}
|
|
|
|
// LoadRootCAs loads the default root certificate authorities from file.
|
|
func LoadRootCAs(caBundleFile string) (err error) {
|
|
if caBundleFile != "" {
|
|
log.Debugf("Loading scan RootCAs: %s", caBundleFile)
|
|
RootCAs, err = helpers.LoadPEMCertPool(caBundleFile)
|
|
}
|
|
return
|
|
}
|
|
|
|
func defaultTLSConfig(hostname string) *tls.Config {
|
|
return &tls.Config{
|
|
ServerName: hostname,
|
|
RootCAs: RootCAs,
|
|
InsecureSkipVerify: true,
|
|
}
|
|
}
|