Created
January 30, 2019 16:10
-
-
Save kwilczynski/32213cfee4f9676ecdabaf09b6f7a581 to your computer and use it in GitHub Desktop.
Nessus Scanner Fact - collect and expose Nessus Scanner status to Ansible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
https://localhost | |
tls ./server.pem ./server.key { | |
ca ./ca.pem | |
clients require | |
protocols tls1.0 tls1.1 | |
} | |
root . | |
log stdout | |
browse |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"context" | |
"crypto/tls" | |
"crypto/x509" | |
"encoding/json" | |
"fmt" | |
"io" | |
"io/ioutil" | |
"log" | |
"net" | |
"net/http" | |
"net/http/httputil" | |
"net/url" | |
"os" | |
"strings" | |
"time" | |
"github.com/hashicorp/logutils" | |
kingpin "gopkg.in/alecthomas/kingpin.v2" | |
) | |
const ( | |
version = "0.1.0" | |
userAgent = "Nessus Scanner Fact" | |
defaultLogLevel = "INFO" | |
defaultHost = "localhost" | |
defaultScheme = "https" | |
defaultTimeout = 3 * time.Second | |
) | |
const ( | |
exitReady = iota | |
exitLocked | |
exitError | |
exitUnknown = (1 << 8) - 1 | |
) | |
var ( | |
defaultPaths = []string{ | |
"/server/status", | |
"/server/properties", | |
} | |
validTLSVersions = map[string]int{ | |
"tls1.0": tls.VersionTLS10, | |
"tls1.1": tls.VersionTLS11, | |
"tls1.2": tls.VersionTLS12, | |
} | |
requiredFields = map[string]struct{}{ | |
"code": struct{}{}, | |
"progress": struct{}{}, | |
"status": struct{}{}, | |
} | |
) | |
type config struct { | |
debug *bool | |
trace *bool | |
logLevel *string | |
host *string | |
port *int | |
timeout *int | |
tlsDisable *bool | |
tlsVerify *bool | |
tlsVersion *string | |
tlsCertificate *string | |
tlsKey *string | |
tlsCA *string | |
exitCodeOnly *bool | |
} | |
func main() { | |
cliArgs := os.Args[1:] | |
envArgs := os.Getenv("NESSUS_FACT_ARGUMENTS") | |
if len(cliArgs) < 1 && envArgs != "" { | |
ss := strings.Split(envArgs, ",") | |
for _, s := range ss { | |
cliArgs = append(cliArgs, strings.Split(s, " ")...) | |
} | |
} | |
flags := kingpin.New("nessus.fact", "Nessus Scanner Fact - collect and expose Nessus Scanner status to Ansible.") | |
config := &config{ | |
debug: flags.Flag("debug", "Print diagnostic information.").Bool(), | |
logLevel: flags.Flag("log-level", `Set the log level (defaults to "INFO").`).PlaceHolder("LEVEL").String(), | |
host: flags.Flag("host", `Host to connect to (defaults to "localhost").`).Short('h').String(), | |
port: flags.Flag("port", `Port to use when connecting to a given host (defaults to "443").`).Short('p').Int(), | |
timeout: flags.Flag("timeout", "Number of seconds to wait before timing out connection.").Short('t').PlaceHolder("SECONDS").Int(), | |
tlsDisable: flags.Flag("tls-disable", "Disable TLS support.").Short('T').Bool(), | |
tlsVerify: flags.Flag("tls-verify", "Enable peer TLS cerificate verification (disabled by default).").Bool(), | |
tlsVersion: flags.Flag("tls-version", `Specify which TLS version to use, from either "tls1.0", "tls1.1" or "tls1.2" (defaults to "tls1.2").`).PlaceHolder("VERSION").String(), | |
tlsCertificate: flags.Flag("tls-certificate", "Path to TLS certificate file.").PlaceHolder("PATH").String(), | |
tlsKey: flags.Flag("tls-key", "Path to TLS key file.").PlaceHolder("PATH").String(), | |
tlsCA: flags.Flag("tls-ca", "Path to CA file.").PlaceHolder("PATH").String(), | |
exitCodeOnly: flags.Flag("exit-code-only", "Do not print anything and only use exit code to communicate status.").Short('E').Bool(), | |
} | |
config.trace = &[]bool{false}[0] | |
flags.Version(version) | |
flags.UsageTemplate(customUsageTemplate) | |
kingpin.MustParse(flags.Parse(cliArgs)) | |
filter := &logutils.LevelFilter{ | |
Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"}, | |
MinLevel: logutils.LogLevel(defaultLogLevel), | |
Writer: os.Stderr, | |
} | |
if *config.logLevel == "" { | |
*config.logLevel = os.Getenv("NESSUS_FACT_LOG_LEVEL") | |
} | |
if *config.logLevel == "" { | |
*config.logLevel = defaultLogLevel | |
} | |
*config.logLevel = strings.ToUpper(*config.logLevel) | |
if *config.exitCodeOnly { | |
*config.logLevel = "NONE" | |
} | |
if *config.logLevel == "TRACE" { | |
*config.trace = true | |
} | |
if *config.trace || *config.logLevel == "DEBUG" || os.Getenv("NESSUS_FACT_DEBUG") == "1" { | |
*config.debug = true | |
} | |
if *config.debug { | |
*config.logLevel = "DEBUG" | |
} | |
if *config.trace { | |
*config.logLevel = "TRACE" | |
} | |
validLogLevel := false | |
for _, level := range filter.Levels { | |
if string(level) == *config.logLevel { | |
validLogLevel = true | |
break | |
} | |
} | |
if !validLogLevel { | |
log.SetFlags(0) | |
log.Fatalf( | |
"An invalid log level %q given. See %s --help.", | |
*config.logLevel, | |
os.Args[0], | |
) | |
} | |
filter.SetMinLevel(logutils.LogLevel(*config.logLevel)) | |
log.SetOutput(filter) | |
log.Printf("[DEBUG] Command line arguments: %v\n", cliArgs) | |
scheme := defaultScheme | |
if *config.tlsDisable { | |
scheme = "http" | |
} | |
host := defaultHost | |
if *config.host != "" { | |
host = *config.host | |
} | |
if *config.port > 0 { | |
host = fmt.Sprintf("%s:%d", host, *config.port) | |
} | |
timeout := defaultTimeout | |
if *config.timeout > 0 { | |
timeout = time.Duration(*config.timeout) * time.Second | |
} | |
tlsConfig := &tls.Config{ | |
ServerName: host, | |
PreferServerCipherSuites: true, | |
InsecureSkipVerify: true, | |
MinVersion: tls.VersionTLS12, | |
} | |
if *config.tlsVersion != "" { | |
version, ok := validTLSVersions[*config.tlsVersion] | |
if !ok { | |
log.Fatalf("[FATAL] Unknown TLS version: %s", *config.tlsVersion) | |
} | |
log.Printf("[DEBUG] Using TLS version: %v\n", *config.tlsVersion) | |
tlsConfig.MinVersion = uint16(version) | |
} | |
if *config.tlsCertificate != "" && *config.tlsKey != "" { | |
cert, err := tls.LoadX509KeyPair(*config.tlsCertificate, *config.tlsKey) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to load TLS certificate: %s ", err) | |
} | |
log.Printf( | |
"[DEBUG] Using TLS certificate: %v, key: %v\n", | |
*config.tlsCertificate, | |
*config.tlsKey, | |
) | |
tlsConfig.Certificates = []tls.Certificate{cert} | |
} | |
if *config.tlsCA != "" { | |
ca, err := ioutil.ReadFile(*config.tlsCA) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to load CA certificate: %s", err) | |
} | |
log.Printf( | |
"[DEBUG] Using CA certificate: %v\n", | |
*config.tlsCA, | |
) | |
certPool := x509.NewCertPool() | |
certPool.AppendCertsFromPEM(ca) | |
tlsConfig.RootCAs = certPool | |
} | |
if *config.tlsVerify { | |
log.Println("[DEBUG] Using TLS certificate verification.") | |
tlsConfig.InsecureSkipVerify = false | |
} | |
tlsConfig.BuildNameToCertificate() | |
transport := &http.Transport{ | |
Proxy: http.ProxyFromEnvironment, | |
Dial: (&net.Dialer{ | |
Timeout: timeout, | |
}).Dial, | |
TLSHandshakeTimeout: 5 * time.Second, | |
TLSClientConfig: tlsConfig, | |
} | |
client := &http.Client{ | |
Transport: transport, | |
CheckRedirect: func(*http.Request, []*http.Request) error { | |
return http.ErrUseLastResponse | |
}, | |
} | |
var ( | |
err error | |
dump []byte | |
url *url.URL | |
req *http.Request | |
resp *http.Response | |
) | |
result := make(map[string]interface{}) | |
for _, path := range defaultPaths { | |
url, err = url.Parse(fmt.Sprintf("%s://%s%s", scheme, host, path)) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to parse request URL: %s", err) | |
} | |
log.Printf("[DEBUG] Sending request to: %v\n", url.String()) | |
req, err = http.NewRequest("GET", url.String(), http.NoBody) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to create a request: %s", err) | |
} | |
req.Header.Set("Accept", "*/*") | |
req.Header.Set("Date", time.Now().UTC().Format(time.RFC1123)) | |
req.Header.Set("User-Agent", fmt.Sprintf("%s/%s", userAgent, version)) | |
req.Header.Set("Content-Type", "application/json") | |
if *config.trace { | |
dump, err = httputil.DumpRequestOut(req, true) | |
if err != nil { | |
log.Printf("[TRACE] Unable to dump request: %v\n", err) | |
} | |
log.Printf("[TRACE] Request:\n%v\n", string(dump)) | |
} | |
resp, err = client.Do(req.WithContext( | |
context.Background(), | |
)) | |
if err != nil { | |
log.Fatalf("[FATAL] Sending request failed: %s", err) | |
} | |
if *config.trace { | |
dump, err = httputil.DumpResponse(resp, true) | |
if err != nil { | |
log.Printf("[TRACE] Unable to dump response: %v\n", err) | |
} | |
log.Printf("[TRACE] Response:\n%v\n", string(dump)) | |
} | |
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { | |
var body map[string]interface{} | |
err = json.NewDecoder(resp.Body).Decode(&body) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to decode JSON response: %s", err) | |
} | |
if len(body) < 1 { | |
log.Fatalln("[FATAL] Received empty response body or empty JSON document.") | |
} | |
for k, v := range body { | |
result[k] = v | |
} | |
} | |
err = resp.Body.Close() | |
if err != nil { | |
log.Fatalf("[FATAL] Failed to close response body: %s", err) | |
} | |
} | |
if *config.debug { | |
dump, err = json.MarshalIndent(result, "", " ") | |
if err != nil { | |
log.Printf("[DEBUG] Unable decode JSON document: %v\n", err) | |
} | |
log.Printf("[DEBUG] JSON document from response:\n%v\n", string(dump)) | |
} | |
if len(result) < 1 { | |
log.Fatalln("[FATAL] Received empty JSON document.") | |
} | |
var seen, score int | |
for k := range requiredFields { | |
if _, ok := result[k]; ok { | |
score++ | |
} | |
seen++ | |
} | |
if score < seen { | |
log.Fatalln("[FATAL] Received JSON document is missing key fields.") | |
} | |
if *config.exitCodeOnly { | |
s, ok := result["status"] | |
if !ok { | |
os.Exit(exitUnknown) | |
} | |
switch s.(string) { | |
case "ready": | |
os.Exit(exitReady) | |
case "locked": | |
os.Exit(exitLocked) | |
default: | |
os.Exit(exitUnknown) | |
} | |
} | |
var ( | |
version string | |
build string | |
) | |
if s, ok := result["server_version"]; ok { | |
version = s.(string) | |
} | |
if s, ok := result["nessus_ui_build"]; ok { | |
build = s.(string) | |
} | |
if version != "" && build != "" { | |
result["ops_version"] = fmt.Sprintf("%s-%s", version, build) | |
} | |
var b bytes.Buffer | |
err = json.NewEncoder(&b).Encode(result) | |
if err != nil { | |
log.Fatalf("[FATAL] Unable to encode JSON: %s", err) | |
} | |
_, err = io.Copy(os.Stdout, &b) | |
if err != nil { | |
panic("unreachable") | |
} | |
} | |
var customUsageTemplate = `{{define "FormatUsage"}} | |
{{ if .Help }} | |
{{ .Help | Wrap 0 }}\ | |
{{ end }}\ | |
{{ end }}\ | |
Usage: {{.App.Name }} [<FLAGS> ...]{{ template "FormatUsage" .App }} | |
{{ if .Context.Flags }}\ | |
Flags: | |
{{ .Context.Flags | FlagsToTwoColumns | FormatTwoColumns }} | |
{{ end }}\ | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment