diff --git a/README.md b/README.md index c9c9cb3..fce1d68 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Crystal SDK and command line client for [SonarSearch/Crobat](https://github.com/ ## Download Prebuilt binary releases available [here](https://github.com/PercussiveElbow/crobat-sdk-crystal/releases). -Preuilt Docker container available [here](https://github.com/users/PercussiveElbow/packages/container/package/crobat). +Prebuilt Docker container available [here](https://github.com/users/PercussiveElbow/packages/container/package/crobat). To grab the latest release: @@ -20,19 +20,19 @@ To run the latest Docker container (~20MB) ``` Usage: ./crobat_client [arguments] - -d DOMAIN, --domain Target domain. - -s TYPE, --type Search type. (SUBDOMAIN, ALL, TLD) + -q QUERY, --query Target domain, IP or IP range to query. + -s TYPE, --type Search type. (SUBDOMAIN, ALL, REVERSE TLD) -f FORMAT, --format File output format. (JSON, TXT, CSV) -o FILE, --output File output location. -h, --help Show help. -E.g ./crobat_client -d twitter.com -s subdomain +E.g ./crobat_client -q twitter.com -s subdomain ``` ### Client - Docker To build and use the Docker container: ``` - sudo docker build . -t crobat && sudo docker run -it crobat -d twitter.com -s subdomain + sudo docker build . -t crobat && sudo docker run -it crobat -q twitter.com -s subdomain ``` ## Usage - SDK @@ -58,6 +58,12 @@ puts(client.retrieve_all("twitter.com")) # Retrieving tlds via SDK puts(client.retrieve_tlds("twitter.com")) + +# Retrieving reverse info for an IP via SDK +puts(client.retrieve_reverse("8.8.8.8")) + +# Retrieving reverse info for an IP range via SDK +puts(client.retrieve_reverse_range("95.138.157.0/16")) ``` ## Building manually diff --git a/src/crobat_client.cr b/src/crobat_client.cr index 6301812..b3841fb 100644 --- a/src/crobat_client.cr +++ b/src/crobat_client.cr @@ -8,7 +8,7 @@ def output_txt_or_csv(output_results : Array(String), output_path : String, outp end end -def output_to_file(results, output_format : String, output_path : String, search_type : String) # needs cleaned up +def output_to_file(results, output_format : String, output_path : String, search_type : String, query : String) output_file = File.open output_path, "w" if results.is_a?(Array(String)) @@ -18,11 +18,29 @@ def output_to_file(results, output_format : String, output_path : String, search when "csv" output_txt_or_csv(results.insert(0,search_type == "tld" ? "tld" : "subdomain"), output_path, output_file) when "json" - output_file.puts({search_type => results}.to_json.to_s) + output_file.puts({query => results}.to_json.to_s) else puts("Unable to output file") end + elsif results.is_a?(Hash(String,Array(String))) + organised_results = [] of String + results.each_key do | key | + results[key].each do | value | + organised_results.push(key + ", " + value) + end + end + + case output_format.downcase + when "txt" + output_txt_or_csv(organised_results, output_path, output_file) + when "csv" + output_txt_or_csv(organised_results.insert(0,"IP, subdomain"), output_path, output_file) + when "json" + output_file.puts({query => results}.to_json.to_s) + else + puts("Unable to output file") + end elsif results.is_a?(Array(Crobat::CrobatSDK::SonarAllResult)) organised_results = [] of String results.each do | result | @@ -52,8 +70,8 @@ def cli OptionParser.parse do |parser| parser.banner = "Crobat Client.\nUsage: ./crobat.cr [arguments]" - parser.on("-d DOMAIN", "--domain", "Target domain.") { |domain| target = domain } - parser.on("-s TYPE","--type", "Search type. (SUBDOMAIN, ALL, TLD)") { |type| search_type = type } + parser.on("-q QUERY", "--query", "Domain, IP or IP range to query.") { |query| target = query } + parser.on("-s TYPE","--type", "Search type. (SUBDOMAIN, TLD, REVERSE, ALL)") { |type| search_type = type } parser.on("-f FORMAT", "--format","File output format. (JSON, TXT, CSV)") { |format| output_format = format} parser.on("-o FILE", "--output","File output location.") { |file| output_path = file} parser.on("-h", "--help", "Show help.") { puts parser } @@ -71,6 +89,12 @@ def cli results = client.retrieve_tlds(target) when "subdomain" results = client.retrieve_subdomains(target) + when "reverse" + if target.includes?("/") + results = client.retrieve_reverse_range(target) + else + results = client.retrieve_reverse(target) + end else puts("Invalid search type supplied. Valid search types: SUBDOMAIN, ALL, TLD") end @@ -81,7 +105,7 @@ def cli end if output_format.size() > 0 && output_path.size() > 0 - output_to_file(results, output_format, output_path,search_type) + output_to_file(results, output_format, output_path, search_type, target) end end diff --git a/src/crobat_sdk.cr b/src/crobat_sdk.cr index 3a0ec08..cb79ba0 100644 --- a/src/crobat_sdk.cr +++ b/src/crobat_sdk.cr @@ -16,7 +16,7 @@ module Crobat resp = HTTP::Client.get("#{@api_url}/subdomains/#{domain}#{page_query}") if resp.status_code == 200 items = Array(String).from_json(resp.body) - return items.size()>=999 ? items + retrieve_subdomains(domain,page+1) : items + return items.size()>=9999 ? items + retrieve_subdomains(domain,page+1) : items else raise CrobatQueryException.new("Error querying Subdomains API Endpoint #{@api_url} with domain #{domain}. Status code #{resp.status_code}") end @@ -32,7 +32,7 @@ module Crobat resp = HTTP::Client.get("#{@api_url}/tlds/#{search_query}#{page_query}") if resp.status_code == 200 items = Array(String).from_json(resp.body) - return items.size()>=999 ? items + retrieve_tlds(search_query,page+1) : items + return items.size()>=9999 ? items + retrieve_tlds(search_query,page+1) : items else raise CrobatQueryException.new("Error querying TLD API Endpoint #{@api_url} with TLD #{search_query}. Status code #{resp.status_code}") end @@ -48,7 +48,7 @@ module Crobat resp = HTTP::Client.get( "#{@api_url}/all/#{search_query}#{page_query}") if resp.status_code == 200 items = Array(SonarAllResult).from_json(resp.body) - if items.size()>=999 + if items.size()>=9999 recursive_items = retrieve_all(search_query,page+1) return recursive_items.size()> 0 ? items + recursive_items : items else @@ -64,6 +64,40 @@ module Crobat return [] of SonarAllResult end end + + def retrieve_reverse_range(search_query : String, page : Int = 0) : Hash(String,Array(String)) + begin + page_query = page > 0 ? "?page=#{page.to_s}" : "" + resp = HTTP::Client.get( "#{@api_url}/reverse/#{search_query}#{page_query}") + if resp.status_code == 200 + return Hash(String,Array(String)).from_json(resp.body) + else + puts(resp.headers) + raise CrobatQueryException.new("Error querying Reverse API Endpoint #{@api_url} with query #{search_query}. Status code #{resp.status_code}") + return Hash(String,Array(String)).new + end + rescue ex : Exception + print(ex) + return Hash(String,Array(String)).new + end + end + + def retrieve_reverse(search_query : String, page : Int = 0) : Array(String) + begin + page_query = page > 0 ? "?page=#{page.to_s}" : "" + resp = HTTP::Client.get( "#{@api_url}/reverse/#{search_query}#{page_query}") + if resp.status_code == 200 + return Array(String).from_json(resp.body) + else + puts(resp.headers) + raise CrobatQueryException.new("Error querying Reverse API Endpoint #{@api_url} with query #{search_query}. Status code #{resp.status_code}") + return [] of String + end + rescue ex : Exception + print(ex) + return [] of String + end + end class SonarAllResult include JSON::Serializable property name : String