virustotal_api 0.1.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +23 -0
  3. data/.github/CODE_OF_CONDUCT.md +46 -0
  4. data/.github/CONTRIBUTING.md +7 -0
  5. data/.github/ISSUE_TEMPLATE.md +15 -0
  6. data/.github/workflows/ruby.yml +26 -0
  7. data/.gitignore +1 -0
  8. data/.rubocop.yml +17 -5
  9. data/CHANGELOG.md +34 -0
  10. data/Gemfile +1 -1
  11. data/README.md +88 -31
  12. data/Rakefile +3 -2
  13. data/lib/virustotal_api.rb +7 -6
  14. data/lib/virustotal_api/analysis.rb +24 -0
  15. data/lib/virustotal_api/base.rb +41 -3
  16. data/lib/virustotal_api/domain.rb +24 -0
  17. data/lib/virustotal_api/exceptions.rb +9 -0
  18. data/lib/virustotal_api/file.rb +56 -0
  19. data/lib/virustotal_api/ip.rb +24 -0
  20. data/lib/virustotal_api/uri.rb +4 -2
  21. data/lib/virustotal_api/url.rb +46 -0
  22. data/lib/virustotal_api/version.rb +4 -2
  23. data/test/analysis_test.rb +23 -0
  24. data/test/base_test.rb +12 -13
  25. data/test/domain_test.rb +32 -0
  26. data/test/exceptions_test.rb +23 -0
  27. data/test/file_test.rb +68 -0
  28. data/test/fixtures/analysis.yml +544 -0
  29. data/test/fixtures/domain.yml +830 -0
  30. data/test/fixtures/file_analyse.yml +52 -0
  31. data/test/fixtures/file_find.yml +1236 -0
  32. data/test/fixtures/file_unauthorized.yml +51 -0
  33. data/test/fixtures/file_upload.yml +54 -0
  34. data/test/fixtures/ip.yml +716 -0
  35. data/test/fixtures/unscanned_url_find.yml +44 -0
  36. data/test/fixtures/url_analyse.yml +52 -0
  37. data/test/fixtures/url_find.yml +599 -0
  38. data/test/{ip_report_test.rb → ip_test.rb} +6 -5
  39. data/test/test_helper.rb +2 -1
  40. data/test/uri_test.rb +3 -2
  41. data/test/url_test.rb +65 -0
  42. data/test/version_test.rb +3 -3
  43. data/virustotal_api.gemspec +16 -13
  44. metadata +113 -77
  45. data/.travis.yml +0 -11
  46. data/lib/virustotal_api/domain_report.rb +0 -35
  47. data/lib/virustotal_api/file_report.rb +0 -36
  48. data/lib/virustotal_api/file_scan.rb +0 -36
  49. data/lib/virustotal_api/ip_report.rb +0 -35
  50. data/lib/virustotal_api/url_report.rb +0 -37
  51. data/test/domain_report_test.rb +0 -31
  52. data/test/file_report_test.rb +0 -34
  53. data/test/file_scan_test.rb +0 -29
  54. data/test/fixtures/domain_report.yml +0 -311
  55. data/test/fixtures/ip_report.yml +0 -1323
  56. data/test/fixtures/report.yml +0 -110
  57. data/test/fixtures/report_not_found.yml +0 -42
  58. data/test/fixtures/request_forbidden.yml +0 -38
  59. data/test/fixtures/scan.yml +0 -49
  60. data/test/fixtures/url_report.yml +0 -95
  61. data/test/url_report_test.rb +0 -39
@@ -1,8 +1,9 @@
1
- # encoding: utf-8
2
- require 'virustotal_api/domain_report'
3
- require 'virustotal_api/file_report'
4
- require 'virustotal_api/file_scan'
5
- require 'virustotal_api/ip_report'
6
- require 'virustotal_api/url_report'
1
+ # frozen_string_literal: true
2
+
3
+ require 'virustotal_api/analysis'
4
+ require 'virustotal_api/domain'
5
+ require 'virustotal_api/file'
6
+ require 'virustotal_api/ip'
7
+ require 'virustotal_api/url'
7
8
  require 'virustotal_api/uri'
8
9
  require 'virustotal_api/version'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/analyses' API
7
+ class Analysis < Base
8
+ attr_reader :report
9
+
10
+ # rubocop:disable Lint/MissingSuper
11
+ def initialize(report)
12
+ @report = report
13
+ end
14
+
15
+ # @param [String] id The Virustotal ID to get the report for.
16
+ # @param [String] api_key The key for virustotal
17
+ # @return [VirustotalAPI::IP] Report
18
+ def self.find(id, api_key)
19
+ report = perform("/analyses/#{id}", api_key)
20
+ new(report)
21
+ end
22
+ end
23
+ end
24
+ # rubocop:enable Lint/MissingSuper
@@ -1,24 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'virustotal_api/exceptions'
1
4
  require 'rest-client'
