virustotal_api 0.2.0 → 0.5.1

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.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODE_OF_CONDUCT.md +46 -0
  3. data/.github/CONTRIBUTING.md +7 -0
  4. data/.github/ISSUE_TEMPLATE.md +15 -0
  5. data/.github/workflows/ruby.yml +26 -0
  6. data/.gitignore +1 -0
  7. data/.rubocop.yml +17 -5
  8. data/CHANGELOG.md +33 -4
  9. data/Gemfile +1 -1
  10. data/README.md +83 -31
  11. data/Rakefile +3 -2
  12. data/lib/virustotal_api.rb +7 -6
  13. data/lib/virustotal_api/analysis.rb +24 -0
  14. data/lib/virustotal_api/base.rb +37 -10
  15. data/lib/virustotal_api/domain.rb +24 -0
  16. data/lib/virustotal_api/exceptions.rb +5 -0
  17. data/lib/virustotal_api/file.rb +56 -0
  18. data/lib/virustotal_api/ip.rb +24 -0
  19. data/lib/virustotal_api/uri.rb +4 -2
  20. data/lib/virustotal_api/url.rb +46 -0
  21. data/lib/virustotal_api/version.rb +4 -2
  22. data/test/analysis_test.rb +23 -0
  23. data/test/base_test.rb +10 -29
  24. data/test/domain_test.rb +32 -0
  25. data/test/exceptions_test.rb +16 -1
  26. data/test/file_test.rb +68 -0
  27. data/test/fixtures/analysis.yml +544 -0
  28. data/test/fixtures/domain.yml +830 -0
  29. data/test/fixtures/file_analyse.yml +52 -0
  30. data/test/fixtures/file_find.yml +1236 -0
  31. data/test/fixtures/file_unauthorized.yml +51 -0
  32. data/test/fixtures/file_upload.yml +54 -0
  33. data/test/fixtures/ip.yml +716 -0
  34. data/test/fixtures/unscanned_url_find.yml +44 -0
  35. data/test/fixtures/url_analyse.yml +52 -0
  36. data/test/fixtures/url_find.yml +599 -0
  37. data/test/{ip_report_test.rb → ip_test.rb} +6 -5
  38. data/test/test_helper.rb +2 -1
  39. data/test/uri_test.rb +3 -2
  40. data/test/url_test.rb +65 -0
  41. data/test/version_test.rb +3 -3
  42. data/virustotal_api.gemspec +16 -13
  43. metadata +108 -77
  44. data/.travis.yml +0 -15
  45. data/lib/virustotal_api/domain_report.rb +0 -35
  46. data/lib/virustotal_api/file_report.rb +0 -36
  47. data/lib/virustotal_api/file_scan.rb +0 -36
  48. data/lib/virustotal_api/ip_report.rb +0 -35
  49. data/lib/virustotal_api/url_report.rb +0 -37
  50. data/test/domain_report_test.rb +0 -31
  51. data/test/file_report_test.rb +0 -34
  52. data/test/file_scan_test.rb +0 -29
  53. data/test/fixtures/domain_report.yml +0 -311
  54. data/test/fixtures/ip_report.yml +0 -1323
  55. data/test/fixtures/report.yml +0 -110
  56. data/test/fixtures/report_not_found.yml +0 -42
  57. data/test/fixtures/request_forbidden.yml +0 -38
  58. data/test/fixtures/scan.yml +0 -49
  59. data/test/fixtures/url_report.yml +0 -95
  60. data/test/url_report_test.rb +0 -39
@@ -1,22 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'virustotal_api/exceptions'
2
4
  require 'rest-client'
3
5
  require 'json'
6
+ require 'base64'
4
7
 
8
+ # The base VirustotalAPI module.
5
9
  module VirustotalAPI
10
+ # The base class implementing the raw calls to Virustotal API V3.
6
11
  class Base
12
+ attr_reader :report
13
+
14
+ def initialize(report)
15
+ @report = report
16
+ end
17
+
7
18
  # @return [String] string of API URI class method
8
19
  def self.api_uri
9
20
  VirustotalAPI::URI
10
21
  end
11
22
 
12
- # @param [RestClient::Response] response
13
- # @return [Hash] the parsed JSON.
14
- def self.parse(response)
15
- if response.code == 204
16
- fail(RateLimitError, 'maximum number of 4 requests per minute reached')
17
- end
18
-
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
+ )
19
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
20
44
  end
21
45
 
22
46
  # @return [String] string of API URI instance method
@@ -25,11 +49,14 @@ module VirustotalAPI
25
49
  end
26
50
 
27
51
  # @return [Boolean] if report for resource exists
28
- # 0 => not_present, 1 => exists, -1 => invalid_ip_address
29
52
  def exists?
30
- response_code = report.fetch('response_code') { nil }
53
+ !report.empty?
54
+ end
31
55
 
32
- 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('=', '')
33
60
  end
34
61
  end
35
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
@@ -1,4 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module VirustotalAPI
2
4
  class RateLimitError < RuntimeError
3
5
  end
6
+
7
+ class Unauthorized < RuntimeError
8
+ end
4
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.2.0'
4
+ # The GEM version
5
+ VERSION = '0.5.1'
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
@@ -14,45 +14,26 @@ class VirustotalAPIBaseTest < Minitest::Test
14
14
 
15
15
  # Instance Method
16
16
  def test_api_uri_instance_method
17
- base_uri = 'https://www.virustotal.com/vtapi/v2'
18
- vt_base = VirustotalAPI::Base.new
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
25
  def test_api_uri_class_method
26
- base_uri = 'https://www.virustotal.com/vtapi/v2'
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
30
- end
31
-
32
- def test_parse_code_200
33
- mock_response_200 = Minitest::Mock.new
34
- mock_response_200.expect(:code, 200)
35
- mock_response_200.expect(:body, '{}')
36
-
37
- assert VirustotalAPI::Base.parse(mock_response_200), {}
38
- end
39
-
40
- def test_parse_code_204
41
- mock_response_204 = Minitest::Mock.new
42
- mock_response_204.expect(:code, 204)
43
- mock_response_204.expect(:body, '{}')
44
-
45
- assert_raises VirustotalAPI::RateLimitError do
46
- VirustotalAPI::Base.parse(mock_response_204)
47
- end
29
+ assert_equal base_uri, VirustotalAPI::Base.api_uri
48
30
  end
49
31
 
50
- # Test using FileReport
51
32
  def test_exists?
52
- VCR.use_cassette('report') do
53
- virustotal_report = VirustotalAPI::FileReport.find(@sha256, @api_key)
33
+ VCR.use_cassette('file_find') do
34
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
54
35
 
55
- assert virustotal_report.exists?, true
36
+ assert virustotal_report.exists?
56
37
  end
57
38
  end
58
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
@@ -1,8 +1,23 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  require './test/test_helper'
3
4
 
4
5
  class RateLimitErrorTest < Minitest::Test
6
+ def setup
7
+ @sha256 = '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
8
+ @api_key = 'testapikey'
9
+ end
10
+
5
11
  def test_class_exists
6
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
7
22
  end
8
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