shodanz 1.0.6 → 2.0.0

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.
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils.rb'
4
+
5
+ # fronzen_string_literal: true
6
+
7
+ module Shodanz
8
+ module API
9
+ # Utils provides simply get, post, and slurp_stream functionality
10
+ # to the client. Under the hood they support both async and non-async
11
+ # usage. You should basically never need to use these methods directly.
12
+ #
13
+ # @author Kent 'picat' Gruber
14
+ module Utils
15
+ # Perform a direct GET HTTP request to the REST API.
16
+ def get(path, **params)
17
+ return sync_get(path, params) unless Async::Task.current?
18
+
19
+ async_get(path, params)
20
+ end
21
+
22
+ # Perform a direct POST HTTP request to the REST API.
23
+ def post(path, **params)
24
+ return sync_post(path, params) unless Async::Task.current?
25
+
26
+ async_post(path, params)
27
+ end
28
+
29
+ # Perform the main function of consuming the streaming API.
30
+ def slurp_stream(path, **params)
31
+ if Async::Task.current?
32
+ async_slurp_stream(path, params) do |result|
33
+ yield result
34
+ end
35
+ else
36
+ sync_slurp_stream(path, params) do |result|
37
+ yield result
38
+ end
39
+ end
40
+ end
41
+
42
+ def turn_into_query(params)
43
+ filters = params.reject { |key, _| key == :query }
44
+ filters.each do |key, value|
45
+ params[:query] << " #{key}:#{value}"
46
+ end
47
+ params.select { |key, _| key == :query }
48
+ end
49
+
50
+ def turn_into_facets(facets)
51
+ return {} if facets.nil?
52
+
53
+ filters = facets.reject { |key, _| key == :facets }
54
+ facets[:facets] = []
55
+ filters.each do |key, value|
56
+ facets[:facets] << "#{key}:#{value}"
57
+ end
58
+ facets[:facets] = facets[:facets].join(',')
59
+ facets.select { |key, _| key == :facets }
60
+ end
61
+
62
+ private
63
+
64
+ RATELIMIT = 'rate limit reached'
65
+ NOINFO = 'no information available'
66
+ NOQUERY = 'empty search query'
67
+
68
+ def handle_any_json_errors(json)
69
+ return json unless json.is_a?(Hash) && json.key?('error')
70
+
71
+ raise Shodanz::Errors::RateLimited if json['error'].casecmp(RATELIMIT) >= 0
72
+ raise Shodanz::Errors::NoInformation if json['error'].casecmp(NOINFO) >= 0
73
+ raise Shodanz::Errors::NoQuery if json['error'].casecmp(NOQUERY) >= 0
74
+
75
+ json
76
+ end
77
+
78
+ def getter(path, **params)
79
+ # param keys should all be strings
80
+ params = params.transform_keys(&:to_s)
81
+ # build up url string based on special params
82
+ url = "#{@url}#{path}?key=#{@key}"
83
+ # special params
84
+ %w[query ips hostnames].each do |param|
85
+ if (value = params.delete(param))
86
+ url += "&#{param}=#{value}"
87
+ end
88
+ end
89
+ resp = @internet.get(url)
90
+
91
+ # parse all lines in the response body as JSON
92
+ json = JSON.parse(resp.body.join)
93
+
94
+ handle_any_json_errors(json)
95
+ ensure
96
+ resp&.close
97
+ end
98
+
99
+ def poster(path, **params)
100
+ # param keys should all be strings
101
+ params = params.transform_keys(&:to_s)
102
+ # make POST request to server
103
+ resp = @internet.post("#{@url}#{path}?key=#{@key}", params)
104
+
105
+ # parse all lines in the response body as JSON
106
+ json = JSON.parse(resp.body.join)
107
+
108
+ handle_any_json_errors(json)
109
+ ensure
110
+ resp&.close
111
+ end
112
+
113
+ def slurper(path, **params)
114
+ # param keys should all be strings
115
+ params = params.transform_keys(&:to_s)
116
+ # check if limit
117
+ if (limit = params.delete('limit'))
118
+ counter = 0
119
+ end
120
+ # make GET request to server
121
+ resp = @internet.get("#{@url}#{path}?key=#{@key}", params)
122
+ # read body line-by-line
123
+ until resp.body.nil? || resp.body.empty?
124
+ resp.body.read.each_line do |line|
125
+ next if line.strip.empty?
126
+
127
+ yield JSON.parse(line)
128
+ if limit
129
+ counter += 1
130
+ resp.close if counter == limit
131
+ end
132
+ end
133
+ end
134
+ ensure
135
+ resp&.close
136
+ end
137
+
138
+ def async_get(path, **params)
139
+ Async::Task.current.async do
140
+ getter(path, params)
141
+ end
142
+ end
143
+
144
+ def sync_get(path, **params)
145
+ Async do
146
+ getter(path, params)
147
+ end.wait
148
+ end
149
+
150
+ def async_post(path, **params)
151
+ Async::Task.current.async do
152
+ poster(path, params)
153
+ end
154
+ end
155
+
156
+ def sync_post(path, **params)
157
+ Async do
158
+ poster(path, params)
159
+ end.wait
160
+ end
161
+
162
+ def async_slurp_stream(path, **params)
163
+ Async::Task.current.async do
164
+ slurper(path, params) { |data| yield data }
165
+ end
166
+ end
167
+
168
+ def sync_slurp_stream(path, **params)
169
+ Async do
170
+ slurper(path, params) { |data| yield data }
171
+ end.wait
172
+ end
173
+ end
174
+ end
175
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shodanz
2
4
  # General client container class for all three
