virustotal_api 0.3.0 → 0.5.2

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +26 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +12 -8
  5. data/CHANGELOG.md +38 -6
  6. data/Gemfile +2 -0
  7. data/README.md +121 -31
  8. data/Rakefile +2 -1
  9. data/lib/virustotal_api.rb +8 -5
  10. data/lib/virustotal_api/analysis.rb +24 -0
  11. data/lib/virustotal_api/base.rb +41 -10
  12. data/lib/virustotal_api/domain.rb +24 -0
  13. data/lib/virustotal_api/exceptions.rb +5 -0
  14. data/lib/virustotal_api/file.rb +56 -0
  15. data/lib/virustotal_api/group.rb +26 -0
  16. data/lib/virustotal_api/ip.rb +24 -0
  17. data/lib/virustotal_api/uri.rb +3 -1
  18. data/lib/virustotal_api/url.rb +46 -0
  19. data/lib/virustotal_api/user.rb +26 -0
  20. data/lib/virustotal_api/version.rb +3 -1
  21. data/test/analysis_test.rb +23 -0
  22. data/test/base_test.rb +14 -25
  23. data/test/domain_test.rb +32 -0
  24. data/test/exceptions_test.rb +23 -0
  25. data/test/file_test.rb +71 -0
  26. data/test/fixtures/analysis.yml +544 -0
  27. data/test/fixtures/domain.yml +830 -0
  28. data/test/fixtures/file_analyse.yml +52 -0
  29. data/test/fixtures/file_find.yml +853 -0
  30. data/test/fixtures/file_not_found.yml +52 -0
  31. data/test/fixtures/file_rate_limit.yml +52 -0
  32. data/test/fixtures/file_unauthorized.yml +51 -0
  33. data/test/fixtures/file_upload.yml +54 -0
  34. data/test/fixtures/group_find.yml +216 -0
  35. data/test/fixtures/ip.yml +716 -0
  36. data/test/fixtures/unscanned_url_find.yml +44 -0
  37. data/test/fixtures/url_analyse.yml +52 -0
  38. data/test/fixtures/url_find.yml +599 -0
  39. data/test/fixtures/user_find.yml +213 -0
  40. data/test/group_test.rb +32 -0
  41. data/test/{ip_report_test.rb → ip_test.rb} +5 -4
  42. data/test/test_helper.rb +1 -0
  43. data/test/uri_test.rb +2 -1
  44. data/test/url_test.rb +65 -0
  45. data/test/user_test.rb +31 -0
  46. data/test/version_test.rb +2 -2
  47. data/virustotal_api.gemspec +12 -9
  48. metadata +104 -65
  49. data/.travis.yml +0 -15
  50. data/lib/virustotal_api/domain_report.rb +0 -35
  51. data/lib/virustotal_api/file_report.rb +0 -36
  52. data/lib/virustotal_api/file_scan.rb +0 -36
  53. data/lib/virustotal_api/ip_report.rb +0 -35
  54. data/lib/virustotal_api/url_report.rb +0 -40
  55. data/test/domain_report_test.rb +0 -31
  56. data/test/file_report_test.rb +0 -34
  57. data/test/file_scan_test.rb +0 -29
  58. data/test/fixtures/domain_report.yml +0 -311
  59. data/test/fixtures/ip_report.yml +0 -1323
  60. data/test/fixtures/queue_unscanned_url_report.yml +0 -46
  61. data/test/fixtures/report.yml +0 -110
  62. data/test/fixtures/report_not_found.yml +0 -42
  63. data/test/fixtures/request_forbidden.yml +0 -38
  64. data/test/fixtures/scan.yml +0 -49
  65. data/test/fixtures/unscanned_url_report.yml +0 -43
  66. data/test/fixtures/url_report.yml +0 -95
  67. data/test/url_report_test.rb +0 -56
@@ -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&.dig('data', 'attributes', 'last_analysis_results', engine, 'category') == 'malicious'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,26 @@
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
+ 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 Group.
17
+ #
18
+ # @param [String] group_id to find
19
+ # @param [String] api_key The key for virustotal
20
+ # @return [VirustotalAPI::User] Report
21
+ def self.find(group_id, api_key)
22
+ report = perform("/groups/#{group_id}", api_key)
23
+ new(report)
24
+ end
25
+ end
26
+ 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
+ # frozen_string_literal: true
1
2
 