2
5
  require 'json'
6
+ require 'base64'
3
7
 
8
+ # The base VirustotalAPI module.
4
9
  module VirustotalAPI
10
+ # The base class implementing the raw calls to Virustotal API V3.
5
11
  class Base
12
+ attr_reader :report
13
+
14
+ def initialize(report)
15
+ @report = report
16
+ end
17
+
6
18
  # @return [String] string of API URI class method
7
19
  def self.api_uri
8
20
  VirustotalAPI::URI
9
21
  end
10
22
 
23
+ # The actual method performing a call to Virustotal
24
+ #
25
+ # @param [String] url The url of the API
26
+ # @param [String] api_key The key for virustotal
27
+ # @param [String] method The HTTP method to use
28
+ # @param [Hash] options Options to pass as payload
29
+ # @return [VirustotalAPI::Domain] Report Search Result
30
+ def self.perform(url, api_key, method = :get, options = {})
31
+ response = RestClient::Request.execute(
32
+ method: method,
33
+ url: api_uri + url,
34
+ headers: { 'x-apikey': api_key },
35
+ payload: options
36
+ )
37
+ JSON.parse(response.body)
38
+ rescue RestClient::NotFound
39
+ nil
40
+ rescue RestClient::Unauthorized
41
+ # Raise a custom exception not to expose the underlying
42
+ # HTTP client.
43
+ raise VirustotalAPI::Unauthorized
44
+ end
45
+
11
46
  # @return [String] string of API URI instance method
12
47
  def api_uri
13
48
  self.class.api_uri
14
49
  end
15
50
 
16
51
  # @return [Boolean] if report for resource exists
17
- # 0 => not_present, 1 => exists, -1 => invalid_ip_address
18
52
  def exists?
19
- response_code = report.fetch('response_code') { nil }
53
+ !report.empty?
54
+ end
20
55
 
21
- response_code == 1 ? true : false
56
+ # Generate a URL identifier.
57
+ # @see https://developers.virustotal.com/v3.0/reference#url
58
+ def self.url_identifier(url)
59
+ Base64.encode64(url).strip.gsub('=', '')
22
60
  end
23
61
  end
24
62
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/domains' API
7
+ class Domain < Base
8
+ # rubocop:disable Lint/UselessMethodDefinition
9
+ def initialize(report)
10
+ super(report)
11
+ end
12
+
13
+ # Find a domain.
14
+ #
15
+ # @param [String] domain The domain to search
16
+ # @param [String] api_key for virustotal
17
+ # @return [VirustotalAPI::Domain] Report Search Result
18
+ def self.find(domain, api_key)
19
+ report = perform("/domains/#{domain}", api_key)
20
+ new(report)
21
+ end
22
+ end
23
+ end
24
+ # rubocop:enable Lint/UselessMethodDefinition
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VirustotalAPI
4
+ class RateLimitError < RuntimeError
5
+ end
6
+
7
+ class Unauthorized < RuntimeError
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/files' API
7
+ class File < Base
8
+ attr_reader :id, :report_url
9
+
10
+ def initialize(report)
11
+ super(report)
12
+ @id = report&.dig('data', 'id')
13
+ @report_url = report&.dig('data', 'links', 'self')
14
+ end
15
+
16
+ # Find a hash.
17
+ #
18
+ # @param [String] resource file as a md5/sha1/sha256 hash
19
+ # @param [String] api_key The key for virustotal
20
+ # @return [VirustotalAPI::File] Report Search Result
21
+ def self.find(resource, api_key)
22
+ report = perform("/files/#{resource}", api_key)
23
+ new(report)
24
+ end
25
+
26
+ # Upload a new file.
27
+ #
28
+ # @param [String] file_path for file to be sent for scan
29
+ # @param [String] api_key The key for virustotal
30
+ # @param [Hash] opts hash for additional options
31
+ # @return [VirusotalAPI::File] Report
32
+ def self.upload(file_path, api_key, opts = {})
33
+ filename = opts.fetch('filename') { ::File.basename(file_path) }
34
+ report = perform('/files', api_key, :post, filename: filename, file: ::File.open(file_path, 'r'))
35
+ new(report)
36
+ end
37
+
38
+ # Analyse a hash again.
39
+ #
40
+ # @param [String] resource file as a md5/sha1/sha256 hash
41
+ # @param [String] api_key The key for virustotal
42
+ # @return [VirustotalAPI::File] Report
43
+ def self.analyse(resource, api_key)
44
+ report = perform("/files/#{resource}/analyse", api_key, :post)
45
+ new(report)
46
+ end
47
+
48
+ # Check if the submitted hash is detected by an AV engine.
49
+ #
50
+ # @param [String] engine The engine to check.
51
+ # @return [Boolean] true if detected
52
+ def detected_by(engine)
53
+ report['data']['attributes']['last_analysis_results'][engine]['category'] == 'harmless'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/ip_addresses' API
7
+ class IP < Base
8
+ # rubocop:disable Lint/UselessMethodDefinition
9
+ def initialize(report)
10
+ super(report)
11
+ end
12
+
13
+ # Find an IP.
14
+ #
15
+ # @param [String] ip address The IP to find.
16
+ # @param [String] api_key The key for virustotal
17
+ # @return [VirustotalAPI::IPReport] Report
18
+ def self.find(ip, api_key)
19
+ report = perform("/ip_addresses/#{ip}", api_key)
20
+ new(report)
21
+ end
22
+ end
23
+ end
24
+ # rubocop:enable Lint/UselessMethodDefinition
@@ -1,4 +1,6 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  module VirustotalAPI
3
- URI = 'https://www.virustotal.com/vtapi/v2'
4
+ # The API base URI
5
+ URI = 'https://www.virustotal.com/api/v3'
4
6
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/urls' API
7
+ class URL < Base
8
+ attr_reader :report_url, :id
9
+
10
+ def initialize(report)
11
+ super(report)
12
+ @report_url = report&.dig('data', 'links', 'self')
13
+ @id = report&.dig('data', 'id')
14
+ end
15
+
16
+ # Find a URL.
17
+ #
18
+ # @param [String] resource as an ip/domain/url
19
+ # @param [String] api_key The key for virustotal
20
+ # @return [VirustotalAPI::URL] Report
21
+ def self.find(resource, api_key)
22
+ report = perform("/urls/#{url_identifier(resource)}", api_key)
23
+ new(report)
24
+ end
25
+
26
+ # Analyse a URL again.
27
+ #
28
+ # @param [String] resource as an ip/domain/url
29
+ # @param [String] api_key The key for virustotal
30
+ # @return [VirustotalAPI::URL] Report
31
+ def self.analyse(resource, api_key)
32
+ report = perform("/urls/#{url_identifier(resource)}/analyse", api_key, :post)
33
+ new(report)
34
+ end
35
+
36
+ # Upload a URL for detection.
37
+ #
38
+ # @param [String] resource as an ip/domain/url
39
+ # @param [String] api_key The key for virustotal
40
+ # @return [VirustotalAPI::URL] Report
41
+ def self.upload(resource, api_key)
42
+ report = perform('/urls', api_key, :post, url: resource)
43
+ new(report)
44
+ end
45
+ end
46
+ end
@@ -1,4 +1,6 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  module VirustotalAPI
3
- VERSION = '0.1.0'
4
+ # The GEM version
5
+ VERSION = '0.5.0'
4
6
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './test/test_helper'
4
+
5
+ class VirustotalAPIAnalysisTest < Minitest::Test
6
+ def setup
7
+ @url = 'http://www.google.com'
8
+ @api_key = 'theapikey'
9
+ end
10
+
11
+ def test_todo
12
+ VCR.use_cassette('url_find') do
13
+ vtreport = VirustotalAPI::URL.find(@url, @api_key)
14
+ @id = vtreport.id
15
+ assert @id
16
+ end
17
+
18
+ VCR.use_cassette('analysis') do
19
+ analysis = VirustotalAPI::Analysis.find(@id, @api_key)
20
+ assert analysis.exists?
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
- # encoding: utf-8
2
- # rubocop:disable LineLength
1
+ # frozen_string_literal: true
2
+
3
3
  require './test/test_helper'
4
4
 
5
5
  class VirustotalAPIBaseTest < Minitest::Test
@@ -13,28 +13,27 @@ class VirustotalAPIBaseTest < Minitest::Test
13
13
  end
14
14
 
15
15
  # Instance Method
16
- def test_api_uri
17
- base_uri = 'https://www.virustotal.com/vtapi/v2'
18
- vt_base = VirustotalAPI::Base.new
16
+ def test_api_uri_instance_method
17
+ base_uri = 'https://www.virustotal.com/api/v3'
18
+ vt_base = VirustotalAPI::Base.new(nil)
19
19
 
20
20
  assert vt_base.api_uri.is_a?(String)
21
- assert vt_base.api_uri, base_uri
21
+ assert_equal base_uri, vt_base.api_uri
22
22
  end
23
23
 
24
24
  # Class Method
