nexpose_thycotic 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 72d72a2f1294229464df29985e8c3a0145f18d60
4
- data.tar.gz: 13e76a6eaeb8f69879b2a808251f9ad2a4bb2716
3
+ metadata.gz: def1949052dd9744a7b1803cf32153ca840c3e04
4
+ data.tar.gz: 616f386251fa0515e8d0df1a4b6a67b88eaa7a0b
5
5
  SHA512:
6
- metadata.gz: 0b6f9d979b6064ca1eadfa76d0026461057701110fcddb172bde51429412a97589e4de7b4e1c96bab5b0f5e760337620f955f4d95b7294d1b5d03fbc6d606bef
7
- data.tar.gz: c9f6ab9b22517feb3035b214492ab2cd7fffa1b0a3b5a4e2c4ef1823ed9e7e41113fa20d7cf25f2db5600346acc2b94b93a4f6911fbef3579d2bc1ca0349e635
6
+ metadata.gz: cb7b619bf897b98bc5910ee284711366dc3be9cbe4dde8653c9e23fc981d9373cd54c0fb96bf43f04ece34f5e2fd76f1bccafdeddc9979fe73a8044996686ce6
7
+ data.tar.gz: 4922dcd8ee3930d85c0ddffb277fb38251c943bce86d906b6e54093e8b14cbfe9090a7357b13786fb98f2707ccbf64cbfe56273099b77ab89185ab660fee3b57
data/README.md CHANGED
@@ -63,6 +63,11 @@ The gem is available as open source under the terms of the [MIT License](http://
63
63
 
64
64
  ## Changelog
65
65
 
66
+ ### 0.2.0
67
+ - User may configure the gem to delete or preserve existing credentials.
68
+ - A customisable comment is now attached to password retrieval requests.
69
+ - Multiple credentials for the same address may be imported.
70
+
66
71
  ### 0.0.7
67
72
  - User now has the option to configure the gem using a configuration file as well as with environment variables. Nexpose and Thycotic options have been added to the configuration file.
68
73
  - Added an encryption configuration file. Usernames and passwords within the configuration file are now encrypted when the application runs.
@@ -24,15 +24,18 @@ options = settings[:options]
24
24
 
25
25
  vault_options = { url: thycotic_options[:thycotic_url],
26
26
  username: thycotic_options[:thycotic_username],
27
- password: thycotic_options[:thycotic_password] }
27
+ password: thycotic_options[:thycotic_password],
28
+ comment: thycotic_options[:comment] }
28
29
 
29
30
  nexpose_options = { nexpose_ip: nexpose_options[:nexpose_url],
30
31
  nexpose_username: nexpose_options[:nexpose_username],
31
32
  nexpose_password: nexpose_options[:nexpose_password],
32
33
  nexpose_port: nexpose_options[:nexpose_port],
33
34
  sites: options[:sites],
35
+ clear_creds: options[:clear_creds],
34
36
  logging_enabled: options[:logging_enabled],
35
37
  log_level: options[:log_level],
36
- log_console: options[:log_console] }
38
+ log_console: options[:log_console] }
39
+
37
40
  NexposeThycotic.update_credentials(vault_options, nexpose_options)
38
41
 
@@ -3,55 +3,86 @@ require 'nexpose_thycotic/operations'
3
3
  require 'nexpose_thycotic/utilities/nx_logger'
4
4
  require 'nexpose'
5
5
 
6
- module NexposeThycotic
6
+ module NexposeThycotic
7
7
  def self.update_credentials(vault_options, nexpose_options = nil)
8
+ #TODO: Should we add the logging alias' from the SCCM gem?
8
9
  log = NexposeThycotic::NxLogger.instance
