Skip to content

Commit 4d626c6

Browse files
committed
Add Shodan
1 parent 3d6db3d commit 4d626c6

File tree

19 files changed

+660
-63
lines changed

19 files changed

+660
-63
lines changed

README.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ it provides both of cli binary and golang API.
6262
- [IPQualityScore](https://www.ipqualityscore.com/)
6363
- [ipregistry.co](https://ipregistry.co/)
6464
- [MaxMind minFraud](https://www.maxmind.com/en/solutions/minfraud-services/)
65+
- [Shodan](https://sodan.io/)
66+
6567

6668
# Quick Usage for binary
6769

@@ -132,12 +134,15 @@ Exec api call of ip address fraud check providers from csv list file
132134

133135
Options:
134136

135-
-h, --help display help information
136-
-p, --provider *set types of api provider (space separated) --provider='ipdata ipinfo minfraud'
137-
-i, --input *input csv/tsv file path --input='./input.csv'
138-
-o, --output *output tsv file path --output='./output.tsv'
139-
--route set if you need route data from IRR (this might be slow) --route
140-
--debug set if you use HTTP debug feature --debug
137+
-h, --help display help information
138+
-p, --provider *set types of api provider (space separated) --provider='ipdata ipinfo minfraud'
139+
-i, --input *input csv/tsv file path --input='./input.csv'
140+
-o, --output *output tsv file path --output='./output.tsv'
141+
--route set if you need route data from IRR (this might be slow) --route
142+
--interval time interval after a API call to handle rate limit (ms=msec s=sec, m=min) --interval=1.5s
143+
-m, --parallel[=2] parallel number (multiple API calls) --parallel=2
144+
-v, --verbose set if you need detail logs --verbose
145+
--debug set if you use HTTP debug feature --debug
141146
```
142147
143148
For example, you can check ip address from csv list like below
@@ -171,6 +176,21 @@ ipdata.co 8.8.4.4 0.00000 Google LLC 15169 US 0.00000 0.00000 false false fa
171176
ipinfo.io 8.8.4.4 dns.google 0.00000 Google LLC 15169 US Mountain View California 37.40560 -122.07750 false false false false false false false false
172177
ipdata.co 1.1.1.1 0.00000 Cloudflare, Inc. 13335 AU 0.00000 0.00000 false false false false false false false false
173178
ipinfo.io 1.1.1.1 one.one.one.one 0.00000 Cloudflare, Inc. 13335 US San Francisco California37.76210 -122.39710 false false true false false false false false
179+
180+
181+
182+
# if provider has a rate limit, then use --interval and --parallel option.
183+
$ ./go-ip-fraud-check list -p 'shodan' -i ./input.csv -o ./output.tsv --interval=1.2s --parallel=1
184+
```
185+
186+
187+
### providers command
188+
189+
`providers` command is used to see supported providers.
190+
191+
```bash
192+
$ ./go-ip-fraud-check providers
193+
[bigdatacloud ip2proxy ipdata ipgeolocation ipinfo ipqualityscore ipregistry minfraud shodan]
174194
```
175195
176196
# Quick Usage for API
@@ -235,3 +255,4 @@ see example dir for more examples.
235255
| `IPREGISTRY_APIKEY` | [Ipregistry API Key](https://ipregistry.co/docs/authentication). |
236256
| `MINFRAUD_ACCOUNT_ID` | [MaxMind Account ID](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
237257
| `MINFRAUD_LICENSE_KEY` | [MaxMind License Key](https://support.maxmind.com/account-faq/license-keys/how-do-i-generate-a-license-key/). |
258+
| `FRAUD_CHECK_SHODAN_APIKEY` | [Shodan API Key](https://developer.shodan.io/api/requirements). |

cmd/command_list.go

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package main
22

33
import (
4+
"fmt"
5+
"os"
6+
"os/signal"
47
"strconv"
58
"strings"
69
"sync"
10+
"syscall"
11+
"time"
712

813
"github.com/mkideal/cli"
914

@@ -25,14 +30,15 @@ var outputHeader = []string{
2530
"region",
2631
"latitude",
2732
"longitude",
28-
"is_anonymous",
29-
"is_anonymous_vpn",
33+
"is_vpn",
3034
"is_hosting",
3135
"is_proxy",
3236
"is_tor",
3337
"is_bot",
3438
"is_bogon",
3539
"has_other_threat",
40+
"threat_comment",
41+
"error",
3642
// "as_routes", append by code
3743
}
3844

@@ -43,10 +49,19 @@ type listT struct {
4349
InputCSV string `cli:"*i,input" usage:"input csv/tsv file path --input='./input.csv'"`
4450
Output string `cli:"*o,output" usage:"output tsv file path --output='./output.tsv'"`
4551
UseRoute bool `cli:"route" usage:"set if you need route data from IRR (this might be slow) --route"`
52+
Interval string `cli:"interval" usage:"time interval after a API call to handle rate limit (ms=msec s=sec, m=min) --interval=1.5s"`
53+
Parallel int `cli:"m,parallel" usage:"parallel number (multiple API calls) --parallel=2" dft:"2"`
54+
Verbose bool `cli:"v,verbose" usage:"set if you need detail logs --verbose"`
4655
Debug bool `cli:"debug" usage:"set if you use HTTP debug feature --debug"`
4756
}
4857

4958
func (a *listT) Validate(ctx *cli.Context) error {
59+
if a.Interval != "" {
60+
if _, err := time.ParseDuration(a.Interval); err != nil {
61+
return fmt.Errorf("invalid 'interval' format: [%w]", err)
62+
}
63+
}
64+
5065
return validateProviderString(a.Provider)
5166
}
5267

@@ -69,16 +84,24 @@ type ListRunner struct {
6984
Provider string
7085
InputCSV string
7186
Output string
87+
Parallel int
7288
UseRoute bool
89+
Interval string
90+
Verbose bool
7391
Debug bool
92+
93+
logger log.Logger
7494
}
7595

7696
func newListRunner(p listT) ListRunner {
7797
return ListRunner{
7898
Provider: p.Provider,
7999
InputCSV: p.InputCSV,
80100
Output: p.Output,
101+
Parallel: p.Parallel,
81102
UseRoute: p.UseRoute,
103+
Interval: p.Interval,
104+
Verbose: p.Verbose,
82105
Debug: p.Debug,
83106
}
84107
}
@@ -99,17 +122,26 @@ func (r *ListRunner) Run() error {
99122
return err
100123
}
101124

102-
maxReqNum := 3
103-
maxReq := make(chan struct{}, maxReqNum)
104-
105125
providerList, err := getProvidersFromString(r.Provider)
106126
if err != nil {
107127
panic(err)
108128
}
109129

130+
// parse interval duration
131+
var interval time.Duration
132+
if r.Interval != "" {
133+
v, err := time.ParseDuration(r.Interval)
134+
if err != nil {
135+
return err
136+
}
137+
interval = v
138+
}
139+
110140
logger := &log.StdLogger{}
141+
r.logger = logger
111142
svc, err := ipfraudcheck.New(ipfraudcheck.Config{
112143
UseRoute: r.UseRoute,
144+
Interval: interval,
113145
Debug: r.Debug,
114146
Logger: logger,
115147
}, providerList)
@@ -122,6 +154,20 @@ func (r *ListRunner) Run() error {
122154

123155
providerSize := len(providerList)
124156
result := make([]string, len(lines)*providerSize)
157+
158+
// handle kill signal
159+
sig := make(chan os.Signal, 1)
160+
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
161+
go func() {
162+
<-sig
163+
logger.Errorf("Stop signal detected!")
164+
logger.Errorf("Saving intermediate results...")
165+
result = append([]string{strings.Join(outputHeader, "\t")}, result...)
166+
w.WriteAll(result)
167+
os.Exit(2)
168+
}()
169+
170+
maxReq := make(chan struct{}, r.Parallel)
125171
var wg sync.WaitGroup
126172
for i, line := range lines {
127173
i = i * providerSize
@@ -133,38 +179,49 @@ func (r *ListRunner) Run() error {
133179
wg.Done()
134180
}()
135181

136-
logger.Infof("exec #: [%d]\n", i/providerSize)
137-
rows, err := r.execAPI(svc, line)
182+
num := i / providerSize
183+
logger.Infof("exec #: [%d]\n", num)
184+
rows, err := r.execAPI(svc, line, num)
138185
if err != nil {
139186
logger.Errorf("#: [%d]; err=[%v]\n", i, err)
140187
return
141188
}
142189
for j, row := range rows {
143190
result[i+j] = strings.Join(row, "\t")
144191
}
192+
svc.WaitInterval()
145193
}(i, line)
146194
}
147195
wg.Wait()
148196

149197
result = append([]string{strings.Join(outputHeader, "\t")}, result...)
198+
logger.Infof("Finished")
150199
return w.WriteAll(result)
151200
}
152201

153-
func (r *ListRunner) execAPI(svc *ipfraudcheck.Client, param map[string]string) ([][]string, error) {
202+
func (r *ListRunner) execAPI(svc *ipfraudcheck.Client, param map[string]string, num int) ([][]string, error) {
154203
resp, err := svc.CheckIP(param["ip_address"])
155204
if err != nil {
156205
return nil, err
157206
}
158207

159208
rows := make([][]string, len(resp.List))
160-
for i, r := range resp.List {
209+
for i, res := range resp.List {
161210
row := make([]string, 0, len(outputHeader))
162211
for _, v := range outputHeader {
163212
if v == "as_routes" {
164213
row = append(row, strings.Join(resp.ASPrefix, " "))
165214
continue
166215
}
167-
row = append(row, getValue(param, r, v))
216+
row = append(row, getValue(param, res, v))
217+
}
218+
if r.Verbose {
219+
switch {
220+
case res.Err != "":
221+
r.logger.Errorf("#: [%d]; provider=[%s] ip=[%s] err=[%v]\n", num, res.ServiceName, res.IP, res.Err)
222+
default:
223+
r.logger.Infof("#: [%d]; provider=[%s] ip=[%s] row=[%v]\n", num, res.ServiceName, res.IP, row)
224+
}
168225
}
169226
rows[i] = row
170227
}
@@ -197,10 +254,8 @@ func getValue(param map[string]string, resp provider.FraudCheckResponse, name st
197254
return strconv.FormatFloat(resp.Latitude, 'f', 5, 64)
198255
case "longitude":
199256
return strconv.FormatFloat(resp.Longitude, 'f', 5, 64)
200-
case "is_anonymous":
201-
return strconv.FormatBool(resp.IsAnonymous)
202257
case "is_anonymous_vpn":
203-
return strconv.FormatBool(resp.IsAnonymousVPN)
258+
return strconv.FormatBool(resp.IsVPN)
204259
case "is_hosting":
205260
return strconv.FormatBool(resp.IsHosting)
206261
case "is_proxy":
@@ -213,6 +268,10 @@ func getValue(param map[string]string, resp provider.FraudCheckResponse, name st
213268
return strconv.FormatBool(resp.IsBogon)
214269
case "has_other_threat":
215270
return strconv.FormatBool(resp.HasOtherThreat)
271+
case "threat_comment":
272+
return resp.ThreatComment
273+
case "error":
274+
return resp.Err
216275
}
217276
return ""
218277
}

cmd/command_providers.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"github.com/mkideal/cli"
8+
)
9+
10+
// parameters of 'providers' command.
11+
type providersT struct {
12+
cli.Helper
13+
}
14+
15+
func (a *providersT) Validate(ctx *cli.Context) error {
16+
return nil
17+
}
18+
19+
var providersC = &cli.Command{
20+
Name: "providers",
21+
Desc: "Show supported provider types",
22+
Argv: func() interface{} { return new(providersT) },
23+
Fn: execShowProviders,
24+
}
25+
26+
func execShowProviders(ctx *cli.Context) error {
27+
argv := ctx.Argv().(*providersT)
28+
29+
r := newShowProvidersRunner(*argv)
30+
return r.Run()
31+
}
32+
33+
type ShowProvidersRunner struct{}
34+
35+
func newShowProvidersRunner(p providersT) ShowProvidersRunner {
36+
return ShowProvidersRunner{}
37+
}
38+
39+
func (r *ShowProvidersRunner) Run() error {
40+
providers := make([]string, 0, len(providerMap))
41+
for key := range providerMap {
42+
providers = append(providers, key)
43+
}
44+
sort.Strings(providers)
45+
fmt.Printf("%v\n", providers)
46+
return nil
47+
}

cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func main() {
1212
cli.Tree(help),
1313
cli.Tree(singleC),
1414
cli.Tree(listC),
15+
cli.Tree(providersC),
1516
).Run(os.Args[1:]); err != nil {
1617
fmt.Fprintln(os.Stderr, err)
1718
os.Exit(1)

cmd/providers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/evalphobia/go-ip-fraud-check/provider/ipqualityscore"
1414
"github.com/evalphobia/go-ip-fraud-check/provider/ipregistry"
1515
"github.com/evalphobia/go-ip-fraud-check/provider/minfraud"
16+
"github.com/evalphobia/go-ip-fraud-check/provider/shodan"
1617
)
1718

1819
const (
@@ -24,6 +25,7 @@ const (
2425
providerIPQS = "ipqualityscore"
2526
providerIPRegistry = "ipregistry"
2627
providerMinFraud = "minfraud"
28+
providerShodan = "shodan"
2729
)
2830

2931
var providerMap = map[string]provider.Provider{
@@ -35,6 +37,7 @@ var providerMap = map[string]provider.Provider{
3537
providerIPQS: &ipqualityscore.IPQualityScoreProvider{},
3638
providerIPRegistry: &ipregistry.IPRegistryProvider{},
3739
providerMinFraud: &minfraud.MinFraudProvider{},
40+
providerShodan: &shodan.ShodanProvider{},
3841
}
3942

4043
func validateProviderString(s string) error {

ipfraudcheck/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
envIPdatacoAPIKey = "FRAUD_CHECK_IPDATACO_APIKEY"
1515
envIPGeolocationAPIKey = "FRAUD_CHECK_IPGEOLOCATION_APIKEY"
1616
envIPinfoioToken = "FRAUD_CHECK_IPINFOIO_TOKEN"
17+
envShodanAPIKey = "FRAUD_CHECK_SHODAN_APIKEY"
1718
)
1819

1920
const (
@@ -41,9 +42,12 @@ type Config struct {
4142
// minFraud
4243
MinFraudAccountID string
4344
MinFraudLicenseKey string
45+
// shodan.io
46+
ShodanAPIKey string
4447

4548
// common option
4649
UseRoute bool
50+
Interval time.Duration
4751
Debug bool
4852
Timeout time.Duration
4953
Logger log.Logger
@@ -100,3 +104,11 @@ func (c Config) GetIPinfoioToken() string {
100104
}
101105
return c.IPinfoioToken
102106
}
107+
108+
func (c Config) GetShodanAPIKey() string {
109+
s := os.Getenv(envShodanAPIKey)
110+
if s != "" {
111+
return s
112+
}
113+
return c.ShodanAPIKey
114+
}

0 commit comments

Comments
 (0)