virustotal_api 0.2.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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