virustotal_api 0.1.0 → 0.5.0

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 (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