25
- def test_api_uri
26
- base_uri = 'https://www.virustotal.com/vtapi/v2'
25
+ def test_api_uri_class_method
26
+ base_uri = 'https://www.virustotal.com/api/v3'
27
27
 
28
28
  assert VirustotalAPI::Base.api_uri.is_a?(String)
29
- assert VirustotalAPI::Base.api_uri, base_uri
29
+ assert_equal base_uri, VirustotalAPI::Base.api_uri
30
30
  end
31
31
 
32
- # Test using FileReport
33
32
  def test_exists?
34
- VCR.use_cassette('report') do
35
- virustotal_report = VirustotalAPI::FileReport.find(@sha256, @api_key)
33
+ VCR.use_cassette('file_find') do
34
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
36
35
 
37
- assert virustotal_report.exists?, true
36
+ assert virustotal_report.exists?
38
37
  end
39
38
  end
40
39
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './test/test_helper'
4
+
5
+ class VirustotalAPIDomainTest < Minitest::Test
6
+ def setup
7
+ @domain = 'virustotal.com'
8
+ @api_key = 'testapikey'
9
+ end
10
+
11
+ def test_class_exists
12
+ assert VirustotalAPI::Domain
13
+ end
14
+
15
+ def test_report_response
16
+ VCR.use_cassette('domain') do
17
+ vtdomain_report = VirustotalAPI::Domain.find(@domain, @api_key)
18
+
19
+ # Make sure that the JSON was parsed
20
+ assert vtdomain_report.is_a?(VirustotalAPI::Domain)
21
+ assert vtdomain_report.report.is_a?(Hash)
22
+ end
23
+ end
24
+
25
+ def test_exists?
26
+ VCR.use_cassette('domain') do
27
+ vtdomain_report = VirustotalAPI::Domain.find(@domain, @api_key)
28
+
29
+ assert vtdomain_report.exists?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './test/test_helper'
4
+
5
+ class RateLimitErrorTest < Minitest::Test
6
+ def setup
7
+ @sha256 = '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
8
+ @api_key = 'testapikey'
9
+ end
10
+
11
+ def test_class_exists
12
+ assert VirustotalAPI::RateLimitError
13
+ assert VirustotalAPI::Unauthorized
14
+ end
15
+
16
+ def test_unauthorized
17
+ VCR.use_cassette('file_unauthorized') do
18
+ assert_raises VirustotalAPI::Unauthorized do
19
+ VirustotalAPI::File.find(@sha256, @api_key)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './test/test_helper'
4
+
5
+ class VirustotalAPIFileTest < Minitest::Test
6
+ def setup
7
+ @sha256 = '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
8
+ @file_path = File.expand_path('test/fixtures/null_file')
9
+ @api_key = 'testapikey'
10
+ end
11
+
12
+ def test_class_exists
13
+ assert VirustotalAPI::File
14
+ end
15
+
16
+ def test_report_response
17
+ VCR.use_cassette('file_find') do
18
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
19
+
20
+ # Make sure that the JSON was parsed
21
+ assert virustotal_report.is_a?(VirustotalAPI::File)
22
+ assert virustotal_report.report.is_a?(Hash)
23
+ end
24
+ end
25
+
26
+ def test_find
27
+ permalink = 'https://www.virustotal.com/api/v3/files/' \
28
+ '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
29
+ VCR.use_cassette('file_find') do
30
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
31
+
32
+ assert virustotal_report.report_url.is_a?(String)
33
+ assert_equal permalink, virustotal_report.report_url
34
+ end
35
+ end
36
+
37
+ def test_upload
38
+ VCR.use_cassette('file_upload') do
39
+ virustotal_upload = VirustotalAPI::File.upload(@file_path, @api_key)
40
+
41
+ assert virustotal_upload.report.is_a?(Hash)
42
+ end
43
+ end
44
+
45
+ def test_upload_id
46
+ VCR.use_cassette('file_upload') do
47
+ virustotal_upload = VirustotalAPI::File.upload(@file_path, @api_key)
48
+
49
+ assert virustotal_upload.id.is_a?(String)
50
+ end
51
+ end
52
+
53
+ def test_analyse
54
+ VCR.use_cassette('file_analyse') do
55
+ virustotal_analyse = VirustotalAPI::File.analyse(@sha256, @api_key)
56
+
57
+ assert virustotal_analyse.report.is_a?(Hash)
58
+ end
59
+ end
60
+
61
+ def test_analyse_id
62
+ VCR.use_cassette('file_analyse') do
63
+ virustotal_analyse = VirustotalAPI::File.analyse(@sha256, @api_key)
64
+
65
+ assert virustotal_analyse.id.is_a?(String)
66
+ end
67
+ end
68
+ end