2
3
  module VirustotalAPI
3
- URI = 'https://www.virustotal.com/vtapi/v2'.freeze
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
@@ -0,0 +1,26 @@
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
+ 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 User.
17
+ #
18
+ # @param [String] user_key with id or api_key
19
+ # @param [String] api_key The key for virustotal
20
+ # @return [VirustotalAPI::User] Report
21
+ def self.find(user_key, api_key)
22
+ report = perform("/users/#{user_key}", api_key)
23
+ new(report)
24
+ end
25
+ end
26
+ end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module VirustotalAPI
3
- VERSION = '0.3.0'.freeze
4
+ # The GEM version
5
+ VERSION = '0.5.2'
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
+ # frozen_string_literal: true
1
2
 
2
- # rubocop:disable LineLength
3
3
  require './test/test_helper'
4
4
 
5
5
  class VirustotalAPIBaseTest < Minitest::Test
@@ -14,45 +14,34 @@ 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
29
+ assert_equal base_uri, VirustotalAPI::Base.api_uri
30
30
  end
31
31
 
32
- def test_parse_code_200
33
- mock_response200 = Minitest::Mock.new
34
- mock_response200.expect(:code, 200)
35
- mock_response200.expect(:body, '{}')
36
-
37
- assert VirustotalAPI::Base.parse(mock_response200), {}
38
- end
39
-
40
- def test_parse_code_204
41
- mock_response204 = Minitest::Mock.new
42
- mock_response204.expect(:code, 204)
43
- mock_response204.expect(:body, '{}')
32
+ def test_exists?
33
+ VCR.use_cassette('file_find') do
34
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
44
35
 
45
- assert_raises VirustotalAPI::RateLimitError do
46
- VirustotalAPI::Base.parse(mock_response204)
36
+ assert virustotal_report.exists?
47
37
  end
48
38
  end
49
39
 
50
- # Test using FileReport
51
- def test_exists?
52
- VCR.use_cassette('report') do
53
- virustotal_report = VirustotalAPI::FileReport.find(@sha256, @api_key)
40
+ def test_not_exists?
41
+ VCR.use_cassette('file_not_found') do
42
+ virustotal_report = VirustotalAPI::File.find(@sha256, @api_key)
54
43
 
55
- assert virustotal_report.exists?, true
44
+ assert !virustotal_report.exists?
56
45
  end
57
46
  end
58
47
  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,31 @@
1
+ # frozen_string_literal: true
1
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
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
7
30
  end
8
31
  end
@@ -0,0 +1,71 @@
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
+ assert virustotal_report.detected_by('Avira')
35
+ assert !virustotal_report.detected_by('Acronis')
36
+ assert !virustotal_report.detected_by('Yeyeyeye') # not present in file
37
+ end
38
+ end
39
+
40
+ def test_upload
41
+ VCR.use_cassette('file_upload') do
42
+ virustotal_upload = VirustotalAPI::File.upload(@file_path, @api_key)
43
+
44
+ assert virustotal_upload.report.is_a?(Hash)
45
+ end
46
+ end
47
+
48
+ def test_upload_id
49
+ VCR.use_cassette('file_upload') do
50
+ virustotal_upload = VirustotalAPI::File.upload(@file_path, @api_key)
51
+
52
+ assert virustotal_upload.id.is_a?(String)
53
+ end
54
+ end
55
+
56
+ def test_analyse
57
+ VCR.use_cassette('file_analyse') do
58
+ virustotal_analyse = VirustotalAPI::File.analyse(@sha256, @api_key)
59
+
60
+ assert virustotal_analyse.report.is_a?(Hash)
61
+ end
62
+ end
63
+
64
+ def test_analyse_id
65
+ VCR.use_cassette('file_analyse') do
66
+ virustotal_analyse = VirustotalAPI::File.analyse(@sha256, @api_key)
67
+
68
+ assert virustotal_analyse.id.is_a?(String)
69
+ end
70
+ end
71
+ end