From 80fb804ef56f0a7167d0239cf6cf45ffa77e244e Mon Sep 17 00:00:00 2001 From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com> Date: Sun, 5 Nov 2023 20:20:43 +0000 Subject: [PATCH 1/5] Bumps deps --- shodanz.gemspec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shodanz.gemspec b/shodanz.gemspec index 3427959..571f85b 100644 --- a/shodanz.gemspec +++ b/shodanz.gemspec @@ -20,13 +20,13 @@ Gem::Specification.new do |spec| end spec.require_paths = ['lib'] - spec.add_dependency 'async-http', '>= 0.38.1', '< 0.57.0' - spec.add_dependency 'async', '>= 1.17.1', '< 2.1.0' + spec.add_dependency 'async-http', '>= 0.38.1', '< 0.62.0' + spec.add_dependency 'async', '>= 1.17.1', '< 2.7.0' - spec.add_development_dependency 'async-rspec', '~> 1.16.1' - spec.add_development_dependency 'bundler', '~> 2.2.0' + spec.add_development_dependency 'async-rspec', '~> 1.17.0' + spec.add_development_dependency 'bundler', '~> 2.4.0' spec.add_development_dependency 'pry', '~> 0.14.1' - spec.add_development_dependency 'rake', '~> 13.0.0' + spec.add_development_dependency 'rake', '~> 13.1.0' spec.add_development_dependency 'rb-readline', '~> 0.5.5' - spec.add_development_dependency 'rspec', '~> 3.11.0' + spec.add_development_dependency 'rspec', '~> 3.12.0' end From 97c66aed5880afa12e276830a93bcf99ec0acf85 Mon Sep 17 00:00:00 2001 From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com> Date: Sun, 5 Nov 2023 20:20:57 +0000 Subject: [PATCH 2/5] Use `console` to set logger level --- lib/shodanz.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/shodanz.rb b/lib/shodanz.rb index 45fd1ae..f1fb364 100644 --- a/lib/shodanz.rb +++ b/lib/shodanz.rb @@ -2,14 +2,14 @@ require 'json' require 'async' +require 'console' require 'async/http/internet' require 'shodanz/version' require 'shodanz/errors' require 'shodanz/api' require 'shodanz/client' -# disable async logs by default -Async.logger.level = 4 +Console.logger.level = 4 # Shodanz is a modern Ruby gem for Shodan, the world's # first search engine for Internet-connected devices. From 62b8ff9697a0b6d728a5b47e0d6c93de00b7c392 Mon Sep 17 00:00:00 2001 From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com> Date: Sun, 5 Nov 2023 20:21:35 +0000 Subject: [PATCH 3/5] Use `async-rspec` for tests --- spec/exploits_api_spec.rb | 22 +- spec/rest_api_spec.rb | 417 ++++++++----------------------------- spec/streaming_api_spec.rb | 20 +- 3 files changed, 98 insertions(+), 361 deletions(-) diff --git a/spec/exploits_api_spec.rb b/spec/exploits_api_spec.rb index f3812aa..8d6c04d 100644 --- a/spec/exploits_api_spec.rb +++ b/spec/exploits_api_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" RSpec.describe Shodanz::API::Exploits do + include_context Async::RSpec::Reactor + before do @client = Shodanz.api.exploits.new end @@ -11,24 +13,10 @@ end describe '#search' do - def check - if Async::Task.current? + it 'should search across a variety of data sources for exploits' do + reactor.async do resp = @client.search("SQL", port: 443).wait - else - resp = @client.search("SQL", port: 443) - end - expect(resp).to be_a(Hash) - end - - describe 'search across a variety of data sources for exploits' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end diff --git a/spec/rest_api_spec.rb b/spec/rest_api_spec.rb index d16bfb9..c285ec0 100644 --- a/spec/rest_api_spec.rb +++ b/spec/rest_api_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" RSpec.describe Shodanz::API::REST do + include_context Async::RSpec::Reactor + before do @client = Shodanz.api.rest.new end @@ -11,50 +13,22 @@ end describe '#scan' do - def check - if Async::Task.current? + it 'should be able to scan a host on the internet' do + reactor.async do resp = @client.scan("1.1.1.1").wait - else - resp = @client.scan("1.1.1.1") - end - expect(resp).to be_a(Hash) - expect(resp["count"]).to be_a(Integer) - expect(resp["id"]).to be_a(String) - expect(resp["credits_left"]).to be_a(Integer) - end - - describe 'scans host on the internet' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) + expect(resp["count"]).to be_a(Integer) + expect(resp["id"]).to be_a(String) + expect(resp["credits_left"]).to be_a(Integer) end end end describe '#info' do - def check - if Async::Task.current? + it 'returns info about the underlying token' do + reactor.async do resp = @client.info.wait - else - resp = @client.info - end - expect(resp).to be_a(Hash) - end - - describe 'returns info about the underlying token' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end @@ -62,24 +36,10 @@ def check describe '#host' do let(:ip) { "8.8.8.8" } - def check - if Async::Task.current? + it 'returns all services that have been found on the given host IP' do + reactor.async do resp = @client.host(ip).wait - else - resp = @client.host(ip) - end - expect(resp).to be_a(Hash) - end - - describe 'returns all services that have been found on the given host IP' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end @@ -87,24 +47,10 @@ def check describe '#host_count' do let(:query) { "apache" } - def check - if Async::Task.current? + it 'returns the total number of results that matches a given query' do + reactor.async do resp = @client.host_count(query).wait - else - resp = @client.host_count(query) - end - expect(resp).to be_a(Hash) - end - - describe 'returns the total number of results that matches a given query' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end @@ -112,24 +58,10 @@ def check describe '#host_search' do let(:query) { "apache" } - def check - if Async::Task.current? + it 'returns the total number of results that matches a given query' do + reactor.async do resp = @client.host_search(query).wait - else - resp = @client.host_search(query) - end - expect(resp).to be_a(Hash) - end - - describe 'returns the total number of results that matches a given query' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end @@ -137,136 +69,67 @@ def check describe '#host_search_tokens' do let(:query) { "apache" } - def check - if Async::Task.current? + it 'returns a parsed version of the query' do + reactor.async do resp = @client.host_search_tokens(query).wait - else - resp = @client.host_search_tokens(query) - end - expect(resp).to be_a(Hash) - expect(resp['attributes']).to be_a(Hash) - expect(resp['errors']).to be_a(Array) - expect(resp['filters']).to be_a(Array) - expect(resp['string']).to be_a(String) - expect(resp['string']).to eq(query) - end - - describe 'returns a parsed version of the query' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) + expect(resp['attributes']).to be_a(Hash) + expect(resp['errors']).to be_a(Array) + expect(resp['filters']).to be_a(Array) + expect(resp['string']).to be_a(String) + expect(resp['string']).to eq(query) end end end describe '#ports' do - def check - if Async::Task.current? + it 'returns a list of port numbers that the crawlers are looking for' do + reactor.async do resp = @client.ports.wait - else - resp = @client.ports - end - expect(resp).to be_a(Array) - end - - describe 'returns a list of port numbers that the crawlers are looking for' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Array) end end end describe '#protocols' do - def check - if Async::Task.current? + it 'returns all protocols that can be used when performing on-demand scans' do + reactor.async do resp = @client.protocols.wait - else - resp = @client.protocols - end - expect(resp).to be_a(Hash) - end - - describe 'returns all protocols that can be used when performing on-demand scans' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end describe '#profile' do - def check - if Async::Task.current? + it 'returns information about the shodan account' do + reactor.async do resp = @client.profile.wait - else - resp = @client.profile - end - expect(resp).to be_a(Hash) - expect(resp["member"]).to be(true).or be(false) - expect(resp["credits"]).to be_a(Integer) - expect(resp["created"]).to be_a(String) - expect(resp.key?("display_name")).to be(true) - end - - describe 'returns information about the shodan account' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) + expect(resp["member"]).to be(true).or be(false) + expect(resp["credits"]).to be_a(Integer) + expect(resp["created"]).to be_a(String) + expect(resp.key?("display_name")).to be(true) end end end describe '#community_queries' do - def check - if Async::Task.current? + it 'obtains a list of search queries that users have saved' do + reactor.async do resp = @client.community_queries.wait - else - resp = @client.community_queries - end - expect(resp).to be_a(Hash) - expect(resp['total']).to be_a(Integer) - expect(resp['matches']).to be_a(Array) - - example_match = resp['matches'].first - - expect(example_match['votes']).to be_a(Integer) - expect(example_match['description']).to be_a(String) - expect(example_match['tags']).to be_a(Array) - expect(example_match['timestamp']).to be_a(String) - expect(example_match['title']).to be_a(String) - expect(example_match['query']).to be_a(String) - end - describe 'obtains a list of search queries that users have saved' do - it 'works synchronously' do - check - end + expect(resp).to be_a(Hash) + expect(resp['total']).to be_a(Integer) + expect(resp['matches']).to be_a(Array) + + example_match = resp['matches'].first - it 'works asynchronously' do - Async do - check - end + expect(example_match['votes']).to be_a(Integer) + expect(example_match['description']).to be_a(String) + expect(example_match['tags']).to be_a(Array) + expect(example_match['timestamp']).to be_a(String) + expect(example_match['title']).to be_a(String) + expect(example_match['query']).to be_a(String) end end end @@ -274,35 +137,22 @@ def check describe '#search_for_community_query' do let(:query) { "apache" } - def check - if Async::Task.current? + it 'search the directory of search queries that users have saved' do + reactor.async do resp = @client.search_for_community_query(query).wait - else - resp = @client.search_for_community_query(query) - end - expect(resp).to be_a(Hash) - expect(resp['total']).to be_a(Integer) - expect(resp['matches']).to be_a(Array) - - example_match = resp['matches'].first - - expect(example_match['votes']).to be_a(Integer) - expect(example_match['description']).to be_a(String) - expect(example_match['tags']).to be_a(Array) - expect(example_match['timestamp']).to be_a(String) - expect(example_match['title']).to be_a(String) - expect(example_match['query']).to be_a(String) - end - describe 'search the directory of search queries that users have saved' do - it 'works synchronously' do - check - end + expect(resp).to be_a(Hash) + expect(resp['total']).to be_a(Integer) + expect(resp['matches']).to be_a(Array) - it 'works asynchronously' do - Async do - check - end + example_match = resp['matches'].first + + expect(example_match['votes']).to be_a(Integer) + expect(example_match['description']).to be_a(String) + expect(example_match['tags']).to be_a(Array) + expect(example_match['timestamp']).to be_a(String) + expect(example_match['title']).to be_a(String) + expect(example_match['query']).to be_a(String) end end end @@ -310,24 +160,10 @@ def check describe '#resolve' do let(:hostname) { "google.com" } - def check - if Async::Task.current? + it 'resolves domains to ip addresses' do + reactor.async do resp = @client.resolve(hostname).wait - else - resp = @client.resolve(hostname) - end - expect(resp).to be_a(Hash) - end - - describe 'resolves domains to ip addresses' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) end end end @@ -335,117 +171,38 @@ def check describe '#reverse_lookup' do let(:ip) { '8.8.8.8' } - def check - if Async::Task.current? + it 'resolves ip addresses to domains' do + reactor.async do resp = @client.reverse_lookup(ip).wait - else - resp = @client.reverse_lookup(ip) - end - expect(resp).to be_a(Hash) - expect('8.8.8.8').not_to be nil - - # NOTE: this was the old behavior... - # - # Now it's in the form ip => [ dns_names ... ] - # {"8.8.8.8"=>["dns.google"]} - # - # expect(resp[ip]).to be_a(Array) - # expect(resp[ip].first).to eq('google-public-dns-a.google.com') - end - - describe 'resolves ip addresses to domains' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) + expect('8.8.8.8').not_to be nil end end end describe '#http_headers' do - def check - if Async::Task.current? + it 'shows the HTTP headers that your client sends when connecting to a webserver' do + reactor.async do resp = @client.http_headers.wait - else - resp = @client.http_headers - end - expect(resp).to be_a(Hash) - expect(resp['Content-Length']).to be_a(String) - expect(resp['Content-Length']).to eq('') - # TODO maybe specify a content-type? - expect(resp['Content-Type']).to be_a(String) - expect(resp['Content-Type']).to eq('') - expect(resp['Host']).to be_a(String) - expect(resp['Host']).to eq('api.shodan.io') - end - describe 'shows the HTTP headers that your client sends when connecting to a webserver' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(Hash) + expect(resp['Content-Length']).to be_a(String) + expect(resp['Content-Length']).to eq('') + # TODO maybe specify a content-type? + expect(resp['Content-Type']).to be_a(String) + expect(resp['Content-Type']).to eq('') + expect(resp['Host']).to be_a(String) + expect(resp['Host']).to eq('api.shodan.io') end end end describe '#my_ip' do - def check - if Async::Task.current? + it 'shows the current IP address as seen from the internet' do + reactor.async do resp = @client.my_ip.wait - else - resp = @client.my_ip - end - expect(resp).to be_a(String) - end - - describe 'shows the current IP address as seen from the internet' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check - end + expect(resp).to be_a(String) end end end - - # This no longer seems to be available, or at least it's not documented - # clearly anywhere I can find. It just stopped working. - # - # describe '#honeypot_score' do - # let(:ip) { '8.8.8.8' } - # - # def check - # if Async::Task.current? - # resp = @client.honeypot_score(ip).wait - # else - # resp = @client.honeypot_score(ip) - # end - # expect(resp).to be_a(Float) - # expect(resp).to eq(0.0) - # end - # - # describe 'returns the calculated likelihood a given IP is a honeypot' do - # it 'works synchronously' do - # check - # end - # - # it 'works asynchronously' do - # Async do - # check - # end - # end - # end - # end - end diff --git a/spec/streaming_api_spec.rb b/spec/streaming_api_spec.rb index 2f0de45..899562f 100644 --- a/spec/streaming_api_spec.rb +++ b/spec/streaming_api_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" RSpec.describe Shodanz::API::Streaming do + include_context Async::RSpec::Reactor + before do @client = Shodanz.api.streaming.new end @@ -11,20 +13,10 @@ end describe '#banners' do - def check - @client.banners(limit: 1) do |banner| - expect(banner).to be_a(Hash) - end - end - - describe 'stream any banner data Shodan collects' do - it 'works synchronously' do - check - end - - it 'works asynchronously' do - Async do - check + it "should stream any banner data Shodan collects" do + reactor.async do + @client.banners(limit: 1) do |banner| + expect(banner).to be_a(Hash) end end end From c0083650a5c1f73bfc654f795901c586b25d4dbf Mon Sep 17 00:00:00 2001 From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com> Date: Sun, 5 Nov 2023 20:24:36 +0000 Subject: [PATCH 4/5] Update `dependabot.yml` --- .github/dependabot.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d169ed9..56add0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,10 +6,3 @@ updates: interval: daily time: "10:00" open-pull-requests-limit: 10 - ignore: - - dependency-name: async-http - versions: - - 0.55.0 - - dependency-name: pry - versions: - - 0.14.0 From d535a01e869d2d3e0a574781be82d159ac3a92e1 Mon Sep 17 00:00:00 2001 From: Kent 'picat' Gruber <kent.picat.gruber@gmail.com> Date: Sun, 5 Nov 2023 20:24:54 +0000 Subject: [PATCH 5/5] Bump `ruby` version in GHA tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15c1146..36378fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: '3.2' bundler-cache: true - name: RSpec env: