fluent-plugin-shodan 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fe44cee9c9cd48566c10962f21e41a17e533e64922ab9310279e4e04c0cc8f8
4
- data.tar.gz: 54775456dd2156f19b1f77944438302f9ec4bce33040478e4521aad3028c4d2f
3
+ metadata.gz: 6433f3162ef921f1b577f5489f802a1a6d88d561026a4274440732d326e55e88
4
+ data.tar.gz: 6d97984422e0d0bcaada2ac521a47d8fcd4049e437327fd03e3c39aac11858e5
5
5
  SHA512:
6
- metadata.gz: 7a81fffe6a3a5266936f8372ca518ca8a67e36dc9d72290b0c6aea97ceb411d6d22eef7e4e67b4a45dfa1a5bf30e61db318ff0e056399fd0fde5c67884d038d2
7
- data.tar.gz: c895e61fa305082e4b98c206bb9886352a8c23257a2fa58cb37326493dfa343094fd768dbb2fb492c8660977b793a50a34a683813842bcda46e8f90c101f57fa
6
+ metadata.gz: 775543570f5613db0515f258ca8b88b034f87e6265d954b82f59b21e09befee57e0f162f79d8ab6fd8b6e932427bd8d4e242c2dbec8b96a57477c0535d97fd06
7
+ data.tar.gz: 82e80191ff18df2b6f393e6b2330f61a273d1a786863f58041c0d0aeef14c79e16f41bbd36febe625d9f5b7005b7ffd1dbd3bf3a792d464b89126d4715eaef6b
data/README.md CHANGED
@@ -83,9 +83,11 @@ Default value: `3600`.
83
83
 
84
84
  The tag to apply to each shodan entries.
85
85
 
86
- #### query (string) (required)
86
+ #### query (string) (optional)
87
87
 
88
- The Shodan query to execute.
88
+ The Shodan query to execute. The query can be empty if at least one filter is set.
89
+
90
+ Default: `nil`
89
91
 
90
92
  #### max_pages (integer) (optional)
91
93
 
@@ -93,6 +95,16 @@ The maximum amount of pages to crawl. A 0 or negative value means to crawl all p
93
95
 
94
96
  Default value: `1`.
95
97
 
