Skip to content

Commit caa6915

Browse files
committed
Add spur
1 parent bd3bc63 commit caa6915

File tree

6 files changed

+254
-0
lines changed

6 files changed

+254
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ it provides both of cli binary and golang API.
6868
- [ipstack](https://ipstack.com/)
6969
- [MaxMind minFraud](https://www.maxmind.com/en/solutions/minfraud-services/)
7070
- [Shodan](https://shodan.io/)
71+
- [Spur](https://spur.us/)
7172

7273

7374
# Quick Usage for binary
@@ -266,3 +267,4 @@ see example dir for more examples.
266267
| `MINFRAUD_ACCOUNT_ID` | [MaxMind Account ID](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
267268
| `MINFRAUD_LICENSE_KEY` | [MaxMind License Key](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
268269
| `FRAUD_CHECK_SHODAN_APIKEY` | [Shodan API Key](https://developer.shodan.io/api/requirements). |
270+
| `FRAUD_CHECK_SPUR_TOKEN` | [spur API token](https://spur.us/products/context-api). |

cmd/providers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/evalphobia/go-ip-fraud-check/provider/ipstack"
2020
"github.com/evalphobia/go-ip-fraud-check/provider/minfraud"
2121
"github.com/evalphobia/go-ip-fraud-check/provider/shodan"
22+
"github.com/evalphobia/go-ip-fraud-check/provider/spur"
2223
)
2324

2425
const (
@@ -36,6 +37,7 @@ const (
3637
providerIPStack = "ipstack"
3738
providerMinFraud = "minfraud"
3839
providerShodan = "shodan"
40+
providerSpur = "spur"
3941
)
4042

4143
var providerMap = map[string]provider.Provider{
@@ -53,6 +55,7 @@ var providerMap = map[string]provider.Provider{
5355
providerIPStack: &ipstack.IPStackProvider{},
5456
providerMinFraud: &minfraud.MinFraudProvider{},
5557
providerShodan: &shodan.ShodanProvider{},
58+
providerSpur: &spur.SpurProvider{},
5659
}
5760

5861
func validateProviderString(s string) error {

ipfraudcheck/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
envIPinfoioToken = "FRAUD_CHECK_IPINFOIO_TOKEN"
2121
envIPStackAPIKey = "FRAUD_CHECK_IPSTACK_APIKEY"
2222
envShodanAPIKey = "FRAUD_CHECK_SHODAN_APIKEY"
23+
envSpurToken = "FRAUD_CHECK_SPUR_TOKEN"
2324
)
2425

2526
const (
@@ -59,6 +60,8 @@ type Config struct {
5960
MinFraudLicenseKey string
6061
// shodan.io
6162
ShodanAPIKey string
63+
// spur.us
64+
SpurToken string
6265

6366
// common option
6467
UseRoute bool
@@ -167,3 +170,11 @@ func (c Config) GetShodanAPIKey() string {
167170
}
168171
return c.ShodanAPIKey
169172
}
173+
174+
func (c Config) GetSpurToken() string {
175+
s := os.Getenv(envSpurToken)
176+
if s != "" {
177+
return s
178+
}
179+
return c.SpurToken
180+
}

provider/spur/client.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package spur
2+
3+
import (
4+
"github.com/evalphobia/go-ip-fraud-check/provider/privateclient"
5+
)
6+
7+
const (
8+
defaultBaseURL = "https://api.spur.us"
9+
)
10+
11+
type Client struct {
12+
privateclient.RESTClient
13+
}
14+
15+
func NewClient(token string) Client {
16+
return Client{
17+
RESTClient: privateclient.RESTClient{
18+
Option: privateclient.Option{
19+
Headers: map[string]string{"Token": token},
20+
BaseURL: defaultBaseURL,
21+
},
22+
},
23+
}
24+
}
25+
26+
func (c *Client) SetDebug(b bool) {
27+
c.RESTClient.Debug = b
28+
}
29+
30+
func (c Client) DoContext(ipaddr string) (Response, error) {
31+
resp := Response{}
32+
err := c.RESTClient.CallGET("/v1/context/"+ipaddr, nil, &resp)
33+
return resp, err
34+
}

provider/spur/provider_spur.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package spur
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/evalphobia/go-ip-fraud-check/ipfraudcheck"
9+
"github.com/evalphobia/go-ip-fraud-check/provider"
10+
)
11+
12+
type SpurProvider struct {
13+
client Client
14+
}
15+
16+
func (p *SpurProvider) Init(conf provider.Config) error {
17+
c, ok := conf.(ipfraudcheck.Config)
18+
if !ok {
19+
return errors.New("incompatible config type for SpurProvider")
20+
}
21+
22+
token := c.GetSpurToken()
23+
if token == "" {
24+
return errors.New("token for spur is empty. you must set directly or use 'FRAUD_CHECK_SPUR_TOKEN' envvar")
25+
}
26+
cli := NewClient(token)
27+
cli.SetDebug(c.Debug)
28+
p.client = cli
29+
return nil
30+
}
31+
32+
func (p SpurProvider) String() string {
33+
return "spur"
34+
}
35+
36+
func (p SpurProvider) CheckIP(ipaddr string) (provider.FraudCheckResponse, error) {
37+
emptyResult := provider.FraudCheckResponse{}
38+
39+
resp, err := p.client.DoContext(ipaddr)
40+
if err != nil {
41+
return emptyResult, err
42+
}
43+
44+
var comments []string
45+
hostingLoc := resp.GeoLite.Country
46+
geop := resp.GeoPrecision
47+
if geop.Exists {
48+
if hostingLoc != geop.Country {
49+
comments = append(comments, fmt.Sprintf("hosting_country=[%s] actual_country=[%s]", hostingLoc, geop.Country))
50+
}
51+
}
52+
if resp.Devices.Estimate >= 25 && !resp.IsMobile() {
53+
comments = append(comments, fmt.Sprintf("estimate_devices=[%d] infra=[%s]", resp.Devices.Estimate, resp.Infrastructure))
54+
}
55+
hasThreat := len(comments) != 0
56+
57+
if resp.IsFileSharing() {
58+
comments = append(comments, "file_sharing")
59+
}
60+
return provider.FraudCheckResponse{
61+
ServiceName: p.String(),
62+
IP: resp.IP,
63+
Organization: resp.AS.Organization,
64+
ASNumber: resp.AS.Number,
65+
Country: resp.GeoLite.Country,
66+
City: resp.GeoLite.City,
67+
Latitude: resp.GeoPrecision.Point.Latitude,
68+
Longitude: resp.GeoPrecision.Point.Longitude,
69+
IsProxy: resp.IsProxy(),
70+
IsVPN: resp.IsVPN(),
71+
IsTor: resp.IsTor(),
72+
IsHosting: resp.IsHosting(),
73+
HasOtherThreat: hasThreat,
74+
ThreatComment: strings.Join(comments, " | "),
75+
}, nil
76+
}
77+
78+
func (p SpurProvider) RawCheckIP(ipaddr string) (interface{}, error) {
79+
return p.client.DoContext(ipaddr)
80+
}

provider/spur/response.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package spur
2+
3+
type Response struct {
4+
IP string `json:"ip"`
5+
Anonymous bool `json:"anonymous"`
6+
AS AS `json:"as"`
7+
Assignment Assignment `json:"assignment"`
8+
DeviceBehaviors DeviceBehaviors `json:"deviceBehaviors"`
9+
Devices Devices `json:"devices"`
10+
GeoLite GeoLite `json:"geoLite"`
11+
GeoPrecision GeoPrecision `json:"geoPrecision"`
12+
Infrastructure string `json:"infrastructure"`
13+
ProxiedTraffic ProxiedTraffic `json:"proxiedTraffic"`
14+
SimilarIPs SimilarIPs `json:"similarIPs"`
15+
VPNOperators VPNOperators `json:"VPNOperators"`
16+
WiFi WiFi `json:"wifi"`
17+
}
18+
19+
func (r Response) IsProxy() bool {
20+
return r.ProxiedTraffic.Exists
21+
}
22+
23+
func (r Response) IsVPN() bool {
24+
return r.VPNOperators.Exists
25+
}
26+
27+
func (r Response) IsHosting() bool {
28+
return r.Infrastructure == "DATACENTER"
29+
}
30+
31+
func (r Response) IsTor() bool {
32+
for _, v := range r.DeviceBehaviors.Behaviors {
33+
if v.Name == "TOR_PROXY_USER" {
34+
return true
35+
}
36+
}
37+
return false
38+
}
39+
40+
func (r Response) IsMobile() bool {
41+
return r.Infrastructure == "MOBILE"
42+
}
43+
44+
func (r Response) IsFileSharing() bool {
45+
for _, v := range r.DeviceBehaviors.Behaviors {
46+
if v.Name == "FILE_SHARING" {
47+
return true
48+
}
49+
}
50+
return false
51+
}
52+
53+
type AS struct {
54+
Number int64 `json:"number"`
55+
Organization string `json:"organization"`
56+
}
57+
58+
type Assignment struct {
59+
Exists bool `json:"exists"`
60+
LastTurnover string `json:"lastTurnover"`
61+
}
62+
63+
type DeviceBehaviors struct {
64+
Exists bool `json:"exists"`
65+
Behaviors []Behavior `json:"behaviors"`
66+
}
67+
68+
type Behavior struct {
69+
Name string `json:"name"`
70+
}
71+
72+
type Devices struct {
73+
Estimate int64 `json:"estimate"`
74+
}
75+
76+
type GeoLite struct {
77+
City string `json:"city"`
78+
Country string `json:"country"`
79+
State string `json:"state"`
80+
}
81+
82+
type GeoPrecision struct {
83+
Exists bool `json:"exists"`
84+
City string `json:"city"`
85+
Country string `json:"country"`
86+
State string `json:"state"`
87+
Hash string `json:"hash"`
88+
Spread string `json:"spread"`
89+
Point Point `json:"point"`
90+
}
91+
92+
type Point struct {
93+
Latitude float64 `json:"latitude"`
94+
Longitude float64 `json:"longitude"`
95+
Radius int64 `json:"radius"`
96+
}
97+
98+
type ProxiedTraffic struct {
99+
Exists bool `json:"exists"`
100+
Proxies []Proxy `json:"proxies"`
101+
}
102+
103+
type Proxy struct {
104+
Name string `json:"name"`
105+
Type string `json:"type"`
106+
}
107+
108+
type SimilarIPs struct {
109+
Exists bool `json:"exists"`
110+
IPs []string `json:"ips"`
111+
}
112+
113+
type VPNOperators struct {
114+
Exists bool `json:"exists"`
115+
Operators []Operator `json:"operators"`
116+
}
117+
118+
type Operator struct {
119+
Name string `json:"name"`
120+
}
121+
122+
type WiFi struct {
123+
Exists bool `json:"exists"`
124+
}

0 commit comments

Comments
 (0)