9
- log.setup_statistics_collection(NexposeThycotic::VENDOR,
10
- NexposeThycotic::PRODUCT,
10
+ log.setup_statistics_collection(NexposeThycotic::VENDOR,
11
+ NexposeThycotic::PRODUCT,
11
12
  NexposeThycotic::VERSION)
12
- log.setup_logging(true, nexpose_options[:log_level], nexpose_options[:log_console])
13
- log.log_message('Starting integration.')
14
-
15
- ss = ThycoticOperations.new(vault_options[:url])
16
- log.log_message("Logging to Thycotic at #{vault_options[:url]}")
13
+ log.setup_logging(true,
14
+ nexpose_options[:log_level],
15
+ nexpose_options[:log_console])
16
+ log.info('Starting integration.')
17
+
18
+ ss = ThycoticOperations.new(vault_options[:url], vault_options[:comment])
19
+ log.info("Logging into Thycotic at #{vault_options[:url]}")
17
20
  token = ss.authenticate(vault_options[:username], vault_options[:password])
18
-
19
- @nx = NexposeOperations.new(nexpose_options[:nexpose_ip],
20
- nexpose_options[:nexpose_username],
21
- nexpose_options[:nexpose_password],
21
+
22
+ @nx = NexposeOperations.new(nexpose_options[:nexpose_ip],
23
+ nexpose_options[:nexpose_username],
24
+ nexpose_options[:nexpose_password],
22
25
  nexpose_options[:nexpose_port])
23
- log.log_message('Processing sites')
26
+ log.info('Processing sites')
24
27
 
25
28
  nexpose_options[:sites].each do |site_id|
26
29
  log.log_debug_message("Processing site #{site_id}")
27
- ips = @nx.get_ips_from_site(site_id)
30
+ addresses = @nx.get_device_addresses(site_id)
31
+
28
32
  site_credentials = []
33
+ if nexpose_options[:clear_creds]
34
+ log.debug('Clearing existing credentials.')
35
+ else
36
+ log.debug('Preserving existing credentials.')
37
+ site_credentials = @nx.get_existing_credentials(site_id)
38
+ end
39
+
40
+ addresses.each do |addr|
41
+ log.debug("Getting credentials for #{addr}")
42
+ summaries = ss.get_secret_summaries(token, addr)
43
+ next if summaries.empty?
44
+
45
+ log.debug("Discovered #{summaries.count} credentials for #{addr}")
46
+
47
+ summaries.each do |summary|
48
+ cred = self.create_credential(ss, token, summary, addr)
29
49
 
30
- ips.each do |ip|
31
- ip_from = ip.instance_of?(Nexpose::HostName) ? ip.host : ip.from
32
- log.log_debug_message("Getting credentials for #{ip_from}")
33
- secret_summary = ss.get_secret_id(token, ip_from)
34
- unless secret_summary.nil?
35
- log.log_debug_message("Found credentials for #{ip_from}")
36
- # Gets OS
37
- asset_data = {}
38
- asset_data[:ip] = ip_from
39
- res = ss.get_secret_simple(token, secret_summary[:secret_id])
40
- asset_data[:username] = res[:username]
41
- asset_data[:password] = res[:password]
42
- credential = Nexpose::Credential.for_service(ss.check_type(secret_summary[:secret_type]),
43
- asset_data[:username],
44
- asset_data[:password],
45
- nil,
46
- ip.from)
47
- site_credentials.push(credential)
50
+ site_credentials.reject! do |c|
51
+ c.host_restriction == cred.host_restriction &&
52
+ c.user_name == cred.user_name && c.service == cred.service
53
+ end
54
+
55
+ site_credentials.push(cred)
48
56
  end
49
57
  end
58
+
50
59
  @nx.save_site(site_id, site_credentials)
51
- log.log_message("Finished processing #{site_id}")
60
+ log.info("Finished processing #{site_id}")
52
61
  end
53
62
  end
54
63
 
64
+ def self.create_credential(connection, token, secret_summary, address)
65
+ # Get the kind of credential e.g. ssh, cifs
66
+ service = connection.check_type(secret_summary[:secret_type])
67
+
68
+ # Define the credential's base properties
69
+ cred_name = secret_summary[:secret_name]
70
+ cred_desc = "Thycotic imported Credential for #{address}"
71
+
72
+ credential = Nexpose::SiteCredentials.for_service(cred_name,
73
+ -1,
74
+ cred_desc,
75
+ address,
76
+ nil,
77
+ service)
78
+ # Retrieve and store the credentials
79
+ res = connection.get_secret(token, secret_summary[:secret_id])
80
+ credential.user_name = res[:username]
81
+ credential.password = res[:password]
82
+
83
+ credential
84
+ end
85
+
55
86
  def self.set_variables(options)
56
87
  settings = {}
57
88
  options.each_key do |key|
@@ -13,6 +13,8 @@
13
13
  # Filters to specific sites one per line, leave empty to generate for all sites.
14
14
  :sites:
15
15
  - '1'
16
+ # Delete existing credentials for the site before importing
17
+ :clear_creds: true
16
18
  :nexpose_options:
17
19
  # (M) Nexpose IP address
18
20
  :nexpose_url: 127.0.0.1
@@ -29,6 +31,8 @@
29
31
  :thycotic_username: user
30
32
  # (M) The password for the above user
31
33
  :thycotic_password: password
34
+ # (M) The comment used when retrieving each password
35
+ :comment: 'Retrieved via Thycotic gem.'
32
36
  :encryption_options:
33
37
  # (M) Path to the encryption.config file
34
38
  :directory: ../../config/encryption.config
@@ -1,3 +1,6 @@
1
+ require 'nexpose_thycotic/utilities/nx_logger'
2
+ require 'set'
3
+
1
4
  module NexposeThycotic
2
5
  class NexposeOperations
3
6
  require 'nexpose'
@@ -7,28 +10,58 @@ module NexposeThycotic
7
10
  def initialize(ip, user, pass, port = 3780)
8
11
  @nsc = Connection.new(ip, user, pass, port)
9
12
  @nsc.login
10
- NexposeThycotic::NxLogger.instance.on_connect(ip, port, @nsc.session_id, "{}")
13
+
14
+ @logger = NexposeThycotic::NxLogger.instance
15
+ @logger.on_connect(ip, port, @nsc.session_id, '{}')
16
+ end
17
+
18
+ def get_site(site_id)
19
+ Site.load(@nsc, site_id)
20
+ end
21
+
22
+ def get_devices_from_site(site_id)
23
+ @nsc.list_devices(site_id)
24
+ end
25
+
26
+ def get_device_addresses(site_id)
27
+ site = get_site(site_id)
28
+ included_targets = site.included_addresses
29
+
30
+ # Get a list of known IP addresses from a site
31
+ devices = get_devices_from_site(site_id)
32
+ device_ips = devices.map { |d| d.address }
33
+
34
+ hosts = []
35
+ ips = device_ips.to_set
36
+ included_targets.each do |address|
37
+ if address.instance_of?(Nexpose::HostName)
38
+ hosts << address.host
39
+ elsif address.to.nil?
40
+ ips.add(address.from)
41
+ end
42
+ end
43
+
44
+ @logger.debug("Discovered #{hosts.count} hosts and #{ips.count} IPs.")
45
+ ips.to_a + hosts
11
46
  end
12
47
 
13
- def get_ips_from_site(site_id)
14
- site = Site.load(@nsc, site_id)
15
- site.assets
48
+ def get_existing_credentials(site_id)
49
+ site = get_site(site_id)
50
+ site.site_credentials
16
51
  end
17
52
 
18
53
  def save_site(site_id, credentials)
19
- site = Site.load(@nsc, site_id)
20
- site.credentials = credentials
54
+ site = get_site(site_id)
55
+ site.site_credentials = credentials
21
56
  site.save(@nsc)
22
57
  end
23
58
 
24
59
  def delete_site_credentials(site_id)
25
- site = Site.load(@nsc, site_id)
26
- site.credentials = []
27
- site.save(@nsc)
60
+ save_site(site_id, [])
28
61
  end
29
62
 
30
63
  def start_scan(site_id)
31
- site = Site.load(@nsc, site_id)
64
+ site = get_site(site_id)
32
65
  site.scan(@nsc)
33
66
  end
34
67
 
@@ -40,11 +73,12 @@ module NexposeThycotic
40
73
  class ThycoticOperations
41
74
  require 'savon'
42
75
  attr_accessor :client
43
- def initialize(url = nil)
76
+ def initialize(url = nil, comment = '')
44
77
  # log: true, log_level: :info
45
- @client = Savon.client(
46
- wsdl: url,
47
- ssl_verify_mode: :none)
78
+ @client = Savon.client(wsdl: url, ssl_verify_mode: :none)
79
+
80
+ # Comment used when retrieving passwords
81
+ @comment = comment
48
82
  end
49
83
 
50
84
  def operations
@@ -70,67 +104,83 @@ module NexposeThycotic
70
104
  end
71
105
 
72
106
  def authenticate(username, password)
73
- auth = @client.call(:authenticate, message: { username: username, password: password })
74
- auth_response = auth.hash
75
- auth_result = auth_response[:envelope][:body][:authenticate_response][:authenticate_result]
76
- CheckForErrors(auth_result)
107
+ operation = :authenticate
108
+ message = { username: username, password: password }
109
+ auth_result = get_secret_result(operation, message)
110
+ check_for_errors(auth_result)
77
111
  @token = auth_result[:token]
78
112
  end
79
113
 
80
- def parse_field(secret_response_result, fieldName)
81
- items = secret_response_result[:secret][:items]
82
- for item in items[:secret_item]
83
- if item[:field_display_name].downcase == fieldName.downcase then
84
- return item[:value]
85
- end
86
- end
114
+ def parse_field(secret_response_result, field_name)
115
+ items = secret_response_result[:secret][:items][:secret_item]
116
+ item = items.find { |i| i[:field_display_name].casecmp(field_name) == 0 }
117
+ item[:value]
87
118
  end
88
119
 
89
- def get_secret_id(token, ip)
90
- secret_id = @client.call( :search_secrets_by_field_value, message: { token: token, fieldName: 'machine', searchTerm: ip, showDeleted: true, showRestricted: true})
91
- secret_result = secret_id.hash
92
- unless secret_result[:envelope][:body][:search_secrets_by_field_value_response][:search_secrets_by_field_value_result][:secret_summaries].nil? then
93
- secret_summary = secret_result[:envelope][:body][:search_secrets_by_field_value_response][:search_secrets_by_field_value_result][:secret_summaries][:secret_summary]
94
- secret_info = { secret_id: secret_summary[:secret_id], secret_type: secret_summary[:secret_type_name] }
95
- end
120
+ def get_secret_result(operation, message)
121
+ secret = @client.call(operation, message: message)
122
+ resp = secret.hash[:envelope][:body]["#{operation}_response".to_sym]
123
+ resp["#{operation}_result".to_sym]
96
124
  end
97
125
 
98
- def get_secret_simple(token, secret_id)
99
- secret = @client.call( :get_secret_legacy, message: { token: token, secretId: secret_id } )
100
- secret_response = secret.hash
101
- secret_response_result = secret_response[:envelope][:body][:get_secret_legacy_response][:get_secret_legacy_result]
102
- CheckForErrors(secret_response_result)
103
- username = parse_field(secret_response_result, "Username")
104
- password = parse_field(secret_response_result, "Password")
105
- asset = { username: username, password: password }
126
+ def get_secret_summaries(token, ip)
127
+ operation = :search_secrets_by_field_value
128
+ message = {
129
+ token: token,
130
+ fieldName: 'machine',
131
+ searchTerm: ip,
132
+ showDeleted: true,
133
+ showRestricted: true
134
+ }
135
+ secret_result = get_secret_result(operation, message)
136
+
137
+ secrets = []
138
+ unless secret_result[:secret_summaries].nil?
139
+ summaries = secret_result[:secret_summaries][:secret_summary]
140
+
141
+ # Ensure summaries is iterable
142
+ summaries = [summaries] if summaries.is_a?(Hash)
143
+
144
+ summaries.each do |secret|
145
+ secret_info = {
146
+ secret_id: secret[:secret_id],
147
+ secret_type: secret[:secret_type_name],
148
+ secret_name: secret[:secret_name]
149
+ }
150
+ secrets << secret_info
151
+ end
152
+ end
106
153
 
154
+ secrets
107
155
  end
108
156
 
109
157
  def get_secret(token, secret_id)
110
- #Code Responses are used if additional information is required when getting the Secret (Ex: Requires Comment is turned on so need to pass Code Response as..)
111
- code_response = nil
112
- #code_response = @client.new("codeResponse") #Code is not working, need to create object of type codeResponse from the wsdl
113
- #code_response.ErrorCode = "COMMENT";
114
- #code_response.Comment = "Running scan";
115
-
116
- #CodeResponses is an array because there could be multiple additional responses needed
117
- code_responses = [code_response]
118
-
119
- secret = @client.call( :get_secret, message: { token: token, secretId: secret_id, CodeResponse: code_responses } )
120
- secret_response = secret.hash
121
- secret_response_result = secret_response[:envelope][:body][:get_secret_response][:get_secret_result]
122
- CheckForErrors(secret_response_result)
123
- username = parse_field(secret_response_result, "Username")
124
- password = parse_field(secret_response_result, "Password")
158
+ operation = :get_secret
159
+ message = { token: token,
160
+ secretId: secret_id,
161
+ loadSettingsAndPermissions: false,
162
+ "codeResponses" => {"CodeResponse" =>
163
+ [{
164
+ "ErrorCode" => "COMMENT",
165
+ "Comment" => @comment
166
+ }]
167
+ }}
168
+ secret_result = get_secret_result(operation, message)
169
+
170
+ check_for_errors(secret_result)
171
+ username = parse_field(secret_result, 'Username')
172
+ password = parse_field(secret_result, 'Password')
173
+
174
+ { username: username, password: password }
125
175
  end
126
176
 
127
- def CheckForErrors(result)
177
+ def check_for_errors(result)
128
178
  errors = result[:errors]
129
- if !errors.blank? then
179
+ unless errors.blank?
130
180
  puts errors
181
+ #TODO: Logging
131
182
  raise Exception.new(errors)
132
183
  end
133
-
134
184
  end
135
185
  end
136
- end
186
+ end
@@ -31,7 +31,11 @@ module NexposeThycotic
31
31
  end
32
32
  end
33
33
 
34
- def setup_logging(enabled, log_level = 'info', stdout=false)
34
+ def setup_logging(enabled,
35
+ log_level = 'info',
36
+ stdout=false,
37
+ rotation='weekly',
38
+ log_size=nil)
35
39
  @stdout = stdout
36
40
 
37
41
  log_message('Logging disabled.') unless enabled || @log.nil?
@@ -46,9 +50,9 @@ module NexposeThycotic
46
50
  io = IO.for_fd(IO.sysopen(@logger_file, 'a'), 'a')
47
51
  io.autoclose = false
48
52
  io.sync = true
49
- @log = Logger.new(io, 'weekly')
50
- @log.level = if log_level.to_s.casecmp('info') == 0
51
- Logger::INFO
53
+ @log = Logger.new(io, rotation, log_size)
54
+ @log.level = if log_level.to_s.casecmp('info') == 0
55
+ Logger::INFO
52
56
  else
53
57
  Logger::DEBUG
54
58
  end
@@ -58,11 +62,12 @@ module NexposeThycotic
58
62
  def create_calls
59
63
  levels = [:info, :debug, :error, :warn]
60
64
  levels.each do |level|
61
- method_name =
62
- define_singleton_method("log_#{level.to_s}_message") do |message|
65
+ method_name = "log_#{level.to_s}_message"
66
+ define_singleton_method(method_name) do |message|
63
67
  puts message if @stdout
64
68
  @log.send(level, message) unless !@enabled || @log.nil?
65
69
  end
70
+ singleton_class.send(:alias_method, level, method_name.to_sym)
66
71
  end
67
72
  end
68
73
 
@@ -93,8 +98,8 @@ module NexposeThycotic
93
98
  end
94
99
 
95
100
  def get_product(product, version)
96
- return nil if ((product.nil? || product.empty?) ||
97
- (version.nil? || version.empty?))
101
+ return nil if ((product.nil? || product.empty?) ||
102
+ (version.nil? || version.empty?))
98
103
 
99
104
  product.gsub!('-', '_')
100
105
  product.slice! product.rindex('_') until product.count('_') <= 1
@@ -148,7 +153,7 @@ module NexposeThycotic
148
153
  log_stat_message('Statistics collection not enabled.')
149
154
  return
150
155
  end
151
-
156
+
152
157
  begin
153
158
  payload = generate_payload value
154
159
  send(nexpose_address, nexpose_port, session_id, payload)
@@ -1,5 +1,5 @@
1
1
  module NexposeThycotic
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  VENDOR = "Thycotic"
4
4
  PRODUCT = "Secret Server"
5
5
  def self.show_version
metadata CHANGED
@@ -1,43 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexpose_thycotic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damian Finol, Adam Robinson
8
+ - David Valente
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2017-05-30 00:00:00.000000000 Z
12
+ date: 2018-04-06 00:00:00.000000000 Z
12
13
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.5'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.5'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
14
  - !ruby/object:Gem::Dependency
42
15
  name: savon
43
16
  requirement: !ruby/object:Gem::Requirement
@@ -52,34 +25,20 @@ dependencies:
52
25
  - - ">="
53
26
  - !ruby/object:Gem::Version
54
27
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubyntlm
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
28
  - !ruby/object:Gem::Dependency
70
29
  name: nexpose
71
30
  requirement: !ruby/object:Gem::Requirement
72
31
  requirements:
73
32
  - - "~>"
74
33
  - !ruby/object:Gem::Version
75
- version: '0.9'
34
+ version: '7.2'
76
35
  type: :runtime
77
36
  prerelease: false
78
37
  version_requirements: !ruby/object:Gem::Requirement
79
38
  requirements:
80
39
  - - "~>"
81
40
  - !ruby/object:Gem::Version
82
- version: '0.9'
41
+ version: '7.2'
83
42
  - !ruby/object:Gem::Dependency
84
43
  name: symmetric-encryption
85
44
  requirement: !ruby/object:Gem::Requirement
@@ -143,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
102
  version: '0'
144
103
  requirements: []
145
104
  rubyforge_project:
146
- rubygems_version: 2.5.1
105
+ rubygems_version: 2.6.14
147
106
  signing_key:
148
107
  specification_version: 4
149
108
  summary: Nexpose Thycotic Gem Integration