diff --git a/Makefile b/Makefile index 03c472c..d195149 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ install: go install cmd/checkip.go run: install - checkip 140.82.114.4 + checkip 91.228.166.47 + checkip 209.141.33.65 checkip 218.92.0.158 - checkip 92.118.160.17 - checkip -j 92.118.160.17 | jq -r '.[] | select(.Type=="Sec") | "\(.Name) => \(.IsMalicious)"' + checkip -j 218.92.0.158 | jq -r '.[] | select(.Type=="Sec" or .Type=="InfoSec") | "\(.Name) => \(.IsMalicious)"' PLATFORMS := linux/amd64 darwin/amd64 linux/arm windows/amd64 diff --git a/README.md b/README.md index 03be9d8..0a6db55 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # checkip `checkip` is a CLI tool and library that checks an IP address using various -public services. +public services. It provides generic and security information. - + ## Installation and configuration diff --git a/abuseipdb.go b/abuseipdb.go index 8cc9c35..bcaded1 100644 --- a/abuseipdb.go +++ b/abuseipdb.go @@ -70,3 +70,7 @@ func (a *AbuseIPDB) Check(ipaddr net.IP) error { func (a *AbuseIPDB) IsMalicious() bool { return a.Data.TotalReports > 0 && !a.Data.IsWhitelisted && a.Data.AbuseConfidenceScore > 25 } + +func (a *AbuseIPDB) Info() string { + return fmt.Sprintf("domain: %s, usage type: %s", na(a.Data.Domain), na(a.Data.UsageType)) +} diff --git a/as.go b/as.go index c6aa682..78cde15 100644 --- a/as.go +++ b/as.go @@ -41,7 +41,7 @@ func (a *AS) Check(ipaddr net.IP) error { // Info returns interesting information from the check. func (a *AS) Info() string { - return a.Description + return fmt.Sprintf("AS description: %s", na(a.Description)) } // search searches the ippadrr in tsvFile and if found fills in AS data. diff --git a/checkip.go b/checkip.go index 270d2c0..b85ac9c 100644 --- a/checkip.go +++ b/checkip.go @@ -35,6 +35,11 @@ type SecChecker interface { Checker } +type InfoSecChecker interface { + InfoChecker + SecChecker +} + // Result holds the result of a check. type Result struct { Name string @@ -61,6 +66,9 @@ func Run(checkers []Checker, ipaddr net.IP) []Result { errMsg = redactSecrets(err.Error()) } switch v := c.(type) { + case InfoSecChecker: + r := Result{Name: v.String(), Type: "InfoSec", Data: v, Info: v.Info(), IsMalicious: v.IsMalicious(), Err: err, ErrMsg: errMsg} + res = append(res, r) case InfoChecker: r := Result{Name: v.String(), Type: "Info", Data: v, Info: v.Info(), Err: err, ErrMsg: errMsg} res = append(res, r) @@ -76,11 +84,6 @@ func Run(checkers []Checker, ipaddr net.IP) []Result { return res } -func redactSecrets(s string) string { - key := regexp.MustCompile(`(key|pass|password)=\w+`) - return key.ReplaceAllString(s, "${1}=REDACTED") -} - type byName []Result func (x byName) Len() int { return len(x) } @@ -96,14 +99,15 @@ func Print(results []Result) error { if r.Err != nil { log.Print(r.ErrMsg) } - if r.Type == "Info" { + if r.Type == "Info" || r.Type == "InfoSec" { fmt.Printf("%-15s %s\n", r.Name, r.Info) - continue } - if r.IsMalicious { - malicious++ + if r.Type == "Sec" || r.Type == "InfoSec" { + totalSec++ + if r.IsMalicious { + malicious++ + } } - totalSec++ } probabilityMalicious := malicious / totalSec @@ -128,3 +132,25 @@ func PrintJSON(results []Result) error { enc := json.NewEncoder(os.Stdout) return enc.Encode(&results) } + +func redactSecrets(s string) string { + key := regexp.MustCompile(`(key|pass|password)=\w+`) + return key.ReplaceAllString(s, "${1}=REDACTED") +} + +func na(s string) string { + if s == "" { + return "n/a" + } + return s +} + +func nonEmpty(strings ...string) []string { + var ss []string + for _, s := range strings { + if s != "" { + ss = append(ss, s) + } + } + return ss +} diff --git a/checkip.png b/checkip.png index c1ac511..1f3e744 100644 Binary files a/checkip.png and b/checkip.png differ diff --git a/cmd/checkip.go b/cmd/checkip.go index df148f2..12e4871 100755 --- a/cmd/checkip.go +++ b/cmd/checkip.go @@ -35,9 +35,7 @@ func main() { &checkip.Blocklist{}, &checkip.CINSArmy{}, &checkip.DNS{}, - &checkip.ET{}, &checkip.Geo{}, - &checkip.IP{}, &checkip.IPsum{}, &checkip.OTX{}, &checkip.Shodan{}, diff --git a/dns.go b/dns.go index 597b53e..ab57ba5 100644 --- a/dns.go +++ b/dns.go @@ -1,6 +1,7 @@ package checkip import ( + "fmt" "net" "strings" ) @@ -23,5 +24,9 @@ func (d *DNS) Check(ipaddr net.IP) error { // Info returns interesting information from the check. func (d *DNS) Info() string { - return strings.Join(d.Names, ", ") + msg := "DNS name" + if len(d.Names) > 1 { + msg += "s" + } + return fmt.Sprintf("%s: %s", msg, na(strings.Join(d.Names, ", "))) } diff --git a/et.go b/et.go deleted file mode 100644 index 9674e78..0000000 --- a/et.go +++ /dev/null @@ -1,61 +0,0 @@ -package checkip - -import ( - "bufio" - "fmt" - "net" - "os" -) - -// ET (Emerging Threats) holds information about an IP address from -// rules.emergingthreats.net -type ET struct { - CompromisedIP bool - CountIPs int -} - -func (e *ET) String() string { return "rules.emergingthreats.net" } - -// Check checks whether the ippaddr is not among compromised IP addresses from -// The Emerging Threats Intelligence feed (ET). I found ET mentioned at -// https://logz.io/blog/open-source-threat-intelligence-feeds/ -func (e *ET) Check(ipaddr net.IP) error { - file := "/var/tmp/et.txt" - url := "https://rules.emergingthreats.net/blockrules/compromised-ips.txt" - - if err := updateFile(file, url, ""); err != nil { - return fmt.Errorf("can't update %s from %s: %v", file, url, err) - } - - if err := e.search(ipaddr, file); err != nil { - return fmt.Errorf("searching %s in %s: %v", ipaddr, file, err) - } - - return nil -} - -func (e *ET) IsMalicious() bool { - return e.CompromisedIP -} - -// search searches the ippadrr in filename fills in ET data. -func (e *ET) search(ipaddr net.IP, filename string) error { - file, err := os.Open(filename) - if err != nil { - return err - } - - s := bufio.NewScanner(file) - for s.Scan() { - line := s.Text() - e.CountIPs++ - if line == ipaddr.String() { - e.CompromisedIP = true - } - } - if s.Err() != nil { - return err - } - - return nil -} diff --git a/geo.go b/geo.go index 3f0ca41..1562ac9 100644 --- a/geo.go +++ b/geo.go @@ -49,14 +49,5 @@ func (g *Geo) Check(ip net.IP) error { // Info returns interesting information from the check. func (g *Geo) Info() string { - if g.City == "" { - g.City = "city unknown" - } - if g.Country == "" { - g.Country = "country unknown" - } - if g.IsoCode == "" { - g.IsoCode = "ISO code unknown" - } - return fmt.Sprintf("%s, %s (%s)", g.City, g.Country, g.IsoCode) + return fmt.Sprintf("city: %s, country: %s, ISO code: %s", na(g.City), na(g.Country), na(g.IsoCode)) } diff --git a/ip.go b/ip.go deleted file mode 100644 index ae62e20..0000000 --- a/ip.go +++ /dev/null @@ -1,36 +0,0 @@ -package checkip - -import ( - "fmt" - "net" - "strconv" - "strings" -) - -// IP holds information from net.IP. -type IP struct { - Private bool - DefaultMask net.IPMask -} - -func (i *IP) String() string { return "net.IP" } - -// Check fills in IP data. -func (i *IP) Check(ipaddr net.IP) error { - i.Private = ipaddr.IsPrivate() - i.DefaultMask = ipaddr.DefaultMask() - return nil -} - -// Info returns interesting information from the check. -func (i *IP) Info() string { - private := "RFC 1918 private" - if !i.Private { - private = "not " + private - } - var mask []string - for _, b := range i.DefaultMask { - mask = append(mask, strconv.Itoa(int(b))) - } - return fmt.Sprintf("%s, default mask %s", private, strings.Join(mask, ".")) -} diff --git a/shodan.go b/shodan.go index f8f6afe..068c2c0 100644 --- a/shodan.go +++ b/shodan.go @@ -13,7 +13,7 @@ import ( type Shodan struct { Org string `json:"org"` Data data `json:"data"` - Os string `json:"os"` + OS string `json:"os"` Ports []int `json:"ports"` } @@ -61,11 +61,6 @@ func (x byPort) Swap(i, j int) { x[i], x[j] = x[j], x[i] } // Info returns interesting information from the check. func (s *Shodan) Info() string { - os := "OS unknown" - if s.Os != "" { - os = s.Os - } - var portInfo []string sort.Sort(byPort(s.Data)) for _, d := range s.Data { @@ -82,7 +77,8 @@ func (s *Shodan) Info() string { if product == "" && version == "" { portInfo = append(portInfo, fmt.Sprintf("%s/%d", d.Transport, d.Port)) } else { - portInfo = append(portInfo, fmt.Sprintf("%s/%d (%s, %s)", d.Transport, d.Port, product, version)) + ss := nonEmpty(product, version) + portInfo = append(portInfo, fmt.Sprintf("%s/%d (%s)", d.Transport, d.Port, strings.Join(ss, ", "))) } } @@ -94,5 +90,5 @@ func (s *Shodan) Info() string { portStr += ":" } - return fmt.Sprintf("%s, %d open %s %s", os, len(portInfo), portStr, strings.Join(portInfo, ", ")) + return fmt.Sprintf("OS: %s, %d open %s %s", na(s.OS), len(portInfo), portStr, strings.Join(portInfo, ", ")) } diff --git a/virustotal.go b/virustotal.go index a1da7c0..84edb63 100644 --- a/virustotal.go +++ b/virustotal.go @@ -5,13 +5,16 @@ import ( "fmt" "net" "net/http" + "strings" ) // VirusTotal holds information about an IP address from virustotal.com. type VirusTotal struct { Data struct { Attributes struct { - Reputation int `json:"reputation"` + Reputation int `json:"reputation"` + Network string `json:"network"` + ASowner string `json:"as_owner"` LastAnalysisStats struct { Harmless int `json:"harmless"` Malicious int `json:"malicious"` @@ -19,6 +22,11 @@ type VirusTotal struct { Timeout int `json:"timeout"` Undetected int `json:"undetected"` } `json:"last_analysis_stats"` + LastHTTPScert struct { + Extensions struct { + SAN []string `json:"subject_alternative_name"` + } `json:"extensions"` + } `json:"last_https_certificate"` } `json:"attributes"` } `json:"data"` } @@ -58,3 +66,7 @@ func (vt *VirusTotal) IsMalicious() bool { // https://developers.virustotal.com/reference#ip-object return vt.Data.Attributes.Reputation < 0 } + +func (vt *VirusTotal) Info() string { + return fmt.Sprintf("AS onwer: %s, network: %s, SAN: %s", na(vt.Data.Attributes.ASowner), na(vt.Data.Attributes.Network), na(strings.Join(vt.Data.Attributes.LastHTTPScert.Extensions.SAN, ", "))) +}