securden 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3ab035ae2404108a3ad9234d90687856acdeef487c9c39f1ccc2d9a1d7292022
4
+ data.tar.gz: bc792eae5a82c88107a444d99269c254cfb4a8d743d1e1452b66c9ffdea9a2b2
5
+ SHA512:
6
+ metadata.gz: 257699c10ca6879c21935259df6625c5bdc92036c20efa7b8296ed390a6a9cf8c29191f30325b7dee7558251686e44a9cf22262201668cfde82d242ecf608d8a
7
+ data.tar.gz: af2dd70f75d6f4902a870ee18079145fce2afef24c46a1cc6dcefd9408cd030d34357a2932ef8784d3a50a297325f3eca08441d9f846beed5253de47f5b06300
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 securden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Securden Chef Plugin
2
+
3
+ The Securden Chef plugin enables seamless integration with Securden Secrets Manager, making it easy to fetch and utilize account details within your Chef recipes. This guide provides a detailed overview of the plugin's features and usage.
4
+
5
+ ## Installation
6
+
7
+ Add the Securden Chef plugin gem to your Chef environment. Ensure all required dependencies, including Ruby and the Chef client, are properly installed and configured.
8
+
9
+ ## Initialization
10
+
11
+ The Securden plugin is initialized with the following parameters:
12
+ - `server_url`: The URL of your Securden Secrets Manager server.
13
+ - `authtoken`: The authentication token required for API access.
14
+ - `certificate`: (Optional) Path to the certificate file. If omitted, the plugin automatically fetches the certificate.
15
+
16
+ ## Usage Example
17
+
18
+ ```ruby
19
+ require 'Securden'
20
+
21
+ securden = nil
22
+
23
+ ruby_block 'Plugin Initialize Block' do
24
+ block do
25
+ securden = Securden::Account.new(
26
+ server_url: node['securden']['server_url'],
27
+ authtoken: node['securden']['authtoken'],
28
+ certificate: node['securden']['certificate'] # Certificate is optional
29
+ )
30
+ end
31
+ end
32
+
33
+ ruby_block 'Get Account Block' do
34
+ block do
35
+ if securden
36
+ account = securden.get(account_id: node['securden']['account_id'])
37
+ if account
38
+ puts "\nPassword: #{account['password']}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ ## Access
46
+
47
+ Once the account data is fetched, you can access its attributes directly, such as `password`, or any additional fields created in the account.
48
+
49
+ ## Example Workflow
50
+
51
+ 1. Define necessary `node` attributes, such as `server_url`, `authtoken`, and optionally, `certificate`.
52
+ 2. Initialize the Securden plugin using the `Securden::Account.new` method.
53
+ 3. Use the `get` method to fetch account details.
54
+ 4. Access and utilize the required attributes in your Chef recipes.
55
+
56
+ ## Key Features
57
+
58
+ 1. **Plugin Initialization**:
59
+ - Simplifies the initialization process with essential parameters (`server_url`, `authtoken`, `certificate`).
60
+
61
+ 2. **Fetching Account Data**:
62
+ - Retrieve account details using the `get` method by providing any of the following:
63
+ - `account_id`
64
+ - `account_title`
65
+ - `account_name`
66
+ - Combination of `account_name` and `account_title`
67
+
68
+ 3. **Accessing Account Attributes**:
69
+ - Access the fetched account's attributes, such as `password`, or additional fields created in the account.
70
+
71
+ ## Additional Notes
72
+
73
+ - Ensure that `server_url` and `authtoken` values are securely stored and not hardcoded in your recipes.
74
+ - The `certificate` parameter is optional; the plugin will automatically fetch it if not provided.
75
+ - Additional fields in the account can be accessed using their respective names.
76
+
77
+ For further details and support, refer to the official Securden documentation.
78
+
@@ -0,0 +1,4 @@
1
+ module Securden
2
+ VERSION = "0.0.1"
3
+ end
4
+
data/lib/securden.rb ADDED
@@ -0,0 +1,420 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "openssl"
4
+
5
+ module Securden
6
+ # request header constants
7
+ AUTHTOKEN = "authtoken"
8
+ ORG = "org"
9
+ # request constants
10
+ GET = "GET"
11
+ POST = "POST"
12
+ PUT = "PUT"
13
+ DELETE = "DELETE"
14
+
15
+ # log contants
16
+ INFO = "info"
17
+ ERROR = "error"
18
+ WARN = "warn"
19
+ DEBUG = "debug"
20
+
21
+ class Error < StandardError; end
22
+
23
+ class << self
24
+ attr_accessor :server_url, :authtoken, :org, :certificate
25
+ def log(message:, level: )
26
+ message = "[SECURDEN] " + message.to_s
27
+ case level
28
+ when ERROR
29
+ Chef::Log.error(message)
30
+ nil
31
+ when DEBUG
32
+ Chef::Log.debug(message)
33
+ when WARN
34
+ Chef::Log.warn(message)
35
+ when INFO
36
+ Chef::Log.info(message)
37
+ end
38
+ end
39
+ end
40
+
41
+ class Init
42
+ def initialize(server_url:, authtoken:, org: nil, certificate: nil)
43
+ if server_url.nil? || authtoken.nil?
44
+ Securden.log(message: "Server URL and authtoken are required to initialize Securden", level: ERROR)
45
+ return nil
46
+ end
47
+ unless server_url.start_with?("http://", "https://")
48
+ Securden.log(message: "Invalid server URL. It must begin with either http:// or https://", level: ERROR)
49
+ return nil
50
+ end
51
+ Securden.server_url = server_url
52
+ Securden.authtoken = authtoken
53
+ Securden.org = org unless org.nil? || org.nil?
54
+ Securden.certificate = certificate
55
+ Securden.log(message: "Securden initialized successfully", level: DEBUG)
56
+ end
57
+ end
58
+
59
+ class Account
60
+ def self.get(account_id: nil, account_name: nil, account_title: nil, account_type: nil)
61
+ begin
62
+ unless Securden.server_url && Securden.authtoken
63
+ Securden.log(message: "Securden has not been initialized. Please initialize before using", level: ERROR)
64
+ return nil
65
+ end
66
+ if account_id.nil? && account_title.nil? && account_name.nil? && account_type.nil?
67
+ Securden.log(message: "Required account attributes Account ID, Account title or Account name", level: ERROR)
68
+ return nil
69
+ end
70
+ Securden.log(message: "Fetching account from Securden", level: DEBUG)
71
+ params = {}
72
+ params["account_id"] = account_id.to_i if account_id
73
+ params["account_title"] = account_title unless account_title.nil?
74
+ params["account_name"] = account_name unless account_name.nil?
75
+ params["account_type"] = account_type unless account_type.nil?
76
+ account = Request.new.raise_request(params, "/secretsmanagement/get_account", GET)
77
+ if account
78
+ if account["message"]
79
+ Securden.log(message: account["message"], level: DEBUG)
80
+ end
81
+ return account
82
+ else
83
+ return nil
84
+ end
85
+ rescue StandardError => e
86
+ Securden.log(message: "Failed to fetch account data: #{e.message}", level: ERROR)
87
+ return nil
88
+ end
89
+ end
90
+
91
+ def self.add(account_title: nil, account_name: nil, account_type: nil, password:nil, ipaddress: nil, notes: nil, tags: nil, personal_account: nil, folder_id: nil, account_expiration_date: nil, distinguished_name: nil, account_alias: nil, domain_name: nil)
92
+ begin
93
+ unless Securden.server_url && Securden.authtoken
94
+ Securden.log(message: "Securden has not been initialized. Please initialize before using", level: ERROR)
95
+ return nil
96
+ end
97
+ if account_type.nil?
98
+ Securden.log(message: "Required Account type", level: ERROR)
99
+ return nil
100
+ elsif account_title.nil?
101
+ Securden.log(message: "Required account title", level: ERROR)
102
+ return nil
103
+ end
104
+ Securden.log(message: "Adding account", level: DEBUG)
105
+ params = {}
106
+ params["account_title"] = account_title
107
+ params["account_type"] = account_type
108
+ params["account_name"] = account_name unless account_name.nil?
109
+ params["personal_account"] = personal_account unless personal_account.nil?
110
+ params["ipaddress"] = ipaddress unless ipaddress.nil?
111
+ params["notes"] = notes unless notes.nil?
112
+ params["tags"] = tags unless tags.nil?
113
+ params["folder_id"] = folder_id unless folder_id.nil?
114
+ params["password"] = password unless password.nil?
115
+ params["account_expiration_date"] = account_expiration_date unless account_expiration_date.nil?
116
+ params["distinguished_name"] = distinguished_name unless distinguished_name.nil?
117
+ params["account_alias"] = account_alias unless account_alias.nil?
118
+ params["domain_name"] = domain_name unless domain_name.nil?
119
+ account = Request.new.raise_request(params, "/api/add_account", POST)
120
+ if account
121
+ if account["message"]
122
+ Securden.log(message: account["message"], level: DEBUG)
123
+ end
124
+ return account
125
+ else
126
+ Securden.log(message: "Could not add account", level: ERROR)
127
+ return nil
128
+ end
129
+ end
130
+ end
131
+
132
+ def self.edit(account_id: nil, account_title: nil, account_name: nil, account_type: nil, ipaddress: nil, notes: nil, tags: nil, personal_account: nil, folder_id: nil, account_expiration_date: nil, distinguished_name: nil, account_alias: nil, domain_name: nil)
133
+ begin
134
+ unless Securden.server_url && Securden.authtoken
135
+ Securden.log(message: "Securden has not been initialized. Please initialize before using", level: ERROR)
136
+ return nil
137
+ end
138
+ Securden.log(message: "Updating account", level: DEBUG)
139
+ if account_id.nil? || account_type.nil?
140
+ Securden.log(message: "Required account ID and Account type", level: ERROR)
141
+ return nil
142
+ end
143
+ params = {}
144
+ params["account_id"] = account_id
145
+ params["account_type"] = account_type
146
+ params["account_title"] = account_title unless account_title.nil?
147
+ params["account_name"] = account_name unless account_name.nil?
148
+ params["ipaddress"] = ipaddress unless ipaddress.nil?
149
+ params["notes"] = notes unless notes.nil?
150
+ params["tags"] = tags unless tags.nil?
151
+ params["personal_account"] = personal_account unless personal_account.nil?
152
+ params["folder_id"] = folder_id unless folder_id.nil?
153
+ params["account_expiration_date"] = account_expiration_date unless account_expiration_date.nil?
154
+ params["distinguished_name"] = distinguished_name unless distinguished_name.nil?
155
+ params["account_alias"] = account_alias unless account_alias.nil?
156
+ params["domain_name"] = domain_name unless domain_name.nil?
157
+ account = Request.new.raise_request(params, "/api/edit_account", PUT)
158
+ if account
159
+ if account["message"]
160
+ Securden.log(message: account["message"], level: DEBUG)
161
+ end
162
+ return account
163
+ else
164
+ Securden.log(message: "Could not update account", level: ERROR)
165
+ return nil
166
+ end
167
+ rescue StandardError => e
168
+ Securden.log(message: "Error updating account: #{e.message}", level: ERROR)
169
+ return nil
170
+ end
171
+ end
172
+ end
173
+
174
+ class Accounts
175
+ def self.get(account_ids:)
176
+ begin
177
+ if Securden.server_url.nil? || Securden.authtoken.nil? || Securden.server_url.strip.empty? || Securden.authtoken.strip.empty?
178
+ Securden.log(message: "Securden has not been initialized. Please initialize before using", level: ERROR)
179
+ return nil
180
+ end
181
+ params = {}
182
+ if account_ids.nil? || account_ids.empty?
183
+ Securden.log(message: "Required account IDs", level: ERROR)
184
+ return nil
185
+ end
186
+ Securden.log(message: "Fetching accounts from Securden", level: DEBUG)
187
+ params["account_ids"] = account_ids
188
+ accounts = Request.new.raise_request(params, "/secretsmanagement/get_accounts", POST)
189
+ if accounts
190
+ if accounts["message"]
191
+ Securden.log(message: accounts["message"], level: DEBUG)
192
+ end
193
+ return accounts
194
+ else
195
+ Securden.log(message: "Could not fetch accounts", level: ERROR)
196
+ return nil
197
+ end
198
+ rescue StandardError => e
199
+ Securden.log(message: "Error fetching accounts: #{e.message}", level: ERROR)
200
+ return nil
201
+ end
202
+ end
203
+
204
+ def self.delete(account_ids:, reason: nil, delete_permanently: nil)
205
+ begin
206
+ if Securden.server_url.nil? || Securden.authtoken.nil?
207
+ Securden.log(message: "Securden has not been initialized. Please initialize before using", level: ERROR)
208
+ return nil
209
+ end
210
+ params = {}
211
+ if account_ids.nil? || account_ids.empty?
212
+ Securden.log(message: "Required account IDs", level: ERROR)
213
+ return nil
214
+ end
215
+ params["account_ids"] = account_ids
216
+ params["reason"] = reason unless reason.nil?
217
+ params["delete_permanently"] = delete_permanently unless delete_permanently.nil?
218
+ Securden.log(message: "Deleting accounts", level: INFO)
219
+ accounts = Request.new.raise_request(params, "/api/delete_accounts", DELETE)
220
+ if accounts
221
+ if accounts["message"]
222
+ Securden.log(message: accounts["message"], level: DEBUG)
223
+ end
224
+ return accounts
225
+ else
226
+ Securden.log(message: "Could not delete accounts", level: ERROR)
227
+ return nil
228
+ end
229
+ rescue StandardError => e
230
+ Securden.log(message: "Error deleting accounts: #{e.message}", level: ERROR)
231
+ return nil
232
+ end
233
+ end
234
+ end
235
+
236
+ class Request
237
+ def raise_request(payload, request_path, method)
238
+ begin
239
+ Securden.log(message: "Raising API request to Securden server", level: DEBUG)
240
+ uri = URI(Securden.server_url + request_path)
241
+ if method == GET
242
+ uri.query = URI.encode_www_form(payload) unless payload.empty?
243
+ end
244
+ http = set_http(uri)
245
+ case method
246
+ when GET
247
+ request = Net::HTTP::Get.new(uri)
248
+ when POST
249
+ request = Net::HTTP::Post.new(uri)
250
+ when PUT
251
+ request = Net::HTTP::Put.new(uri)
252
+ when DELETE
253
+ request = Net::HTTP::Delete.new(uri)
254
+ end
255
+ request = set_header(request)
256
+ if method != GET
257
+ request["Content-Type"] = "application/json"
258
+ end
259
+ request.body = payload.to_json unless payload.nil? || payload.empty?
260
+ response = http.request(request)
261
+ return handle_response(response)
262
+ rescue StandardError => e
263
+ Securden.log(message: "#{e}", level: ERROR)
264
+ return nil
265
+ end
266
+ end
267
+
268
+ protected
269
+ def handle_response(response)
270
+ begin
271
+ status_code = nil
272
+ if response.code && !response.code.strip.empty?
273
+ status_code = response.code.to_i
274
+ else response.status_code && !response.status_code.strip.empty?
275
+ status_code = response.status_code.to_i
276
+ end
277
+ response = JSON.parse(response.body)
278
+ if !status_code.nil? && status_code == 200
279
+ return response
280
+ elsif response['error']
281
+ message = response['error']['message']
282
+ else
283
+ message = response['message']
284
+ end
285
+ Securden.log(message: "#{response['status_code']}: #{message}", level: ERROR)
286
+ return nil
287
+ rescue StandardError => e
288
+ Securden.log(message: "HTTP request failed: #{e.message}", level: ERROR)
289
+ return nil
290
+ end
291
+ end
292
+
293
+ def set_header(request)
294
+ request[AUTHTOKEN] = Securden.authtoken
295
+ request[ORG] = Securden.org if Securden.org && !Securden.org.strip.empty?
296
+ request
297
+ end
298
+
299
+ def set_http(uri)
300
+ begin
301
+ Securden.log(message: "Setting up HTTP connection.", level: DEBUG)
302
+ http = Net::HTTP.new(uri.host, uri.port)
303
+ http.use_ssl = uri.scheme == "https"
304
+ if uri.scheme == "https"
305
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
306
+ configure_ssl(http, uri)
307
+ end
308
+ http
309
+ rescue StandardError => e
310
+ Securden.log(message: "Failed to set HTTP connection: #{e.message}", level: ERROR)
311
+ return nil
312
+ end
313
+ end
314
+
315
+ def configure_ssl(http, uri)
316
+ begin
317
+ if Securden.server_url.start_with?("https://")
318
+ if Securden.certificate
319
+ handle_ssl_certificate(http)
320
+ else
321
+ Securden.log(message: "No SSL certificate provided. Attempting to fetch from server.", level: DEBUG)
322
+ fetch_and_set_server_certificate(http, uri)
323
+ end
324
+ end
325
+ rescue StandardError => e
326
+ Securden.log(message: "Failed to configure SSL: #{e.message}", level: ERROR)
327
+ return nil
328
+ end
329
+ end
330
+
331
+ def handle_ssl_certificate(http)
332
+ begin
333
+ Securden.log(message: "Adding SSL certificate.", level: INFO)
334
+ if Securden.certificate.start_with?("-----BEGIN CERTIFICATE-----")
335
+ cert_object = OpenSSL::X509::Certificate.new(Securden.certificate)
336
+ http.cert_store = OpenSSL::X509::Store.new
337
+ http.cert_store.add_cert(cert_object)
338
+ elsif File.exist?(Securden.certificate)
339
+ http.ca_file = Securden.certificate
340
+ else
341
+ Securden.log(message: "Invalid certificate value or file path", level: ERROR)
342
+ return nil
343
+ end
344
+ http
345
+ rescue OpenSSL::X509::CertificateError => e
346
+ Securden.log(message: "Invalid certificate format: #{e.message}", level: ERROR)
347
+ return nil
348
+ end
349
+ end
350
+
351
+ def verify_certificate_domain(cert, uri)
352
+ cn = cert.subject.to_s.match(/CN=([^\s\/,]+)/i)&.captures&.first
353
+ sans = cert.extensions.select { |ext| ext.oid == 'subjectAltName' }.flat_map { |ext| ext.value.split(', ').grep(/^DNS:/) }.map { |dns| dns.gsub(/^DNS:/, '') }
354
+ cert_domains = [cn, *sans].compact
355
+ cert_domains.any? do |domain|
356
+ if domain.start_with?('*.')
357
+ uri.host.end_with?(domain[2..-1])
358
+ else
359
+ uri.host == domain
360
+ end
361
+ end
362
+ end
363
+
364
+ def fetch_and_set_server_certificate(http, uri)
365
+ begin
366
+ Securden.log(message: "Attempting to fetch SSL certificate from #{uri.host}:#{uri.port}", level: DEBUG)
367
+ tcp_socket = TCPSocket.new(uri.host, uri.port || 443)
368
+ ssl_context = OpenSSL::SSL::SSLContext.new
369
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
370
+ ssl_socket.sync_close = true
371
+ ssl_socket.connect
372
+ peer_cert = ssl_socket.peer_cert
373
+ peer_cert_chain = ssl_socket.peer_cert_chain || [peer_cert]
374
+ unless verify_certificate_domain(peer_cert, uri)
375
+ Securden.log(message: "Hostname mismatch: Certificate is for #{peer_cert.subject}, requested #{uri.host}", level: DEBUG)
376
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
377
+ return
378
+ end
379
+ ssl_socket.close
380
+ tcp_socket.close
381
+ http.cert_store = OpenSSL::X509::Store.new
382
+ http.cert_store.set_default_paths
383
+ peer_cert_chain.each do |cert|
384
+ begin
385
+ http.cert_store.add_cert(cert)
386
+ rescue OpenSSL::X509::StoreError => e
387
+ Securden.log(message: "Certificate already in store or invalid: #{e.message}", level: DEBUG)
388
+ end
389
+ end
390
+ verification_result = http.cert_store.verify(peer_cert_chain.first)
391
+ if verification_result
392
+ Securden.log(message: "Successfully verified certificate", level: DEBUG)
393
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
394
+ else
395
+ Securden.log(message: "Certificate verification failed: #{http.cert_store.error_string}", level: WARN)
396
+ if self_signed?(peer_cert_chain.first)
397
+ Securden.log(message: "Server uses self-signed certificate. Adding to trust store.", level: DEBUG)
398
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
399
+ else
400
+ Securden.log(message: "Untrusted certificate. Disabling SSL verification.", level: WARN)
401
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
402
+ end
403
+ end
404
+
405
+ rescue StandardError => e
406
+ Securden.log(message: "Failed to establish secure connection: #{e.message}. Disabling SSL verification", level: WARN)
407
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
408
+ ensure
409
+ ssl_socket&.close rescue nil
410
+ tcp_socket&.close rescue nil
411
+ end
412
+ end
413
+
414
+ def self_signed?(cert)
415
+ cert.subject.to_s == cert.issuer.to_s
416
+ end
417
+ end
418
+
419
+ private_constant :Request
420
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: securden
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Securden
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - devops-support@securden.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - lib/securden.rb
23
+ - lib/securden/version.rb
24
+ homepage: https://securden.com
25
+ licenses:
26
+ - Apache-2.0
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.3.27
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Leverage the Chef development environment for seamless access to passwords,
47
+ secrets, certificates, and keys stored in Securden.
48
+ test_files: []