nessus_api 2.0.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.
- checksums.yaml +7 -0
- data/LICENSE.md +20 -0
- data/README.md +0 -0
- data/lib/nessus_api/report.rb +35 -0
- data/lib/nessus_api/scan.rb +55 -0
- data/lib/nessus_api/session.rb +96 -0
- data/lib/nessus_api/template.rb +60 -0
- data/lib/nessus_api.rb +11 -0
- data/spec/api_spec.rb +110 -0
- data/spec/factories.rb +7 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 21bf2c9f1cca3c36a7d49f20cf5d945045754c15
|
|
4
|
+
data.tar.gz: 716f4ffad165197e24db6cebbaba20b43d5f8e0e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 61762f9aa9dc1e1a858c26cd6ce585ce8f83a05be63fc0cf186ffe4e6da4ca4d0b7adb90bd15c0e7c9d564105408c1c4faacc5d90cbdf332461deba6c4c3690c
|
|
7
|
+
data.tar.gz: 8cbbf1276608c843a40ff7a28dc51d76df8dc8b59af1c5a2f7cc7b9acad504258a3732b8506c73286ed91d4cfe1553125ec2ed85ee609c67ec75cfb8b64fce57
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Nicholas Wold
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# report.rb
|
|
2
|
+
# A collection of helper functions
|
|
3
|
+
# that make API calls and then return
|
|
4
|
+
# wanted information.
|
|
5
|
+
|
|
6
|
+
module NessusAPI
|
|
7
|
+
module Helpers
|
|
8
|
+
def self.getSeverity(uuid, session=Session.current)
|
|
9
|
+
result = 0
|
|
10
|
+
xml = session.get('report2/hosts', {'report' => uuid})
|
|
11
|
+
xml.css('item').each do |i|
|
|
12
|
+
level = i.css('severityLevel').text.to_i
|
|
13
|
+
threat = i.css('count').text.to_i
|
|
14
|
+
if level > result
|
|
15
|
+
if threat
|
|
16
|
+
result = level
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
return result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.translateSeverity(n)
|
|
24
|
+
if n < 0 or n > 4
|
|
25
|
+
return 'Unknown'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return {0 => 'Minimal',
|
|
29
|
+
1 => 'Low',
|
|
30
|
+
2 => 'Medium',
|
|
31
|
+
3 => 'High',
|
|
32
|
+
4 => 'Critical'}[n]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# scan.rb
|
|
2
|
+
# Handles all of the scan logic
|
|
3
|
+
# between the API and the installation.
|
|
4
|
+
|
|
5
|
+
module NessusAPI
|
|
6
|
+
class Scan
|
|
7
|
+
# The class that handles API calls
|
|
8
|
+
# for individuals scans.
|
|
9
|
+
def initialize(target, scan_name, policy, session=Session.current)
|
|
10
|
+
# Creates a new scan on the Nessus
|
|
11
|
+
# installation using the given params.
|
|
12
|
+
@target = target
|
|
13
|
+
@name = scan_name
|
|
14
|
+
@policy = policy
|
|
15
|
+
@session = session
|
|
16
|
+
@uuid = @session.get('scan/new', {'target' => @target,
|
|
17
|
+
'scan_name' => @name, 'policy_id' => @policy}).at_css("uuid").text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stop
|
|
21
|
+
changeStatus('stop')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pause
|
|
25
|
+
# Pauses the current scan.
|
|
26
|
+
changeStatus('pause')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resume
|
|
30
|
+
# Resumes the current scan from being
|
|
31
|
+
# paused.
|
|
32
|
+
changeStatus('resume')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def changeStatus(path)
|
|
36
|
+
# Helper function for stop, pause
|
|
37
|
+
# and resume.
|
|
38
|
+
if @session.get("scan/#{path}",
|
|
39
|
+
{'scan_uuid' => @uuid}).css('status').text == 'OK'
|
|
40
|
+
return true
|
|
41
|
+
else
|
|
42
|
+
return false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def uuid
|
|
47
|
+
@uuid
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.list(session = Session.current)
|
|
51
|
+
# Returns all currently running scan jobs.
|
|
52
|
+
session.scanList
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# session.rb
|
|
2
|
+
# Handles all of the session shit between
|
|
3
|
+
# the gem and the Nessus installation.
|
|
4
|
+
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
require 'nokogiri'
|
|
8
|
+
require 'openssl'
|
|
9
|
+
|
|
10
|
+
module NessusAPI
|
|
11
|
+
class Session
|
|
12
|
+
# Keep that in mind when I start extending
|
|
13
|
+
# the class.
|
|
14
|
+
@@current = nil
|
|
15
|
+
|
|
16
|
+
def initialize(host=ENV['NESSUS_HOST'], user=ENV['NESSUS_USER'],
|
|
17
|
+
pw=ENV['NESSUS_PASS'], port=ENV['NESSUS_PORT'])
|
|
18
|
+
# Attempts to connect with the given instance
|
|
19
|
+
# of Nessus. Returns errors when it cannot reach
|
|
20
|
+
# an installation, or if there are bad credentials
|
|
21
|
+
# given. Returns a token otherwise.
|
|
22
|
+
@host = host
|
|
23
|
+
@port = port
|
|
24
|
+
@token = self.get('login', {'login' => user, 'password' => pw},
|
|
25
|
+
nil).css("token").text
|
|
26
|
+
@@current = self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get(path, args={}, token=@token)
|
|
30
|
+
# Performs an API call using the path and arguments given.
|
|
31
|
+
# Returns a token if there is not already a token.
|
|
32
|
+
# Otherwise, it returns the response from the server.
|
|
33
|
+
args['token'] = @token
|
|
34
|
+
args['seq'] = Random.new.rand(9999).to_s
|
|
35
|
+
url = URI('https://' + @host + ':' + @port + '/' + path)
|
|
36
|
+
request = Net::HTTP::Post.new(url.path)
|
|
37
|
+
request.set_form_data(args)
|
|
38
|
+
conn = Net::HTTP.new(url.host, url.port)
|
|
39
|
+
conn.use_ssl = true
|
|
40
|
+
conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
41
|
+
begin
|
|
42
|
+
response = conn.request(request)
|
|
43
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
44
|
+
response_xml = Nokogiri::XML(response.body)
|
|
45
|
+
if response_xml.at_css("seq").text != args['seq']
|
|
46
|
+
raise StandardError, "Secret token did not match!"
|
|
47
|
+
elsif response_xml.at_css("status").text != 'OK'
|
|
48
|
+
raise AuthenticationError, "Credentials are not valid!"
|
|
49
|
+
end
|
|
50
|
+
return response_xml
|
|
51
|
+
else
|
|
52
|
+
raise ConnectionError, "Could not connect properly!"
|
|
53
|
+
end
|
|
54
|
+
rescue => e
|
|
55
|
+
raise e
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def close
|
|
60
|
+
# Logs out of Nessus installation
|
|
61
|
+
# Returns a true, if it works.
|
|
62
|
+
if self.get('logout').css('contents').text == 'OK'
|
|
63
|
+
return true
|
|
64
|
+
else
|
|
65
|
+
return false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.current
|
|
70
|
+
@@current
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def current
|
|
74
|
+
@@current
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scanList
|
|
78
|
+
get('scan/list', {}).at_css('scanList')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def policies
|
|
82
|
+
results = []
|
|
83
|
+
@doc = get('policy/list', {})
|
|
84
|
+
(0..@doc.css("policies policyName").length-1).each do |i|
|
|
85
|
+
results << [@doc.css("policies policyName")[i].text, @doc.css("policies policyID")[i].text]
|
|
86
|
+
end
|
|
87
|
+
return results
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class AuthenticationError < StandardError
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class ConnectionError < StandardError
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# template.rb
|
|
2
|
+
# Does API things with installation.
|
|
3
|
+
|
|
4
|
+
module NessusAPI
|
|
5
|
+
class Template
|
|
6
|
+
# The class that handles the specific
|
|
7
|
+
# calls for templates.
|
|
8
|
+
def initialize(template_name, policy_id, target,
|
|
9
|
+
startTime=nil, rRules=nil,
|
|
10
|
+
session=Session.current)
|
|
11
|
+
@name = template_name
|
|
12
|
+
@policy = policy_id
|
|
13
|
+
@target = target
|
|
14
|
+
@session = session
|
|
15
|
+
@time = startTime
|
|
16
|
+
@rules = rRules
|
|
17
|
+
params = optional({'template_name' => @name, 'policy_id' => @policy,
|
|
18
|
+
'target' => @target}, @time, @rules)
|
|
19
|
+
@uuid = @session.get('scan/template/new', params).at_css("name").text
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def edit(old_name, new_name, policy_id, target,
|
|
23
|
+
startTime=nil, rRules=nil)
|
|
24
|
+
params = optional({'template' => old_name, 'template_name' => new_name,
|
|
25
|
+
'policy_id' => policy_id, 'target' => target})
|
|
26
|
+
if @session.get('scan/template/edit', params).css('status').text == 'OK'
|
|
27
|
+
return true
|
|
28
|
+
else
|
|
29
|
+
return false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def launch(uuid=@uuid)
|
|
34
|
+
# Returns the uuid of a template scan.
|
|
35
|
+
return @session.get('scan/template/launch',
|
|
36
|
+
{'template' => uuid}).at_css('uuid').text
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def delete(uuid=@uuid)
|
|
40
|
+
if @session.get('scan/template/delete',
|
|
41
|
+
{'template' => uuid}).css('status').text == 'OK'
|
|
42
|
+
return true
|
|
43
|
+
else
|
|
44
|
+
return false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def optional(params, startTime, rRules)
|
|
49
|
+
# Returns a hash given with a new hash
|
|
50
|
+
# with the optional attributes added.
|
|
51
|
+
if !startTime.nil?
|
|
52
|
+
params['startTime'] = startTime
|
|
53
|
+
end
|
|
54
|
+
if !rRules.nil?
|
|
55
|
+
params['rRules'] = rRules
|
|
56
|
+
end
|
|
57
|
+
return params
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/nessus_api.rb
ADDED
data/spec/api_spec.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# session_spec.rb
|
|
2
|
+
require 'factory_girl'
|
|
3
|
+
require_relative '../lib/nessus_api'
|
|
4
|
+
require 'nokogiri'
|
|
5
|
+
|
|
6
|
+
RSpec.configure do |config|
|
|
7
|
+
config.include FactoryGirl::Syntax::Methods
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe NessusAPI::Session do
|
|
11
|
+
describe ".new" do
|
|
12
|
+
context 'can connect' do
|
|
13
|
+
it 'does not raise an error' do
|
|
14
|
+
expect{NessusAPI::Session.new().close()}.to_not raise_error()
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'cannot connect' do
|
|
19
|
+
it 'responds with an error' do
|
|
20
|
+
expect{NessusAPI::Session.new('256.256.256.256').close}.to raise_error
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context 'bad credentials' do
|
|
25
|
+
it 'responds with an error' do
|
|
26
|
+
expect{NessusAPI::Session.new(ENV['NESSUS_HOST'], nil, nil).close}.to raise_error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe ".close" do
|
|
32
|
+
context 'done doing things' do
|
|
33
|
+
it 'returns true' do
|
|
34
|
+
expect{NessusAPI::Session.new.close}.to be_true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe ".list" do
|
|
40
|
+
let(:scan) {NessusAPI::Session.new()}
|
|
41
|
+
it 'returns an xml object' do
|
|
42
|
+
expect {scan.scanList.is_a?(Nokogiri::XML::Document)}.to be_true
|
|
43
|
+
scan.close()
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe NessusAPI::Scan do
|
|
49
|
+
let(:session) { NessusAPI::Session.new() }
|
|
50
|
+
let(:scan) { NessusAPI::Scan.new('127.0.0.1', 'API Test Scan', '-9') }
|
|
51
|
+
|
|
52
|
+
describe ".new" do
|
|
53
|
+
it 'to have a uuid' do
|
|
54
|
+
expect{scan.uuid}.to_not be_nil()
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe ".pause" do
|
|
59
|
+
it 'returns true' do
|
|
60
|
+
expect {scan.pause}.to be_true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe ".resume" do
|
|
65
|
+
it 'returns true' do
|
|
66
|
+
expect {scan.resume}.to be_true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe ".stop" do
|
|
71
|
+
it 'returns true' do
|
|
72
|
+
expect {scan.stop}.to be_true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe ".list" do
|
|
77
|
+
it 'returns a nokogiri object' do
|
|
78
|
+
expect {NessusAPI::Scan.list.is_a?(Nokogiri::XML::Document)}.to be_true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe NessusAPI::Template do
|
|
84
|
+
let(:session) {NessusAPI::Session.new()}
|
|
85
|
+
let(:template) {NessusAPI::Template.new('Test Template', '-9', '127.0.0.1')}
|
|
86
|
+
describe ".new" do
|
|
87
|
+
it 'does not raise an error' do
|
|
88
|
+
expect {template.is_a?(NessusAPI::Template)}.to be_true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe ".edit" do
|
|
93
|
+
it 'returns true' do
|
|
94
|
+
expect {template.edit('Test Template', 'Test 2: Test Harder', '-9',
|
|
95
|
+
'127.0.0.1')}.to be_true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe '.launch' do
|
|
100
|
+
it 'returns true' do
|
|
101
|
+
expect {template.launch}.to be_true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe '.delete' do
|
|
106
|
+
it 'returns true' do
|
|
107
|
+
expect {template.delete}.to be_true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/spec/factories.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nessus_api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nicholas Wold
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-02-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dotenv
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: nokogiri
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: Intelligently allows for use of Nessus API, both with gathering and editing
|
|
56
|
+
information.
|
|
57
|
+
email: nicholas.wold@berkeley.edu
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- lib/nessus_api/report.rb
|
|
63
|
+
- lib/nessus_api/template.rb
|
|
64
|
+
- lib/nessus_api/session.rb
|
|
65
|
+
- lib/nessus_api/scan.rb
|
|
66
|
+
- lib/nessus_api.rb
|
|
67
|
+
- spec/api_spec.rb
|
|
68
|
+
- spec/factories.rb
|
|
69
|
+
- README.md
|
|
70
|
+
- LICENSE.md
|
|
71
|
+
homepage: http://www.nicholaswold.com
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata: {}
|
|
75
|
+
post_install_message:
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubyforge_project:
|
|
91
|
+
rubygems_version: 2.1.10
|
|
92
|
+
signing_key:
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Allows interaction with Nessus REST API.
|
|
95
|
+
test_files: []
|
|
96
|
+
has_rdoc:
|