virustotal_api_compat 0.1.7

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODE_OF_CONDUCT.md +46 -0
  3. data/.github/CONTRIBUTING.md +7 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.github/pull_request_template.md +11 -0
  7. data/.github/workflows/ruby.yml +46 -0
  8. data/.gitignore +18 -0
  9. data/.rubocop.yml +39 -0
  10. data/CHANGELOG.md +70 -0
  11. data/Gemfile +5 -0
  12. data/LICENSE.txt +22 -0
  13. data/README.md +253 -0
  14. data/Rakefile +25 -0
  15. data/lib/virustotal_api/analysis.rb +16 -0
  16. data/lib/virustotal_api/base.rb +78 -0
  17. data/lib/virustotal_api/domain.rb +18 -0
  18. data/lib/virustotal_api/exceptions.rb +9 -0
  19. data/lib/virustotal_api/file.rb +67 -0
  20. data/lib/virustotal_api/group.rb +18 -0
  21. data/lib/virustotal_api/ip.rb +18 -0
  22. data/lib/virustotal_api/uri.rb +6 -0
  23. data/lib/virustotal_api/url.rb +38 -0
  24. data/lib/virustotal_api/user.rb +18 -0
  25. data/lib/virustotal_api/version.rb +6 -0
  26. data/lib/virustotal_api.rb +11 -0
  27. data/test/analysis_test.rb +26 -0
  28. data/test/base_test.rb +63 -0
  29. data/test/domain_test.rb +27 -0
  30. data/test/exceptions_test.rb +31 -0
  31. data/test/file_test.rb +73 -0
  32. data/test/fixtures/analysis.yml +544 -0
  33. data/test/fixtures/domain.yml +830 -0
  34. data/test/fixtures/domain_bad_request.yml +52 -0
  35. data/test/fixtures/file_analyse.yml +52 -0
  36. data/test/fixtures/file_find.yml +853 -0
  37. data/test/fixtures/file_not_found.yml +52 -0
  38. data/test/fixtures/file_rate_limit.yml +52 -0
  39. data/test/fixtures/file_unauthorized.yml +51 -0
  40. data/test/fixtures/file_upload.yml +54 -0
  41. data/test/fixtures/group_find.yml +216 -0
  42. data/test/fixtures/ip.yml +716 -0
  43. data/test/fixtures/large_file_upload.yml +99 -0
  44. data/test/fixtures/null_file +1 -0
  45. data/test/fixtures/unscanned_url_find.yml +44 -0
  46. data/test/fixtures/url_analyse.yml +52 -0
  47. data/test/fixtures/url_encoding_find.yml +651 -0
  48. data/test/fixtures/url_find.yml +599 -0
  49. data/test/fixtures/user_find.yml +213 -0
  50. data/test/group_test.rb +27 -0
  51. data/test/ip_test.rb +26 -0
  52. data/test/test_helper.rb +11 -0
  53. data/test/uri_test.rb +10 -0
  54. data/test/url_test.rb +47 -0
  55. data/test/user_test.rb +26 -0
  56. data/test/version_test.rb +9 -0
  57. data/virustotal_api.gemspec +33 -0
  58. metadata +287 -0
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'virustotal_api/exceptions'
4
+ require 'rest-client'
5
+ require 'json'
6
+ require 'base64'
7
+
8
+ # The base VirustotalAPI module.
9
+ module VirustotalAPI
10
+ # The base class implementing the raw calls to Virustotal API V3.
11
+ class Base
12
+ attr_reader :report, :report_url, :id
13
+
14
+ def initialize(report)
15
+ @report = report
16
+ @report_url = report&.dig('data', 'links', 'self')
17
+ @id = report&.dig('data', 'id')
18
+ end
19
+
20
+ # @return [String] string of API URI class method
21
+ def self.api_uri
22
+ VirustotalAPI::URI
23
+ end
24
+
25
+ def self.perform(path, api_key, method = :get, options = {})
26
+ base_perform(api_uri + path, api_key, method, options)
27
+ end
28
+
29
+ def self.perform_absolute(url, api_key, method = :get, options = {})
30
+ base_perform(url, api_key, method, options)
31
+ end
32
+
33
+ # The actual method performing a call to Virustotal
34
+ #
35
+ # @param [String] url The url of the API
36
+ # @param [String] api_key The key for virustotal
37
+ # @param [String] method The HTTP method to use
38
+ # @param [Hash] options Options to pass as payload
39
+ # @return [VirustotalAPI::Domain] Report Search Result
40
+ def self.base_perform(url, api_key, method = :get, options = {})
41
+ response = RestClient::Request.execute(
42
+ method: method,
43
+ url: url,
44
+ headers: { 'x-apikey': api_key },
45
+ payload: options
46
+ )
47
+ JSON.parse(response.body)
48
+ rescue RestClient::NotFound, RestClient::BadRequest
49
+ {}
50
+ rescue RestClient::Unauthorized
51
+ # Raise a custom exception not to expose the underlying
52
+ # HTTP client.
53
+ raise VirustotalAPI::Unauthorized
54
+ rescue RestClient::TooManyRequests
55
+ # Raise a custom exception not to expose the underlying
56
+ # HTTP client.
57
+ raise VirustotalAPI::RateLimitError
58
+ end
59
+
60
+ private_class_method :base_perform
61
+
62
+ # @return [String] string of API URI instance method
63
+ def api_uri
64
+ self.class.api_uri
65
+ end
66
+
67
+ # @return [Boolean] if report for resource exists
68
+ def exists?
69
+ !report.empty?
70
+ end
71
+
72
+ # Generate a URL identifier.
73
+ # @see https://developers.virustotal.com/v3.0/reference#url
74
+ def self.url_identifier(url)
75
+ Base64.urlsafe_encode64(url).strip.gsub('=', '')
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,18 @@
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
+ # Find a domain.
9
+ #
10
+ # @param [String] domain The domain to search
11
+ # @param [String] api_key for virustotal
12
+ # @return [VirustotalAPI::Domain] Report Search Result
13
+ def self.find(domain, api_key)
14
+ report = perform("/domains/#{domain}", api_key)
15
+ new(report)
16
+ end
17
+ end
18
+ end
@@ -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,67 @@
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
+ # Find a hash.
9
+ #
10
+ # @param [String] resource file as a md5/sha1/sha256 hash
11
+ # @param [String] api_key The key for virustotal
12
+ # @return [VirustotalAPI::File] Report Search Result
13
+ def self.find(resource, api_key)
14
+ report = perform("/files/#{resource}", api_key)
15
+ new(report)
16
+ end
17
+
18
+ # Upload a new file.
19
+ #
20
+ # @param [String] file_path for file to be sent for scan
21
+ # @param [String] api_key The key for virustotal
22
+ # @param [Hash] opts hash for additional options
23
+ # @return [VirusotalAPI::File] Report
24
+ def self.upload(file_path, api_key, opts = {})
25
+ filename = opts.fetch('filename') { ::File.basename(file_path) }
26
+ report = perform('/files', api_key, :post, filename: filename, file: ::File.open(file_path, 'r'))
27
+ new(report)
28
+ end
29
+
30
+ # Upload a new file with size more than 32MB.
31
+ #
32
+ # @param [String] file_path for file to be sent for scan
33
+ # @param [String] api_key The key for virustotal
34
+ # @param [Hash] opts hash for additional options
35
+ # @return [VirusotalAPI::File] Report
36
+ def self.upload_large(file_path, api_key, opts = {})
37
+ filename = opts.fetch('filename') { ::File.basename(file_path) }
38
+ url = upload_url(api_key)
39
+ report = perform_absolute(url, api_key, :post, filename: filename, file: ::File.open(file_path, 'r'))
40
+ new(report)
41
+ end
42
+
43
+ # Analyse a hash again.
44
+ #
45
+ # @param [String] resource file as a md5/sha1/sha256 hash
46
+ # @param [String] api_key The key for virustotal
47
+ # @return [VirustotalAPI::File] Report
48
+ def self.analyse(resource, api_key)
49
+ report = perform("/files/#{resource}/analyse", api_key, :post)
50
+ new(report)
51
+ end
52
+
53
+ # @return [String] url for upload file
54
+ def self.upload_url(api_key)
55
+ data = perform('/files/upload_url', api_key)
56
+ data&.dig('data')
57
+ end
58
+
59
+ # Check if the submitted hash is detected by an AV engine.
60
+ #
61
+ # @param [String] engine The engine to check.
62
+ # @return [Boolean] true if detected
63
+ def detected_by(engine)
64
+ report&.dig('data', 'attributes', 'last_analysis_results', engine, 'category') == 'malicious'
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/groups' API
7
+ class Group < Base
8
+ # Find a Group.
9
+ #
10
+ # @param [String] group_id to find
11
+ # @param [String] api_key The key for virustotal
12
+ # @return [VirustotalAPI::User] Report
13
+ def self.find(group_id, api_key)
14
+ report = perform("/groups/#{group_id}", api_key)
15
+ new(report)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
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
+ # Find an IP.
9
+ #
10
+ # @param [String] ip address The IP to find.
11
+ # @param [String] api_key The key for virustotal
12
+ # @return [VirustotalAPI::IPReport] Report
13
+ def self.find(ip, api_key)
14
+ report = perform("/ip_addresses/#{ip}", api_key)
15
+ new(report)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VirustotalAPI
4
+ # The API base URI
5
+ URI = 'https://www.virustotal.com/api/v3'
6
+ end
@@ -0,0 +1,38 @@
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
+ # Find a URL.
9
+ #
10
+ # @param [String] resource as an ip/domain/url
11
+ # @param [String] api_key The key for virustotal
12
+ # @return [VirustotalAPI::URL] Report
13
+ def self.find(resource, api_key)
14
+ report = perform("/urls/#{url_identifier(resource)}", api_key)
15
+ new(report)
16
+ end
17
+
18
+ # Analyse a URL again.
19
+ #
20
+ # @param [String] resource as an ip/domain/url
21
+ # @param [String] api_key The key for virustotal
22
+ # @return [VirustotalAPI::URL] Report
23
+ def self.analyse(resource, api_key)
24
+ report = perform("/urls/#{url_identifier(resource)}/analyse", api_key, :post)
25
+ new(report)
26
+ end
27
+
28
+ # Upload a URL for detection.
29
+ #
30
+ # @param [String] resource as an ip/domain/url
31
+ # @param [String] api_key The key for virustotal
32
+ # @return [VirustotalAPI::URL] Report
33
+ def self.upload(resource, api_key)
34
+ report = perform('/urls', api_key, :post, url: resource)
35
+ new(report)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module VirustotalAPI
6
+ # A class for '/users' API
7
+ class User < Base
8
+ # Find a User.
9
+ #
10
+ # @param [String] user_key with id or api_key
11
+ # @param [String] api_key The key for virustotal
12
+ # @return [VirustotalAPI::User] Report
13
+ def self.find(user_key, api_key)
14
+ report = perform("/users/#{user_key}", api_key)
15
+ new(report)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VirustotalAPI
4
+ # The GEM version
5
+ VERSION = '0.1.7'
6
+ end
@@ -0,0 +1,11 @@
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/group'
7
+ require 'virustotal_api/ip'
8
+ require 'virustotal_api/url'
9
+ require 'virustotal_api/uri'
10
+ require 'virustotal_api/user'
11
+ require 'virustotal_api/version'
@@ -0,0 +1,26 @@
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
+
15
+ @id = vtreport.id
16
+ assert @id.is_a?(String)
17
+ end
18
+
19
+ VCR.use_cassette('analysis') do
20
+ analysis = VirustotalAPI::Analysis.find(@id, @api_key)
21
+
22
+ assert analysis.exists?
23
+ assert analysis.id.is_a?(String)
24
+ end
25
+ end
26
+ end
data/test/base_test.rb ADDED
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './test/test_helper'
4
+
5
+ class VirustotalAPIBaseTest < Minitest::Test
6
+ def setup
7
+ @domain = 'xpressco.za'
8
+ @sha256 = '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
9
+ @url = 'https://www.dropbox.com/s/qmi112rc4ns75eb/Confidential_123.xls?dl=1'
10
+ @api_key = 'testapikey'
11
+ end
12
+
13
+ def test_class_exists
14
+ assert VirustotalAPI::Base
15
+ end
16
+
17
+ # Instance Method
18
+ def test_api_uri_instance_method
19
+ base_uri = 'https://www.virustotal.com/api/v3'
20
+ vt_base = VirustotalAPI::Base.new(nil)
21
+
22
+ assert vt_base.api_uri.is_a?(String)
23
+ assert_equal base_uri, vt_base.api_uri
24
+ end
25
+
26
+ # Class Method
27
+ def test_api_uri_class_method
28
+ base_uri = 'https://www.virustotal.com/api/v3'
29
+
30
+ assert VirustotalAPI::Base.api_uri.is_a?(String)
31
+ assert_equal base_uri, VirustotalAPI::Base.api_uri
32
+ end
33
+
34
+ def test_exists?
35
+ VCR.use_cassette('file_find') do
36
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
37
+
38
+ assert virustotal_report.exists?
39
+ end
40
+ end
41
+
42
+ def test_not_exists?
43
+ VCR.use_cassette('file_not_found') do
44
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
45
+
46
+ assert !virustotal_report.exists?
47
+ end
48
+
49
+ VCR.use_cassette('domain_bad_request') do
50
+ virustotal_report = VirustotalAPI::Domain.find(@domain, @api_key)
51
+
52
+ assert !virustotal_report.exists?
53
+ end
54
+ end
55
+
56
+ def test_url_encoding
57
+ VCR.use_cassette('url_encoding_find') do
58
+ virustotal_report = VirustotalAPI::URL.find(@url, @api_key)
59
+
60
+ assert virustotal_report.exists?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,27 @@
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.exists?
21
+ assert vtdomain_report.is_a?(VirustotalAPI::Domain)
22
+ assert vtdomain_report.report.is_a?(Hash)
23
+ assert vtdomain_report.id.is_a?(String)
24
+ assert vtdomain_report.report_url.is_a?(String)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
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
+
24
+ def test_rate_limit
25
+ VCR.use_cassette('file_rate_limit') do
26
+ assert_raises VirustotalAPI::RateLimitError do
27
+ VirustotalAPI::File.analyse(@sha256, @api_key)
28
+ end
29
+ end
30
+ end
31
+ end
data/test/file_test.rb ADDED
@@ -0,0 +1,73 @@
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
+ vt_file_report = VirustotalAPI::File.find(@sha256, @api_key)
19
+
20
+ # Make sure that the JSON was parsed
21
+ assert vt_file_report.exists?
22
+ assert vt_file_report.is_a?(VirustotalAPI::File)
23
+ assert vt_file_report.report.is_a?(Hash)
24
+ assert vt_file_report.id.is_a?(String)
25
+ assert vt_file_report.report_url.is_a?(String)
26
+ end
27
+ end
28
+
29
+ def test_find
30
+ id = '01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b'
31
+ permalink = "https://www.virustotal.com/api/v3/files/#{id}"
32
+
33
+ VCR.use_cassette('file_find') do
34
+ vt_file_report = VirustotalAPI::File.find(@sha256, @api_key)
35
+
36
+ assert_equal permalink, vt_file_report.report_url
37
+ assert_equal id, vt_file_report.id
38
+ assert vt_file_report.detected_by('Avira')
39
+ assert !vt_file_report.detected_by('Acronis')
40
+ assert !vt_file_report.detected_by('Yeyeyeye') # not present in file
41
+ end
42
+ end
43
+
44
+ def test_upload
45
+ VCR.use_cassette('file_upload') do
46
+ vt_file_upload = VirustotalAPI::File.upload(@file_path, @api_key)
47
+
48
+ assert vt_file_upload.exists?
49
+ assert vt_file_upload.report.is_a?(Hash)
50
+ assert vt_file_upload.id.is_a?(String)
51
+ end
52
+ end
53
+
54
+ def test_upload_large
55
+ VCR.use_cassette('large_file_upload') do
56
+ vt_file_upload = VirustotalAPI::File.upload_large(@file_path, @api_key)
57
+
58
+ assert vt_file_upload.exists?
59
+ assert vt_file_upload.report.is_a?(Hash)
60
+ assert vt_file_upload.id.is_a?(String)
61
+ end
62
+ end
63
+
64
+ def test_analyse
65
+ VCR.use_cassette('file_analyse') do
66
+ vt_file_analyse = VirustotalAPI::File.analyse(@sha256, @api_key)
67
+
68
+ assert vt_file_analyse.exists?
69
+ assert vt_file_analyse.report.is_a?(Hash)
70
+ assert vt_file_analyse.id.is_a?(String)
71
+ end
72
+ end
73
+ end