Skip to content

Commit

Permalink
Add spur
Browse files Browse the repository at this point in the history
  • Loading branch information
evalphobia committed Oct 31, 2021
1 parent bd3bc63 commit caa6915
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ it provides both of cli binary and golang API.
- [ipstack](https://ipstack.com/)
- [MaxMind minFraud](https://www.maxmind.com/en/solutions/minfraud-services/)
- [Shodan](https://shodan.io/)
- [Spur](https://spur.us/)


# Quick Usage for binary
Expand Down Expand Up @@ -266,3 +267,4 @@ see example dir for more examples.
| `MINFRAUD_ACCOUNT_ID` | [MaxMind Account ID](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
| `MINFRAUD_LICENSE_KEY` | [MaxMind License Key](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
| `FRAUD_CHECK_SHODAN_APIKEY` | [Shodan API Key](https://developer.shodan.io/api/requirements). |
| `FRAUD_CHECK_SPUR_TOKEN` | [spur API token](https://spur.us/products/context-api). |
3 changes: 3 additions & 0 deletions cmd/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/evalphobia/go-ip-fraud-check/provider/ipstack"
"github.com/evalphobia/go-ip-fraud-check/provider/minfraud"
"github.com/evalphobia/go-ip-fraud-check/provider/shodan"
"github.com/evalphobia/go-ip-fraud-check/provider/spur"
)

const (
Expand All @@ -36,6 +37,7 @@ const (
providerIPStack = "ipstack"
providerMinFraud = "minfraud"
providerShodan = "shodan"
providerSpur = "spur"
)

var providerMap = map[string]provider.Provider{
Expand All @@ -53,6 +55,7 @@ var providerMap = map[string]provider.Provider{
providerIPStack: &ipstack.IPStackProvider{},
providerMinFraud: &minfraud.MinFraudProvider{},
providerShodan: &shodan.ShodanProvider{},
providerSpur: &spur.SpurProvider{},
}

func validateProviderString(s string) error {
Expand Down
11 changes: 11 additions & 0 deletions ipfraudcheck/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const (
envIPinfoioToken = "FRAUD_CHECK_IPINFOIO_TOKEN"
envIPStackAPIKey = "FRAUD_CHECK_IPSTACK_APIKEY"
envShodanAPIKey = "FRAUD_CHECK_SHODAN_APIKEY"
envSpurToken = "FRAUD_CHECK_SPUR_TOKEN"
)

const (
Expand Down Expand Up @@ -59,6 +60,8 @@ type Config struct {
MinFraudLicenseKey string
// shodan.io
ShodanAPIKey string
// spur.us
SpurToken string

// common option
UseRoute bool
Expand Down Expand Up @@ -167,3 +170,11 @@ func (c Config) GetShodanAPIKey() string {
}
return c.ShodanAPIKey
}

func (c Config) GetSpurToken() string {
s := os.Getenv(envSpurToken)
if s != "" {
return s
}
return c.SpurToken
}
34 changes: 34 additions & 0 deletions provider/spur/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package spur

import (
"github.com/evalphobia/go-ip-fraud-check/provider/privateclient"
)

const (
defaultBaseURL = "https://api.spur.us"
)

type Client struct {
privateclient.RESTClient
}

func NewClient(token string) Client {
return Client{
RESTClient: privateclient.RESTClient{
Option: privateclient.Option{
Headers: map[string]string{"Token": token},
BaseURL: defaultBaseURL,
},
},
}
}

func (c *Client) SetDebug(b bool) {
c.RESTClient.Debug = b
}

func (c Client) DoContext(ipaddr string) (Response, error) {
resp := Response{}
err := c.RESTClient.CallGET("/v1/context/"+ipaddr, nil, &resp)
return resp, err
}
80 changes: 80 additions & 0 deletions provider/spur/provider_spur.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package spur

import (
"errors"
"fmt"
"strings"

"github.com/evalphobia/go-ip-fraud-check/ipfraudcheck"
"github.com/evalphobia/go-ip-fraud-check/provider"
)

type SpurProvider struct {
client Client
}

func (p *SpurProvider) Init(conf provider.Config) error {
c, ok := conf.(ipfraudcheck.Config)
if !ok {
return errors.New("incompatible config type for SpurProvider")
}

token := c.GetSpurToken()
if token == "" {
return errors.New("token for spur is empty. you must set directly or use 'FRAUD_CHECK_SPUR_TOKEN' envvar")
}
cli := NewClient(token)
cli.SetDebug(c.Debug)
p.client = cli
return nil
}

func (p SpurProvider) String() string {
return "spur"
}

func (p SpurProvider) CheckIP(ipaddr string) (provider.FraudCheckResponse, error) {
emptyResult := provider.FraudCheckResponse{}

resp, err := p.client.DoContext(ipaddr)
if err != nil {
return emptyResult, err
}

var comments []string
hostingLoc := resp.GeoLite.Country
geop := resp.GeoPrecision
if geop.Exists {
if hostingLoc != geop.Country {
comments = append(comments, fmt.Sprintf("hosting_country=[%s] actual_country=[%s]", hostingLoc, geop.Country))
}
}
if resp.Devices.Estimate >= 25 && !resp.IsMobile() {
comments = append(comments, fmt.Sprintf("estimate_devices=[%d] infra=[%s]", resp.Devices.Estimate, resp.Infrastructure))
}
hasThreat := len(comments) != 0

if resp.IsFileSharing() {
comments = append(comments, "file_sharing")
}
return provider.FraudCheckResponse{
ServiceName: p.String(),
IP: resp.IP,
Organization: resp.AS.Organization,
ASNumber: resp.AS.Number,
Country: resp.GeoLite.Country,
City: resp.GeoLite.City,
Latitude: resp.GeoPrecision.Point.Latitude,
Longitude: resp.GeoPrecision.Point.Longitude,
IsProxy: resp.IsProxy(),
IsVPN: resp.IsVPN(),
IsTor: resp.IsTor(),
IsHosting: resp.IsHosting(),
HasOtherThreat: hasThreat,
ThreatComment: strings.Join(comments, " | "),
}, nil
}

func (p SpurProvider) RawCheckIP(ipaddr string) (interface{}, error) {
return p.client.DoContext(ipaddr)
}
124 changes: 124 additions & 0 deletions provider/spur/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package spur

type Response struct {
IP string `json:"ip"`
Anonymous bool `json:"anonymous"`
AS AS `json:"as"`
Assignment Assignment `json:"assignment"`
DeviceBehaviors DeviceBehaviors `json:"deviceBehaviors"`
Devices Devices `json:"devices"`
GeoLite GeoLite `json:"geoLite"`
GeoPrecision GeoPrecision `json:"geoPrecision"`
Infrastructure string `json:"infrastructure"`
ProxiedTraffic ProxiedTraffic `json:"proxiedTraffic"`
SimilarIPs SimilarIPs `json:"similarIPs"`
VPNOperators VPNOperators `json:"VPNOperators"`
WiFi WiFi `json:"wifi"`
}

func (r Response) IsProxy() bool {
return r.ProxiedTraffic.Exists
}

func (r Response) IsVPN() bool {
return r.VPNOperators.Exists
}

func (r Response) IsHosting() bool {
return r.Infrastructure == "DATACENTER"
}

func (r Response) IsTor() bool {
for _, v := range r.DeviceBehaviors.Behaviors {
if v.Name == "TOR_PROXY_USER" {
return true
}
}
return false
}

func (r Response) IsMobile() bool {
return r.Infrastructure == "MOBILE"
}

func (r Response) IsFileSharing() bool {
for _, v := range r.DeviceBehaviors.Behaviors {
if v.Name == "FILE_SHARING" {
return true
}
}
return false
}

type AS struct {
Number int64 `json:"number"`
Organization string `json:"organization"`
}

type Assignment struct {
Exists bool `json:"exists"`
LastTurnover string `json:"lastTurnover"`
}

type DeviceBehaviors struct {
Exists bool `json:"exists"`
Behaviors []Behavior `json:"behaviors"`
}

type Behavior struct {
Name string `json:"name"`
}

type Devices struct {
Estimate int64 `json:"estimate"`
}

type GeoLite struct {
City string `json:"city"`
Country string `json:"country"`
State string `json:"state"`
}

type GeoPrecision struct {
Exists bool `json:"exists"`
City string `json:"city"`
Country string `json:"country"`
State string `json:"state"`
Hash string `json:"hash"`
Spread string `json:"spread"`
Point Point `json:"point"`
}

type Point struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Radius int64 `json:"radius"`
}

type ProxiedTraffic struct {
Exists bool `json:"exists"`
Proxies []Proxy `json:"proxies"`
}

type Proxy struct {
Name string `json:"name"`
Type string `json:"type"`
}

type SimilarIPs struct {
Exists bool `json:"exists"`
IPs []string `json:"ips"`
}

type VPNOperators struct {
Exists bool `json:"exists"`
Operators []Operator `json:"operators"`
}

type Operator struct {
Name string `json:"name"`
}

type WiFi struct {
Exists bool `json:"exists"`
}

0 comments on commit caa6915

Please sign in to comment.