3
5
  # of the available API endpoints in a
@@ -17,7 +19,8 @@ module Shodanz
17
19
  # Optionally provide your Shodan API key, or the environment
18
20
  # variable SHODAN_API_KEY will be used.
19
21
  def initialize(key: ENV['SHODAN_API_KEY'])
20
- raise "No API key has been found or provided! ( setup your SHODAN_API_KEY environment varialbe )" if key.nil?
22
+ raise Shodanz::Errors::NoAPIKey if key.nil?
23
+
21
24
  # pass the given API key to each of the underlying clients
22
25
  #
23
26
  # Note: you can optionally change these API keys later, if you
@@ -27,5 +30,89 @@ module Shodanz
27
30
  @streaming_api = Shodanz.api.streaming.new(key: key)
28
31
  @exploits_api = Shodanz.api.exploits.new(key: key)
29
32
  end
33
+
34
+ def host(ip, **params)
35
+ rest_api.host(ip, **params)
36
+ end
37
+
38
+ def host_count(query = '', facets: {}, **params)
39
+ rest_api.host_count(query, facets: facets, **params)
40
+ end
41
+
42
+ def host_search(query = '', facets: {}, page: 1, minify: true, **params)
43
+ rest_api.host_search(query, facets: facets, page: page, minify: minify, **params)
44
+ end
45
+
46
+ def host_search_tokens(query = '', **params)
47
+ rest_api.host_search(query, params)
48
+ end
49
+
50
+ def ports
51
+ rest_api.ports
52
+ end
53
+
54
+ def protocols
55
+ rest_api.protocols
56
+ end
57
+
58
+ def scan(*ips)
59
+ rest_api.scan(ips)
60
+ end
61
+
62
+ def crawl_for(**params)
63
+ rest_api.scan(params)
64
+ end
65
+
66
+ def scan_status(id)
67
+ rest_api.scan_status(id)
68
+ end
69
+
70
+ def community_queries(**params)
71
+ rest_api.community_queries(params)
72
+ end
73
+
74
+ def search_for_community_query(query, **params)
75
+ rest_api.search_for_community_query(query, params)
76
+ end
77
+
78
+ def popular_query_tags(size = 10)
79
+ rest_api.popular_query_tags(size)
80
+ end
81
+
82
+ def profile
83
+ rest_api.profile
84
+ end
85
+
86
+ def resolve(*hostnames)
87
+ rest_api.resolve(hostnames)
88
+ end
89
+
90
+ def reverse_lookup(*ips)
91
+ rest_api.reverse_lookup(ips)
92
+ end
93
+
94
+ def http_headers
95
+ rest_api.http_headers
96
+ end
97
+
98
+ def my_ip
99
+ rest_api.my_ip
100
+ end
101
+
102
+ def honeypot_score(ip)
103
+ rest_api.honeypot_score(ip)
104
+ end
105
+
106
+ def info
107
+ rest_api.info
108
+ end
109
+
110
+ def exploit_search(query = '', page: 1, **params)
111
+ exploits_api.search(query, page: page, **params)
112
+ end
113
+
114
+ def exploit_count(query = '', page: 1, **params)
115
+ exploits_api.count(query, page: page, **params)
116
+ end
30
117
  end
31
118
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shodanz
4
+ module Errors
5
+ class RateLimited < StandardError
6
+ def initialize(msg = 'Request rate limit reached (1 request/ second). Please wait a second before trying again and slow down your API calls.')
7
+ super
8
+ end
9
+ end
10
+
11
+ class NoInformation < StandardError
12
+ def initialize(msg = 'No information available.')
13
+ super
14
+ end
15
+ end
16
+
17
+ class NoAPIKey < StandardError
18
+ def initialize(msg = 'No API key has been found or provided! ( setup your SHODAN_API_KEY environment varialbe )')
19
+ super
20
+ end
21
+ end
22
+
23
+ class NoQuery < StandardError
24
+ def initialize(msg = 'Empty search query.')
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shodanz
2
- VERSION = '1.0.6'.freeze
4
+ VERSION = '2.0.0'
3
5
  end
@@ -1,29 +1,32 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "shodanz/version"
5
+ require 'shodanz/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "shodanz"
8
+ spec.name = 'shodanz'
8
9
  spec.version = Shodanz::VERSION
9
10
  spec.authors = ["Kent 'picatz' Gruber"]