98
+ #### filter (optional) (multi)
99
+
100
+ ##### name (string) (required)
101
+
102
+ The name of the filter to be added to the query. Full filters list is available on the [Shodan filter reference page](https://www.shodan.io/search/filters). The filter can be negated by prepending `-` to the filter name (ex: `name -port`).
103
+
104
+ ##### value (string) (required)
105
+
106
+ The value to be passed to the filter.
107
+
96
108
  ## Shodan Stream
97
109
 
98
110
  WIP
@@ -103,7 +115,7 @@ WIP
103
115
 
104
116
  ```
105
117
  <source>
106
- @type shodan_search
118
+ @type shodan_alert
107
119
  interval 15m
108
120
  alert_id GA3FRJ1HJNDPORHV
109
121
  api_key 1234567890AZERTYUIOP
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.2
1
+ 0.0.3
@@ -7,6 +7,25 @@ module Fluent::Plugin
7
7
 
8
8
  helpers :timer
9
9
 
10
+ SUPPORTED_FILTERS = [
11
+ 'asn','city','country','cpe','device','geo','has_ipv6','has_screenshot',
12
+ 'has_ssl','has_vuln','hash','hostname','ip','isp','link','net','org','os',
13
+ 'port','postal','product','region','scan','shodan.module','state',
14
+ 'version','screenshot.label','cloud.provider','cloud.region',
15
+ 'cloud.service','http.component','http.component_category',
16
+ 'http.favicon.hash','http.html','http.html_hash','http.robots_hash',
17
+ 'http.securitytxt','http.status','http.title','http.waf','bitcoin.ip',
18
+ 'bitcoin.ip_count','bitcoin.port','bitcoin.version','snmp.contact',
19
+ 'snmp.location','snmp.name','ssl','ssl.alpn','ssl.cert.alg',
20
+ 'ssl.cert.expired','ssl.cert.extension','ssl.cert.fingerprint',
21
+ 'ssl.cert.issuer.cn','ssl.cert.pubkey.bits','ssl.cert.pubkey.type',
22
+ 'ssl.cert.serial','ssl.cert.subject.cn','ssl.chain_count',
23
+ 'ssl.cipher.bits','ssl.cipher.name','ssl.cipher.version','ssl.ja3s',
24
+ 'ssl.jarm','ssl.version','ntp.ip','ntp.ip_count','ntp.more','ntp.port',
25
+ 'telnet.do','telnet.dont','telnet.option','telnet.will','telnet.wont',
26
+ 'ssh.hassh','ssh.type', 'tag', 'vuln'
27
+ ]
28
+
10
29
  desc "The API key to connect to the Shodan API."
11
30
  config_param :api_key, :string, secret: true
12
31
  desc "The interval time between running queries."
@@ -14,9 +33,16 @@ module Fluent::Plugin
14
33
  desc "The tag to apply to each shodan entries."
15
34
  config_param :tag, :string, default: nil
16
35
  desc "The Shodan query to execute."
17
- config_param :query, :string
36
+ config_param :query, :string, default: ''
18
37
  desc "The maximum amount of pages to crawl. A 0 or negative value means to crawl all pages."
19
38
  config_param :max_pages, :integer, default: 1
39
+ desc "Search filters configuration."
40
+ config_section :filter, param_name: 'filters', required: false, multi: true do
41
+ desc "Name of the filter. See https://www.shodan.io/search/filters for a list of supported filters"
42
+ config_param :name, :enum, list: (SUPPORTED_FILTERS + SUPPORTED_FILTERS.map {|filter| "-#{filter}"}).map { |filter| filter.to_sym }
43
+ desc "Value to be given to the filter"
44
+ config_param :value, :string
45
+ end
20
46
 
21
47
  def configure(conf)
22
48
  super
@@ -27,6 +53,13 @@ module Fluent::Plugin
27
53
  rescue RuntimeError => exception
28
54
  raise Fluent::ConfigError.new "Invalid Shodan API key"
29
55
  end
56
+
57
+ raise Fluent::ConfigError.new("At least a query or one filter should be configured") if @query.empty? and @filters.empty?
58
+
59
+ @search_filters = {}
60
+ @filters.each do |filter|
61
+ @search_filters[filter.name] = filter.value
62
+ end
30
63
  end
31
64
 
32
65
  def multi_workers_ready?
@@ -44,20 +77,21 @@ module Fluent::Plugin
44
77
  def run
45
78
  log.debug "Starting Shodan search", query: @query, max_pages: @max_pages
46
79
  es_time = Fluent::EventTime.now
47
- current_page = 0
80
+ opts = @search_filters.merge({page: 0})
48
81
  read_entries = 0
49
82
  loop do
50
- current_page += 1
51
- result = @client.host_search(@query, page: current_page)
83
+ opts[:page] += 1
84
+ log.trace query: @query, opts: opts
85
+ result = @client.host_search(@query.dup, **opts)
52
86
  result['matches'].each do |rec|
53
87
  router.emit(@tag, es_time, rec)
54
88
  end
55
89
  read_entries += result['matches'].length
56
- break if (@max_pages >= 0 && current_page >= @max_pages) || read_entries >= result['total']
90
+ break if (@max_pages >= 0 && opts[:page] >= @max_pages) || read_entries >= result['total']
57
91
  end
58
- log.debug "Shodan search ending", query: @query, total_read: read_entries
92
+ log.debug "Shodan search ending", query: @query, filters: @search_filters, total_read: read_entries
59
93
  rescue RuntimeError => re
60
- log.error "Unable to execute Shodan query", query: @query, page: current_page, error: re
94
+ log.error "Unable to execute Shodan query", query: @query, filters: @search_filters, page: current_page, error: re
61
95
  rescue => exception
62
96
  log.error "Error executing Shodan query", error: exception
63
97
  end
@@ -9,6 +9,13 @@ class ShodanSearchInputTest < Test::Unit::TestCase
9
9
  include Fluent::Test::Helpers
10
10
 
11
11
  API_KEY = ENV['SHODAN_TEST_API_KEY']
12
+ CONFIG = %[
13
+ api_key #{API_KEY}
14
+ query 8.8.8.8
15
+ ]
16
+
17
+ private_constant :API_KEY
18
+ private_constant :CONFIG
12
19
 
13
20
  def setup
14
21
  Fluent::Test.setup
@@ -30,10 +37,21 @@ class ShodanSearchInputTest < Test::Unit::TestCase
30
37
  ])
31
38
  end
32
39
  end
33
- test 'is invalid without a query' do
40
+ test 'is invalid without a query or a filter' do
41
+ assert_raise Fluent::ConfigError do
42
+ create_driver(%[
43
+ api_key #{API_KEY}
44
+ ])
45
+ end
46
+ end
47
+ test 'is invalid with an unsupported filter' do
34
48
  assert_raise Fluent::ConfigError do
35
49
  create_driver(%[
36
50
  api_key #{API_KEY}
51
+ <filter>
52
+ name some.dumb.filter
53
+ value 42
54
+ </filter>
37
55
  ])
38
56
  end
39
57
  end
@@ -41,18 +59,100 @@ class ShodanSearchInputTest < Test::Unit::TestCase
41
59
  d = create_driver
42
60
  assert_equal '8.8.8.8', d.instance.query
43
61
  end
62
+ test 'is valid with one filter and no query' do
63
+ assert_nothing_raised do
64
+ d = create_driver(%[
65
+ api_key #{API_KEY}
66
+ <filter>
67
+ name product
68
+ value ssh
69
+ </filter>
70
+ ])
71
+ end
72
+ end
44
73
  end
45
-
74
+
46
75
  sub_test_case 'Plugin emission' do
76
+ test 'with simple query' do
77
+ expected_tag = 'test_shodan'
78
+ d = create_driver(CONFIG + "\ninterval 5\ntag #{expected_tag}")
79
+ d.run(expect_emits: 10, timeout: 30)
80
+ events = d.events
81
+ assert_not_empty(events)
82
+ assert_all(events, "Events are not properly tagged") {|evt| expected_tag == evt[0]}
83
+ assert_all(events, "Events do not match query") {|evt| evt[2]['data'].downcase.include?('8.8.8.8')}
84
+ assert_all(events, "Events do not have a '_shodan' key") {|evt| evt[2].has_key?('_shodan')}
85
+ end
86
+
87
+ test 'with a single filter' do
88
+ config = %[
89
+ api_key #{API_KEY}
90
+ query ssh
91
+ interval 5
92
+ tag shodan_test
93
+ <filter>
94
+ name product
95
+ value ssh
96
+ </filter>
97
+ ]
98
+ d = create_driver(config)
99
+ d.run(expect_emits: 10, timeout: 30)
100
+ events = d.events
101
+ assert_not_empty(events)
102
+ assert_all(events) {|evt| evt[2]['product'].downcase.include?('ssh')}
103
+ end
104
+
105
+ test 'with multiple filters' do
106
+ config = %[
107
+ api_key #{API_KEY}
108
+ query ssh
109
+ interval 5
110
+ tag shodan_test
111
+ <filter>
112
+ name product
113
+ value ssh
114
+ </filter>
115
+ <filter>
116
+ name -port
117
+ value 22
118
+ </filter>
119
+ ]
120
+ d = create_driver(config)
121
+ d.run(expect_emits: 10, timeout: 30)
122
+ events = d.events
123
+ assert_not_empty(events)
124
+ assert_all(events) {|evt| evt[2]['product'].downcase.include?('ssh')}
125
+ assert_all(events) {|evt| evt[2]['port'] != 22}
126
+ end
127
+
128
+ test 'combine query and filters' do
129
+ config = %[
130
+ api_key #{API_KEY}
131
+ query ssh
132
+ interval 5
133
+ tag shodan_test
134
+ query plex
135
+ <filter>
136
+ name country
137
+ value FR
138
+ </filter>
139
+ <filter>
140
+ name -port
141
+ value 32400
142
+ </filter>
143
+ ]
144
+ d = create_driver(config)
145
+ d.run(expect_emits: 10, timeout: 30)
146
+ events = d.events
147
+ assert_not_empty(events)
148
+ assert_all(events) {|evt| evt[2]['data'].downcase.include?('plex')}
149
+ assert_all(events) {|evt| evt[2]['location']['country_code'] == 'FR'}
150
+ assert_all(events) {|evt| evt[2]['port'] != 32400}
151
+ end
47
152
  end
48
153
 
49
154
  private
50
155
 
51
- CONFIG = %[
52
- api_key #{API_KEY}
53
- query 8.8.8.8
54
- ]
55
-
56
156
  def create_driver(conf = CONFIG)
57
157
  Fluent::Test::Driver::Input.new(Fluent::Plugin::ShodanSearch).configure(conf)
58
158
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-shodan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - srilumpa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-07 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd