virustotal_api_compat 0.1.7

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