diff --git a/check/onyphe.go b/check/onyphe.go new file mode 100644 index 0000000..2d7bf90 --- /dev/null +++ b/check/onyphe.go @@ -0,0 +1,148 @@ +package check + +import ( + "encoding/json" + "fmt" + "net" + "sort" + "strings" + "strconv" + + "github.com/jreisinger/checkip" +) + +type onyphe struct { + Text string `json:"text"` + Vulns []string `json:"vulns"` + Results onypheData `json:"results"` +} + +type onypheData []struct { + OS string `json:"os"` + OsVendor string `json:"osvendor"` + //Version string `json:"version"` + Port interface{} `json:"port"` + Protocol string `json:"protocol"` + Product string `json:"product"` + Transport string `json:"transport"` // tcp, udp +} + +var onypheUrl = "https://www.onyphe.io/api/v2" + +// Onyphe gets generic information from api.onyphe.io. +func Onyphe(ipaddr net.IP) (checkip.Result, error) { + result := checkip.Result{ + Name: "onyphe.io", + Type: checkip.TypeInfoSec, + } + + apiKey, err := getConfigValue("ONYPHE_API_KEY") + if err != nil { + return result, newCheckError(err) + } + if apiKey == "" { + return result, nil + } + + headers := map[string]string{ + "Authorization": "bearer " + apiKey, + "Accept": "application/json", + //"Content-Type": "application/x-www-form-urlencoded", + } + var onyphe onyphe + apiURL := fmt.Sprintf("%s/simple/datascan/%s", onypheUrl, ipaddr) + if err := defaultHttpClient.GetJson(apiURL, headers, map[string]string{}, &onyphe); err != nil { + return result, newCheckError(err) + } + + for _, d := range onyphe.Results { + var port string + switch v := d.Port.(type) { + case float64: + port = fmt.Sprintf("%d", int(v)) + case string: + port = v + } + if port != "80" && port != "443" && port != "53" { // undecidable ports + result.Malicious = true + } + } + + result.Info = onyphe + + return result, nil +} + +type byPortO onypheData + +func (x byPortO) Len() int { return len(x) } +func (x byPortO) Less(i, j int) bool { + var portI float64 + var portJ float64 + switch v := x[i].Port.(type) { + case float64: + portI = v + case string: + portI, _ = strconv.ParseFloat(v, 64) + } + switch v := x[j].Port.(type) { + case float64: + portJ = v + case string: + portJ, _ = strconv.ParseFloat(v, 64) + } + return portI < portJ +} + +func (x byPortO) Swap(i, j int) { x[i], x[j] = x[j], x[i] } + +// Info returns interesting information from the check. +func (o onyphe) Summary() string { + var portInfo []string + service := make(map[string]int) + sort.Sort(byPortO(o.Results)) + for _, d := range o.Results { + var os string + if d.OS != "" { + os = d.OS + } + + var osvendor string + if d.OsVendor != "" { + osvendor = d.OsVendor + } + + var product string + if d.Product != "" { + product = d.Product + " " + } + + var port string + switch v := d.Port.(type) { + case float64: + port = fmt.Sprintf("%d", int(v)) + case string: + port = v + } + + sport := fmt.Sprintf("%s/%s", d.Transport, port) + service[sport]++ + + if service[sport] > 1 { + continue + } + + if os == "" && osvendor == "" { + portInfo = append(portInfo, fmt.Sprintf("%s %s/%s", d.Protocol, d.Transport, port)) + } else { + ss := nonEmpty(os, osvendor) + portInfo = append(portInfo, fmt.Sprintf("%s %s/%s %s(%s)", d.Protocol, d.Transport, port, product, strings.Join(ss, ", "))) + } + } + + return fmt.Sprintf("Open: %s", strings.Join(portInfo, ", ")) +} + +func (o onyphe) Json() ([]byte, error) { + return json.Marshal(o) +} diff --git a/check/onyphe_test.go b/check/onyphe_test.go new file mode 100644 index 0000000..0726af7 --- /dev/null +++ b/check/onyphe_test.go @@ -0,0 +1,63 @@ +package check + +import ( + "net" + "net/http" + "testing" + + "github.com/jreisinger/checkip" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOnyphe(t *testing.T) { + + apiKey, err := getConfigValue("CENSYS_KEY") + if err != nil || apiKey == "" { + return + } + apiSec, err := getConfigValue("CENSYS_SEC") + if err != nil || apiSec == "" { + return + } + + t.Run("given valid response then result and no error is returned", func(t *testing.T) { + handlerFn := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write(loadResponse(t, "onyphe_response.json")) + }) + + testUrl := setMockHttpClient(t, handlerFn) + setOnypheUrl(t, testUrl) + + result, err := Onyphe(net.ParseIP("118.25.6.39")) + require.NoError(t, err) + assert.Equal(t, "onyphe.io", result.Name) + assert.Equal(t, checkip.TypeInfoSec, result.Type) + assert.Equal(t, true, result.Malicious) + assert.Equal(t, "Open: snmp udp/161 (RouterOS, Mikrotik), winbox tcp/8291 (Linux Kernel, Linux)", result.Info.Summary()) + }) + + + t.Run("given non 2xx response then error is returned", func(t *testing.T) { + handlerFn := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusInternalServerError) + }) + + testUrl := setMockHttpClient(t, handlerFn) + setOnypheUrl(t, testUrl) + + _, err := Onyphe(net.ParseIP("118.25.6.39")) + require.Error(t, err) + }) +} + +// --- test helpers --- + +func setOnypheUrl(t *testing.T, testUrl string) { + url := onypheUrl + onypheUrl = testUrl + t.Cleanup(func() { + onypheUrl = url + }) +} diff --git a/check/testdata/onyphe_response.json b/check/testdata/onyphe_response.json new file mode 100644 index 0000000..4e5d7a5 --- /dev/null +++ b/check/testdata/onyphe_response.json @@ -0,0 +1,198 @@ +{ + "count": 3, + "error": 0, + "max_page": 1, + "myip": "192.168.1.3", + "page": 1, + "page_size": 3, + "results": [ + { + "@category": "datascan", + "@timestamp": "2024-06-03T00:09:07.000Z", + "app": { + "length": 20 + }, + "asn": "AS0", + "city": "Fortaleza", + "country": "BR", + "data": "", + "datamd5": "", + "datammh3": 0, + "domain": [ + "example.org" + ], + "geolocus": { + "asn": "AS0", + "continent": "SA", + "continentname": "South America", + "country": "BR", + "countryname": "Brazil", + "domain": [ + "cert.br", + "example.org" + ], + "isineu": "false", + "latitude": "-14.000000", + "location": "-14.000000,-51.00000", + "longitude": "-51.00000", + "netname": "", + "organization": "SERVICOS DE TELECOM", + "subnet": "10.20.64.0/18" + }, + "host": [ + "10.20-84-242" + ], + "hostname": [ + "10.20-84-242.example.org" + ], + "ip": "10.20.84.242", + "ipv6": "false", + "latitude": "-3.0000", + "location": "-3.0000,-38.0000", + "longitude": "-38.0000", + "node": { + }, + "organization": "SERVICOS DE TELECOM", + "os": "Linux Kernel", + "osvendor": "Linux", + "port": 8291, + "protocol": "winbox", + "reverse": [ + "10.20-84-242.example.org" + ], + "seen_date": "2024-06-03", + "source": "datascan", + "subnet": "10.20.64.0/18", + "tld": [ + "com.br" + ], + "tls": "false", + "transport": "tcp" + }, + { + "@category": "datascan", + "@timestamp": "2024-05-08T09:54:12.000Z", + "app": { + "length": 20 + }, + "asn": "AS0", + "city": "Fortaleza", + "country": "BR", + "data": "", + "datamd5": "", + "datammh3": 0, + "domain": [ + "example.org" + ], + "geolocus": { + "asn": "AS0", + "continent": "SA", + "continentname": "South America", + "country": "BR", + "countryname": "Brazil", + "domain": [ + "cert.br", + "example.org" + ], + "isineu": "false", + "latitude": "-14.000000", + "location": "-14.000000,-51.00000", + "longitude": "-51.00000", + "netname": "", + "organization": "SERVICOS DE TELECOM", + "subnet": "10.20.64.0/18" + }, + "host": [ + "10.20-84-242" + ], + "hostname": [ + "10.20-84-242.example.org" + ], + "ip": "10.20.84.242", + "ipv6": "false", + "latitude": "-3.0000", + "location": "-3.0000,-38.0000", + "longitude": "-38.0000", + "node": { + }, + "organization": "SERVICOS DE TELECOM", + "os": "Linux Kernel", + "osvendor": "Linux", + "port": 8291, + "protocol": "winbox", + "reverse": [ + "10.20-84-242.example.org" + ], + "seen_date": "2024-05-08", + "source": "datascan", + "subnet": "10.20.64.0/18", + "tld": [ + "com.br" + ], + "tls": "false", + "transport": "tcp" + }, + { + "@category": "datascan", + "@timestamp": "2024-05-07T12:19:10.000Z", + "app": { + "length": "60" + }, + "asn": "AS0", + "city": "Fortaleza", + "country": "BR", + "data": "", + "datamd5": "", + "device": { + }, + "domain": [ + "example.org" + ], + "geolocus": { + "asn": "AS0", + "continent": "SA", + "continentname": "South America", + "country": "BR", + "countryname": "Brazil", + "isineu": "false", + "latitude": "-14.000000", + "location": "-14.000000,-51.00000", + "longitude": "-51.00000", + "netname": "", + "organization": "SERVICOS DE TELECOM", + "subnet": "10.20.64.0/18" + }, + "host": [ + "10.20-84-242" + ], + "hostname": [ + "10.20-84-242.example.org" + ], + "ip": "10.20.84.242", + "ipv6": "false", + "latitude": "-3.0000", + "location": "-3.0000,-38.0000", + "longitude": "-38.0000", + "organization": "SERVICOS DE TELECOM", + "os": "RouterOS", + "osvendor": "Mikrotik", + "port": "161", + "protocol": "snmp", + "reverse": [ + "10.20-84-242.example.org" + ], + "seen_date": "2024-05-07", + "source": "udpscan", + "subnet": "10.20.64.0/18", + "tld": [ + "com.br" + ], + "tls": "false", + "transport": "udp" + } + ], + "status": "ok", + "text": "Success", + "took": 0.119, + "total": 3 +}