vtapi 0.1.0 → 0.2.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.
- data/README.md +45 -6
- data/lib/vtapi/api.rb +24 -1
- data/lib/vtapi/response.rb +15 -2
- data/lib/vtapi/version.rb +1 -1
- data/spec/response_spec.rb +34 -1
- data/spec/vtapi_spec.rb +96 -0
- metadata +14 -14
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# VtAPI
|
|
1
|
+
# VtAPI [](http://badge.fury.io/rb/vtapi)
|
|
2
2
|
|
|
3
3
|
Ruby gem for VirusTotal Public API v2.0
|
|
4
4
|
|
|
@@ -37,7 +37,24 @@ puts resp.positives # num of positives
|
|
|
37
37
|
puts resp.scan_results # {"McAfee"=>nil, "Symantec"=>"Android.ZertSecurity", ... }
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
### File
|
|
40
|
+
### File Report (multiple resources)
|
|
41
|
+
```ruby
|
|
42
|
+
# retrieve file report by file hash(SHA256, SHA1, MD5)
|
|
43
|
+
# up to 4 resources can assign
|
|
44
|
+
resources = ['00ce460c8b33711091206..', ..]
|
|
45
|
+
resps = api.file_report(resources)
|
|
46
|
+
resps.each do |r|
|
|
47
|
+
puts "#{r.sha256}: #{r.positives} / #{r.total}" if r.response_code == 1
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
The following APIs are supported Posting multi resources(URLs).
|
|
51
|
+
* VtAPI#file_report
|
|
52
|
+
* VtAPI#file_rescan
|
|
53
|
+
* VtAPI#url_scan
|
|
54
|
+
* VtAPI#url_report
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### File Scan (File Upload)
|
|
41
58
|
```ruby
|
|
42
59
|
# read file
|
|
43
60
|
data = File.open(some_path, 'rb') {|f| f.read }
|
|
@@ -49,21 +66,43 @@ resp = api.file_scan(data)
|
|
|
49
66
|
puts resp.response_code # 1: OK, 0: result doesn't exist, -2: still queued
|
|
50
67
|
```
|
|
51
68
|
|
|
69
|
+
|
|
70
|
+
### URL Scan
|
|
71
|
+
```ruby
|
|
72
|
+
# upload url
|
|
73
|
+
resp = api.url_scan(url)
|
|
74
|
+
|
|
75
|
+
# confirm response_code
|
|
76
|
+
puts resp.response_code # 1: OK, 0: result doesn't exist, -2: still queued
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### URL Report
|
|
81
|
+
```ruby
|
|
82
|
+
# upload url
|
|
83
|
+
resp = api.url_scan(url)
|
|
84
|
+
|
|
85
|
+
# confirm result
|
|
86
|
+
puts resp.scans
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
52
90
|
## Features
|
|
53
|
-
###
|
|
91
|
+
### Support API
|
|
54
92
|
* file/scan
|
|
55
93
|
* file/resan
|
|
56
94
|
* file/report
|
|
57
|
-
|
|
58
|
-
### not implemented yet
|
|
59
95
|
* url/scan
|
|
60
96
|
* url/report
|
|
97
|
+
|
|
98
|
+
### Not implemented yet
|
|
61
99
|
* ip-address/report
|
|
62
100
|
* domain/report
|
|
63
101
|
|
|
64
|
-
###
|
|
102
|
+
### Unsupported
|
|
65
103
|
* comments/puts
|
|
66
104
|
|
|
105
|
+
|
|
67
106
|
## Contributing
|
|
68
107
|
|
|
69
108
|
1. Fork it
|
data/lib/vtapi/api.rb
CHANGED
|
@@ -24,13 +24,36 @@ class VtAPI
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def file_rescan(resource)
|
|
27
|
+
if resource.is_a? Array
|
|
28
|
+
raise 'limit is up to 25 items' if resource.size > 25
|
|
29
|
+
resource = resource.join(', ')
|
|
30
|
+
end
|
|
27
31
|
http_post('file/rescan', resource: resource)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def file_report(resource)
|
|
35
|
+
if resource.is_a? Array
|
|
36
|
+
raise 'limit is up to 4 items' if resource.size > 4
|
|
37
|
+
resource = resource.join(', ')
|
|
38
|
+
end
|
|
31
39
|
http_post('file/report', resource: resource)
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
def url_scan(url)
|
|
43
|
+
if url.is_a? Array
|
|
44
|
+
raise 'limit is up to 4 items' if url.size > 4
|
|
45
|
+
url = url.join("\n")
|
|
46
|
+
end
|
|
47
|
+
http_post('url/scan', url: url)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def url_report(url)
|
|
51
|
+
if url.is_a? Array
|
|
52
|
+
raise 'limit is up to 4 items' if url.size > 4
|
|
53
|
+
url = url.join(", ")
|
|
54
|
+
end
|
|
55
|
+
http_post('url/report', resource: url)
|
|
56
|
+
end
|
|
34
57
|
|
|
35
58
|
def http_post(path, params = {})
|
|
36
59
|
uri = BASE_URL + path
|
|
@@ -45,6 +68,6 @@ class VtAPI
|
|
|
45
68
|
resp.return!(req, result, &block)
|
|
46
69
|
end
|
|
47
70
|
end
|
|
48
|
-
Response.
|
|
71
|
+
Response.parse(resp.body)
|
|
49
72
|
end
|
|
50
73
|
end
|
data/lib/vtapi/response.rb
CHANGED
|
@@ -3,8 +3,17 @@ require 'time'
|
|
|
3
3
|
|
|
4
4
|
class VtAPI
|
|
5
5
|
class Response
|
|
6
|
-
def
|
|
7
|
-
|
|
6
|
+
def self.parse(response_body)
|
|
7
|
+
json = JSON.parse(response_body)
|
|
8
|
+
if json.is_a? Array
|
|
9
|
+
return json.map{|e| Response.new(e) }
|
|
10
|
+
else
|
|
11
|
+
return Response.new(json)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(json)
|
|
16
|
+
@json = json
|
|
8
17
|
@json.keys.each do |key|
|
|
9
18
|
Response.class_eval {
|
|
10
19
|
define_method key.to_s do |*args|
|
|
@@ -34,5 +43,9 @@ class VtAPI
|
|
|
34
43
|
def [](key)
|
|
35
44
|
@json.fetch(key.to_s) # raise KeyError when key doesn't exist.
|
|
36
45
|
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
@json.to_json
|
|
49
|
+
end
|
|
37
50
|
end
|
|
38
51
|
end
|
data/lib/vtapi/version.rb
CHANGED
data/spec/response_spec.rb
CHANGED
|
@@ -5,7 +5,9 @@ describe VtAPI::Response do
|
|
|
5
5
|
let(:sample_response) {
|
|
6
6
|
"{\"scans\":{\"McAfee\":{\"detected\":false,\"version\":\"5.400.0.1158\",\"result\":null,\"update\":\"20130512\"},\"Symantec\":{\"detected\":true,\"version\":\"20121.3.0.76\",\"result\":\"Android.ZertSecurity\",\"update\":\"20130512\"},\"Kaspersky\":{\"detected\":true,\"version\":\"9.0.0.837\",\"result\":\"HEUR:Trojan-Banker.AndroidOS.Zitmo.a\",\"update\":\"20130512\"},\"TrendMicro\":{\"detected\":false,\"version\":\"9.740.0.1012\",\"result\":null,\"update\":\"20130512\"},\"Microsoft\":{\"detected\":false,\"version\":\"1.9402\",\"result\":null,\"update\":\"20130512\"}},\"scan_id\":\"00ce460c8b337110912066f746731a916e85bf1d7f4b44f09ca3cc39f9b52a98-1368320515\",\"sha1\":\"e1b727b3e9336033606df79eeba03dd218b56c20\",\"resource\":\"00ce460c8b337110912066f746731a916e85bf1d7f4b44f09ca3cc39f9b52a98\",\"response_code\":1,\"scan_date\":\"2013-05-12 01:01:55\",\"permalink\":\"https://www.virustotal.com/file/00ce460c8b337110912066f746731a916e85bf1d7f4b44f09ca3cc39f9b52a98/analysis/1368320515/\",\"verbose_msg\":\"Scan finished, scan information embedded in this object\",\"total\":46,\"positives\":22,\"sha256\":\"00ce460c8b337110912066f746731a916e85bf1d7f4b44f09ca3cc39f9b52a98\",\"md5\":\"1cf41bdc0fdd409774eb755031a6f49d\"}"
|
|
7
7
|
}
|
|
8
|
-
let(:
|
|
8
|
+
let(:sample_json) { JSON.parse(sample_response) }
|
|
9
|
+
let(:response) { VtAPI::Response.new(sample_json) }
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
describe '#md5' do
|
|
11
13
|
it { expect(response.md5).to eq '1cf41bdc0fdd409774eb755031a6f49d' }
|
|
@@ -57,6 +59,14 @@ describe VtAPI::Response do
|
|
|
57
59
|
subject { response.scan_results }
|
|
58
60
|
it { expect(subject).to eq({"McAfee"=>nil, "Symantec"=>"Android.ZertSecurity", "Kaspersky"=>"HEUR:Trojan-Banker.AndroidOS.Zitmo.a", "TrendMicro"=>nil, "Microsoft"=>nil}) }
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
describe '#to_s' do
|
|
64
|
+
subject { response.to_s }
|
|
65
|
+
it 'should be able to parse as json' do
|
|
66
|
+
expect{ JSON.parse(subject) }.to_not raise_error
|
|
67
|
+
end
|
|
68
|
+
it { expect(JSON.parse(subject).keys).to match_array ['scans', 'scan_id', 'sha1', 'resource', 'response_code', 'scan_date', 'permalink', 'verbose_msg', 'total', 'positives', 'sha256', 'md5'] }
|
|
69
|
+
end
|
|
60
70
|
end
|
|
61
71
|
|
|
62
72
|
context 'with no scan result' do
|
|
@@ -67,5 +77,28 @@ describe VtAPI::Response do
|
|
|
67
77
|
describe '#positive_threats' do
|
|
68
78
|
pending 'not implemented yet'
|
|
69
79
|
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe '.parse' do
|
|
84
|
+
subject { VtAPI::Response.parse(response) }
|
|
85
|
+
context 'assigns single response' do
|
|
86
|
+
let(:response) { '{"md5":"1cf41bdc0fdd409774eb755031a6f49d"}' }
|
|
87
|
+
it { expect(subject).to be_a VtAPI::Response }
|
|
88
|
+
it { expect(subject.md5).to eq "1cf41bdc0fdd409774eb755031a6f49d" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'assigns array response' do
|
|
92
|
+
subject {VtAPI::Response.parse(response) }
|
|
93
|
+
let(:response) { '[{"md5":"1cf41bdc0fdd409774eb755031a6f49d"}, {"md5":"1eef5bdc64241652f8a0df0c2dc92df6"}]' }
|
|
94
|
+
it { expect(subject).to be_a Array }
|
|
95
|
+
it { expect(subject).to have(2).items }
|
|
96
|
+
|
|
97
|
+
context 'about first object' do
|
|
98
|
+
subject {VtAPI::Response.parse(response).first }
|
|
99
|
+
it { expect(subject).to be_a VtAPI::Response }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
70
102
|
end
|
|
103
|
+
|
|
71
104
|
end
|
data/spec/vtapi_spec.rb
CHANGED
|
@@ -48,6 +48,22 @@ describe VtAPI do
|
|
|
48
48
|
.to_return(:body => sample_response, :status => 200)
|
|
49
49
|
subject
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
context 'assigns 2 resources' do
|
|
53
|
+
let(:resource) { ['ff' * 32, '00' * 32] }
|
|
54
|
+
let(:sample_response) { '[{}]' }
|
|
55
|
+
it "should post resource parameter as comma separete value" do
|
|
56
|
+
stub_request(:post, api_url)
|
|
57
|
+
.with(:body => {'resource' => resource.join(', '), 'apikey' => apikey} )
|
|
58
|
+
.to_return(:body => sample_response, :status => 200)
|
|
59
|
+
subject
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context 'when assign 25 resources(over the limitation)' do
|
|
64
|
+
let(:resource) { ['ff' * 32] * 25 }
|
|
65
|
+
it { expect{ subject }.to raise_error }
|
|
66
|
+
end
|
|
51
67
|
end
|
|
52
68
|
|
|
53
69
|
describe '#file_report' do
|
|
@@ -74,5 +90,85 @@ describe VtAPI do
|
|
|
74
90
|
expect{ subject }.to raise_error(VtAPI::AuthError)
|
|
75
91
|
end
|
|
76
92
|
end
|
|
93
|
+
|
|
94
|
+
context 'when assign 2 resources' do
|
|
95
|
+
let(:resource) { ['ff' * 32, '00' * 32] }
|
|
96
|
+
it "should post resource parameter as comma separete value" do
|
|
97
|
+
stub_request(:post, api_url)
|
|
98
|
+
.with(:body => {'resource' => resource.join(', '), 'apikey' => apikey} )
|
|
99
|
+
.to_return(:body => sample_response, :status => 200)
|
|
100
|
+
subject
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when assign 5 resources(over the limitation)' do
|
|
105
|
+
let(:resource) { ['ff' * 32] * 5 }
|
|
106
|
+
it { expect{ subject }.to raise_error }
|
|
107
|
+
end
|
|
77
108
|
end
|
|
109
|
+
|
|
110
|
+
describe '#url_scan' do
|
|
111
|
+
let(:sample_response) { '{}' }
|
|
112
|
+
let(:api_url) { 'https://www.virustotal.com/vtapi/v2/url/scan' }
|
|
113
|
+
let(:target_url) { 'http://www.foobar.com/' }
|
|
114
|
+
subject { api.url_scan(target_url) }
|
|
115
|
+
|
|
116
|
+
it "should connect to 'https://www.virustotal.com/vtapi/v2/url/scan'" do
|
|
117
|
+
stub_request(:post, api_url)
|
|
118
|
+
.with(:body => {'url' => target_url, 'apikey' => apikey} )
|
|
119
|
+
.to_return(:body => sample_response, :status => 200)
|
|
120
|
+
subject
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
context 'when assign 2 urls' do
|
|
124
|
+
let(:target_url) { ['http://foobar.com/', 'http://abc.com'] }
|
|
125
|
+
|
|
126
|
+
it "should post url parameter which is joined by '\\n'" do
|
|
127
|
+
stub_request(:post, api_url)
|
|
128
|
+
.with(:body => {'url' => target_url.join("\n"), 'apikey' => apikey} )
|
|
129
|
+
.to_return(:body => sample_response, :status => 200)
|
|
130
|
+
subject
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
context 'when assign 5 urls (over the limitation)' do
|
|
135
|
+
let(:target_url) { ['http://www.foobar.com/'] * 5 }
|
|
136
|
+
|
|
137
|
+
it { expect{ subject }.to raise_error }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#url_report' do
|
|
142
|
+
let(:sample_response) { '{}' }
|
|
143
|
+
let(:api_url) { 'https://www.virustotal.com/vtapi/v2/url/report' }
|
|
144
|
+
let(:target_url) { 'http://www.foobar.com/' }
|
|
145
|
+
subject { api.url_report(target_url) }
|
|
146
|
+
|
|
147
|
+
it "should connect to 'https://www.virustotal.com/vtapi/v2/url/report'" do
|
|
148
|
+
stub_request(:post, api_url)
|
|
149
|
+
.with(:body => {'resource' => target_url, 'apikey' => apikey} )
|
|
150
|
+
.to_return(:body => sample_response, :status => 200)
|
|
151
|
+
subject
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context 'when assign 2 urls' do
|
|
155
|
+
let(:target_url) { ['http://foobar.com/', 'http://abc.com'] }
|
|
156
|
+
|
|
157
|
+
it "should post url parameter which is joined by ', '" do
|
|
158
|
+
stub_request(:post, api_url)
|
|
159
|
+
.with(:body => {'resource' => target_url.join(", "), 'apikey' => apikey} )
|
|
160
|
+
.to_return(:body => sample_response, :status => 200)
|
|
161
|
+
subject
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
context 'when assign 5 urls (over the limitation)' do
|
|
166
|
+
let(:target_url) { ['http://www.foobar.com/'] * 5 }
|
|
167
|
+
|
|
168
|
+
it { expect{ subject }.to raise_error }
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
78
174
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vtapi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,11 +9,11 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-05-
|
|
12
|
+
date: 2013-05-24 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: bundler
|
|
16
|
-
requirement: &
|
|
16
|
+
requirement: &70343748341080 !ruby/object:Gem::Requirement
|
|
17
17
|
none: false
|
|
18
18
|
requirements:
|
|
19
19
|
- - ~>
|
|
@@ -21,10 +21,10 @@ dependencies:
|
|
|
21
21
|
version: '1.3'
|
|
22
22
|
type: :development
|
|
23
23
|
prerelease: false
|
|
24
|
-
version_requirements: *
|
|
24
|
+
version_requirements: *70343748341080
|
|
25
25
|
- !ruby/object:Gem::Dependency
|
|
26
26
|
name: rake
|
|
27
|
-
requirement: &
|
|
27
|
+
requirement: &70343748340660 !ruby/object:Gem::Requirement
|
|
28
28
|
none: false
|
|
29
29
|
requirements:
|
|
30
30
|
- - ! '>='
|
|
@@ -32,10 +32,10 @@ dependencies:
|
|
|
32
32
|
version: '0'
|
|
33
33
|
type: :development
|
|
34
34
|
prerelease: false
|
|
35
|
-
version_requirements: *
|
|
35
|
+
version_requirements: *70343748340660
|
|
36
36
|
- !ruby/object:Gem::Dependency
|
|
37
37
|
name: rspec
|
|
38
|
-
requirement: &
|
|
38
|
+
requirement: &70343748340120 !ruby/object:Gem::Requirement
|
|
39
39
|
none: false
|
|
40
40
|
requirements:
|
|
41
41
|
- - ~>
|
|
@@ -43,10 +43,10 @@ dependencies:
|
|
|
43
43
|
version: 2.13.0
|
|
44
44
|
type: :development
|
|
45
45
|
prerelease: false
|
|
46
|
-
version_requirements: *
|
|
46
|
+
version_requirements: *70343748340120
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: webmock
|
|
49
|
-
requirement: &
|
|
49
|
+
requirement: &70343748339620 !ruby/object:Gem::Requirement
|
|
50
50
|
none: false
|
|
51
51
|
requirements:
|
|
52
52
|
- - ~>
|
|
@@ -54,10 +54,10 @@ dependencies:
|
|
|
54
54
|
version: 1.11.0
|
|
55
55
|
type: :development
|
|
56
56
|
prerelease: false
|
|
57
|
-
version_requirements: *
|
|
57
|
+
version_requirements: *70343748339620
|
|
58
58
|
- !ruby/object:Gem::Dependency
|
|
59
59
|
name: rest-client
|
|
60
|
-
requirement: &
|
|
60
|
+
requirement: &70343748339160 !ruby/object:Gem::Requirement
|
|
61
61
|
none: false
|
|
62
62
|
requirements:
|
|
63
63
|
- - ~>
|
|
@@ -65,7 +65,7 @@ dependencies:
|
|
|
65
65
|
version: 1.6.7
|
|
66
66
|
type: :runtime
|
|
67
67
|
prerelease: false
|
|
68
|
-
version_requirements: *
|
|
68
|
+
version_requirements: *70343748339160
|
|
69
69
|
description: gem for VirusTotal Public API version2.0.
|
|
70
70
|
email:
|
|
71
71
|
- masatanish@gmail.com
|
|
@@ -102,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
102
102
|
version: '0'
|
|
103
103
|
segments:
|
|
104
104
|
- 0
|
|
105
|
-
hash: -
|
|
105
|
+
hash: -803017513526154134
|
|
106
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
107
107
|
none: false
|
|
108
108
|
requirements:
|
|
@@ -111,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
111
111
|
version: '0'
|
|
112
112
|
segments:
|
|
113
113
|
- 0
|
|
114
|
-
hash: -
|
|
114
|
+
hash: -803017513526154134
|
|
115
115
|
requirements: []
|
|
116
116
|
rubyforge_project:
|
|
117
117
|
rubygems_version: 1.8.16
|