10
- spec.email = ["kgruber1@emich.edu"]
11
+ spec.email = ['kgruber1@emich.edu']
11
12
 
12
- spec.summary = %q{A modern Ruby gem for Shodan, the world's first search engine for Internet-connected devices.}
13
- spec.description = %q{Featuring full support for the REST, Streaming and Exploits API}
14
- spec.homepage = "https://github.com/picatz/shodanz"
15
- spec.license = "MIT"
13
+ spec.summary = "A modern, async Ruby gem for Shodan, the world's first search engine for Internet-connected devices."
14
+ spec.description = 'Featuring full support for the REST, Streaming and Exploits API'
15
+ spec.homepage = 'https://github.com/picatz/shodanz'
16
+ spec.license = 'MIT'
16
17
 
17
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
19
  f.match(%r{^(test|spec|features)/})
19
20
  end
20
- spec.require_paths = ["lib"]
21
-
22
- spec.add_dependency "unirest", "1.1.2"
23
- spec.add_dependency "json" # specifying version breaks things, what?
24
- spec.add_dependency "oj", "3.5.0"
25
-
26
- spec.add_development_dependency "bundler", "~> 1.16.1"
27
- spec.add_development_dependency "rake", "~> 12.3.1"
28
- spec.add_development_dependency "rspec", "~> 3.7.0"
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'async', '~> 1.17.1'
24
+ spec.add_dependency 'async-http', '~> 0.38.1'
25
+
26
+ spec.add_development_dependency 'async-rspec', '~> 1.12.1'
27
+ spec.add_development_dependency 'bundler', '~> 1.17.2'
28
+ spec.add_development_dependency 'pry', '~> 0.12.2'
29
+ spec.add_development_dependency 'rake', '~> 12.3.2'
30
+ spec.add_development_dependency 'rb-readline', '~> 0.5.5'
31
+ spec.add_development_dependency 'rspec', '~> 3.8.0'
29
32
  end
metadata CHANGED
@@ -1,99 +1,127 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shodanz
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kent 'picatz' Gruber
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-04 00:00:00.000000000 Z
11
+ date: 2019-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: unirest
14
+ name: async
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.1.2
19
+ version: 1.17.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '='
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.1.2
26
+ version: 1.17.1
27
27
  - !ruby/object:Gem::Dependency
28
- name: json
28
+ name: async-http
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 0.38.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 0.38.1
41
41
  - !ruby/object:Gem::Dependency
42
- name: oj
42
+ name: async-rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '='
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 3.5.0
48
- type: :runtime
47
+ version: 1.12.1
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '='
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 3.5.0
54
+ version: 1.12.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: 1.16.1
61
+ version: 1.17.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.17.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.12.2
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: 1.16.1
82
+ version: 0.12.2
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rake
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 12.3.1
89
+ version: 12.3.2
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 12.3.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: rb-readline
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.5.5
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
108
  - - "~>"
81
109
  - !ruby/object:Gem::Version
82
- version: 12.3.1
110
+ version: 0.5.5
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: rspec
85
113
  requirement: !ruby/object:Gem::Requirement
86
114
  requirements:
87
115
  - - "~>"
88
116
  - !ruby/object:Gem::Version
89
- version: 3.7.0
117
+ version: 3.8.0
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
- version: 3.7.0
124
+ version: 3.8.0
97
125
  description: Featuring full support for the REST, Streaming and Exploits API
98
126
  email:
99
127
  - kgruber1@emich.edu
@@ -109,6 +137,9 @@ files:
109
137
  - LICENSE.txt
110
138
  - README.md
111
139
  - Rakefile
140
+ - examples/async_honeypot_detector.rb
141
+ - examples/async_host_search_example.rb
142
+ - examples/async_stream_example.rb
112
143
  - examples/debug.rb
113
144
  - examples/streaming_banner_product_stats.rb
114
145
  - examples/top_10_countries_running.rb
@@ -118,7 +149,9 @@ files:
118
149
  - lib/shodanz/apis/exploits.rb
119
150
  - lib/shodanz/apis/rest.rb
120
151
  - lib/shodanz/apis/streaming.rb
152
+ - lib/shodanz/apis/utils.rb
121
153
  - lib/shodanz/client.rb
154
+ - lib/shodanz/errors.rb
122
155
  - lib/shodanz/version.rb
123
156
  - shodanz.gemspec
124
157
  homepage: https://github.com/picatz/shodanz
@@ -141,9 +174,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
174
  version: '0'
142
175
  requirements: []
143
176
  rubyforge_project:
144
- rubygems_version: 2.7.6
177
+ rubygems_version: 3.0.0.beta1
145
178
  signing_key:
146
179
  specification_version: 4
147
- summary: A modern Ruby gem for Shodan, the world's first search engine for Internet-connected
148
- devices.
180
+ summary: A modern, async Ruby gem for Shodan, the world's first search engine for
181
+ Internet-connected devices.
149
182
  test_files: []