puppetserver-ca 2.4.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6df60141ee08a19cc170166263abff7028ee6085bf833981efad3458cef5ce26
4
- data.tar.gz: 4da9da0d545605f13bd0dfa2a6272a301dfe46251d0277663e8d09e6d3f65a29
3
+ metadata.gz: 78b8703a01de06ec5c4a6202f8341662452f4d33d37c1390b558556ce817253d
4
+ data.tar.gz: eea0c603eee53c14a82de6b80f263014ca76261eb39fa2b679031ad6ecc69b1f
5
5
  SHA512:
6
- metadata.gz: 15891316786fa4fcd63c3f86ac2f66051841fb3e680fe63e5d0d6ceb025241cc5c122ae1f3b2819c9b1063f7c1a166efd1ebd010b35921fb2c6f3fef2917da88
7
- data.tar.gz: fee93fa98f038ab7eb660fe55ca466874221cb980e5794c8d32c7561a2dfa069cdc003046799b89ef99b0f6b7bc97b58928e788b1e057f5ef68362bd864c16c7
6
+ metadata.gz: 64ff93865e33b2dccd3f4308580278315d007b450611acdbb36631aabc9794d82b5f8973aa5872384f05146abe62dfa3290b5e3774f8816bafa4b70db7bf3fbc
7
+ data.tar.gz: 734dc57fa4e9f8ce390a966502ea8104cb7640aec4564759ca39c377fdaeb06ed8992fbab88dfa3ef5a2c24921e6400223e4bee2c902d0f4f92f676537f779c5
@@ -0,0 +1,37 @@
1
+ name: mend_scan
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: checkout repo content
13
+ uses: actions/checkout@v2 # checkout the repository content to github runner.
14
+ with:
15
+ fetch-depth: 1
16
+ - name: setup ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: 2.7
20
+ - name: create lock
21
+ run: bundle lock
22
+ # install java
23
+ - uses: actions/setup-java@v3
24
+ with:
25
+ distribution: 'temurin' # See 'Supported distributions' for available options
26
+ java-version: '17'
27
+ # download mend
28
+ - name: download_mend
29
+ run: curl -o wss-unified-agent.jar https://unified-agent.s3.amazonaws.com/wss-unified-agent.jar
30
+ - name: run mend
31
+ run: java -jar wss-unified-agent.jar
32
+ env:
33
+ WS_APIKEY: ${{ secrets.MEND_API_KEY }}
34
+ WS_WSS_URL: https://saas-eu.whitesourcesoftware.com/agent
35
+ WS_USERKEY: ${{ secrets.MEND_TOKEN }}
36
+ WS_PRODUCTNAME: Puppet Enterprise
37
+ WS_PROJECTNAME: ${{ github.event.repository.name }}
data/.travis.yml CHANGED
@@ -8,6 +8,8 @@ rvm:
8
8
  - 2.5
9
9
  - 2.6
10
10
  - 2.7
11
+ - 3.1
12
+ - 3.2
11
13
  before_install:
12
14
  gem install bundler -v 1.16.1 && (gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true)
13
15
  script:
@@ -0,0 +1,286 @@
1
+ require 'openssl'
2
+ require 'optparse'
3
+ require 'time'
4
+ require 'puppetserver/ca/certificate_authority'
5
+ require 'puppetserver/ca/config/puppet'
6
+ require 'puppetserver/ca/errors'
7
+ require 'puppetserver/ca/utils/cli_parsing'
8
+ require 'puppetserver/ca/utils/file_system'
9
+ require 'puppetserver/ca/utils/inventory'
10
+ require 'puppetserver/ca/x509_loader'
11
+
12
+ module Puppetserver
13
+ module Ca
14
+ module Action
15
+ class Delete
16
+ include Puppetserver::Ca::Utils
17
+
18
+ CERTNAME_BLOCKLIST = %w{--config --expired --revoked --all}
19
+
20
+ SUMMARY = 'Delete signed certificate(s) from disk'
21
+ BANNER = <<-BANNER
22
+ Usage:
23
+ puppetserver ca delete [--help]
24
+ puppetserver ca delete [--config CONF] [--expired] [--revoked]
25
+ [--certname NAME[,NAME]] [--all]
26
+
27
+ Description:
28
+ Deletes signed certificates from disk. Once a certificate is
29
+ signed and delivered to a node, it no longer necessarily needs
30
+ to be stored on disk.
31
+
32
+ Options:
33
+ BANNER
34
+
35
+ def initialize(logger)
36
+ @logger = logger
37
+ end
38
+
39
+ def self.parser(parsed = {})
40
+ OptionParser.new do |opts|
41
+ opts.banner = BANNER
42
+ opts.on('--help', 'Display this command-specific help output') do |help|
43
+ parsed['help'] = true
44
+ end
45
+ opts.on('--config CONF', 'Path to puppet.conf') do |conf|
46
+ parsed['config'] = conf
47
+ end
48
+ opts.on('--expired', 'Delete expired signed certificates') do |expired|
49
+ parsed['expired'] = true
50
+ end
51
+ opts.on('--revoked', 'Delete signed certificates that have already been revoked') do |revoked|
52
+ parsed['revoked'] = true
53
+ end
54
+ opts.on('--certname NAME[,NAME]', Array,
55
+ 'One or more comma-separated certnames for which to delete signed certificates') do |certs|
56
+ parsed['certname'] = [certs].flatten
57
+ end
58
+ opts.on('--all', 'Delete all signed certificates on disk') do |all|
59
+ parsed['all'] = true
60
+ end
61
+ end
62
+ end
63
+
64
+ def parse(args)
65
+ results = {}
66
+ parser = self.class.parser(results)
67
+
68
+ errors = CliParsing.parse_with_errors(parser, args)
69
+
70
+ if results['certname']
71
+ results['certname'].each do |certname|
72
+ if CERTNAME_BLOCKLIST.include?(certname)
73
+ errors << " Cannot manage cert named `#{certname}` from "+
74
+ "the CLI. If needed, use the HTTP API directly."
75
+ end
76
+ end
77
+ end
78
+
79
+ unless results['help'] || results['expired'] || results['revoked'] || results['certname'] || results['all']
80
+ errors << ' Must pass one of the valid flags to determine which certs to delete'
81
+ end
82
+
83
+ if results['all'] && (results['expired'] || results['revoked'] || results['certname'])
84
+ errors << ' The --all flag must not be used with --expired, --revoked, or --certname'
85
+ end
86
+
87
+ errors_were_handled = Errors.handle_with_usage(@logger, errors, parser.help)
88
+
89
+ exit_code = errors_were_handled ? 1 : nil
90
+ return results, exit_code
91
+ end
92
+
93
+ def run(args)
94
+ config = args['config']
95
+
96
+ # Validate the config path
97
+ if config
98
+ errors = FileSystem.validate_file_paths(config)
99
+ return 1 if Errors.handle_with_usage(@logger, errors)
100
+ end
101
+
102
+ # Validate puppet config setting
103
+ puppet = Config::Puppet.parse(config, @logger)
104
+ settings = puppet.settings
105
+ return 1 if Errors.handle_with_usage(@logger, puppet.errors)
106
+
107
+ # Validate that we are offline
108
+ return 1 if HttpClient.check_server_online(settings, @logger)
109
+
110
+ # Perform the desired action, keeping track if any errors occurred
111
+ errored = false
112
+ deleted_count = 0
113
+ cadir = settings[:cadir]
114
+ inventory_file_path = File.join(cadir, 'inventory.txt')
115
+
116
+ # Because --revoke has a potentially fatal error it can throw,
117
+ # process it first.
118
+ if args['revoked']
119
+ loader = X509Loader.new(settings[:cacert], settings[:cakey], settings[:cacrl])
120
+ verified_crls = loader.crls.select { |crl| crl.verify(loader.key) }
121
+ unless verified_crls.length == 1
122
+ @logger.err("Could not identify Puppet's CRL. Aborting delete action.")
123
+ return 1
124
+ end
125
+ crl = verified_crls.first
126
+
127
+ # First, search the inventory for the revoked serial.
128
+ # If it matches the current serial for the cert, delete the cert.
129
+ # If it is an old serial for the certname, verify the file on disk
130
+ # is not the serial that was revoked, and then ignore it. If the
131
+ # file on disk does match that serial, delete it.
132
+ # If it isn't in the inventory, fall back to searching every cert on
133
+ # disk for the given serial.
134
+ inventory, err = Inventory.parse_inventory_file(inventory_file_path, @logger)
135
+ revoked_serials = crl.revoked.map { |r| r.serial.to_i }
136
+ to_delete = []
137
+ revoked_serials.each do |revoked_serial|
138
+ current_serial = inventory.find { |k,v| v[:serial] == revoked_serial }
139
+ old_serial = inventory.find { |k,v| v[:old_serials].include?(revoked_serial) }
140
+ if current_serial
141
+ @logger.debug("#{revoked_serial} is the current serial for #{current_serial.first}")
142
+ to_delete << current_serial.first
143
+ elsif old_serial
144
+ @logger.debug("#{revoked_serial} appears to be an old serial for #{old_serial.first}. Verifying cert on disk is not the revoked serial.")
145
+ begin
146
+ serial = get_cert_serial("#{cadir}/signed/#{old_serial.first}.pem")
147
+ # This should never happen unless someone has messed with
148
+ # the inventory.txt file or replaced the cert on disk with
149
+ # an old one.
150
+ to_delete << old_serial.first if serial == revoked_serial
151
+ rescue Exception => e
152
+ @logger.err("Error reading serial from certificate for #{old_serial.first} with exception #{e}")
153
+ errored = true
154
+ end
155
+ else
156
+ @logger.debug("Could not find #{revoked_serial} in inventory.txt. Searching certs on disk for this serial.")
157
+ begin
158
+ certname = find_cert_with_serial(cadir, revoked_serial)
159
+ if certname
160
+ to_delete << certname
161
+ else
162
+ @logger.err("Could not find serial #{revoked_serial} in inventory.txt or in any certificate file currently on disk.")
163
+ errored = true
164
+ end
165
+ rescue Exception => e
166
+ @logger.err("Error reading serial from certificates when trying to find certificate with serial #{revoked_serial} with exception #{e}")
167
+ errored = true
168
+ end
169
+ end
170
+ end
171
+ # Because the CRL will likely contain certs that no longer exist on disk,
172
+ # don't show an error if we can't find the file.
173
+ count, err = delete_certs(cadir, to_delete, false)
174
+ errored ||= err
175
+ deleted_count += count
176
+ end
177
+
178
+ if args['expired']
179
+ # Delete expired certs found in inventory first since this is cheaper.
180
+ # Then, look for any certs not in the inventory, check if they
181
+ # are expired, then delete those.
182
+ inventory, err = Inventory.parse_inventory_file(inventory_file_path, @logger)
183
+ errored ||= err
184
+ expired_in_inventory = inventory.select { |k,v| v[:not_after] < Time.now }.map(&:first)
185
+ # Don't print errors if the cert is not found, since the inventory
186
+ # file can contain old entries that have already been deleted.
187
+ count, err = delete_certs(cadir, expired_in_inventory, false)
188
+ deleted_count += count
189
+ errored ||= err
190
+ other_certs_to_check = find_certs_not_in_inventory(cadir, inventory.map(&:first))
191
+ count, err = delete_expired_certs(cadir, other_certs_to_check)
192
+ deleted_count += count
193
+ errored ||= err
194
+ end
195
+
196
+ if args['certname']
197
+ count, errored = delete_certs(cadir, args['certname'])
198
+ deleted_count += count
199
+ end
200
+
201
+ if args['all']
202
+ certnames = Dir.glob("#{cadir}/signed/*.pem").map{ |c| File.basename(c, '.pem') }
203
+ # Since we don't run this with any other flags, we can set these variables directly
204
+ deleted_count, errored = delete_certs(cadir, certnames)
205
+ end
206
+
207
+ plural = deleted_count == 1 ? "" : "s"
208
+ @logger.inform("#{deleted_count} certificate#{plural} deleted.")
209
+ # If encountered non-fatal errors (an invalid entry in inventory.txt, cert not existing on disk)
210
+ # return 24. Returning 1 should be for fatal errors where we could not do any part of the action.
211
+ return errored ? 24 : 0
212
+ end
213
+
214
+ def find_certs_not_in_inventory(cadir, inventory_certnames)
215
+ all_cert_files = Dir.glob("#{cadir}/signed/*.pem").map { |f| File.basename(f, '.pem') }
216
+ all_cert_files - inventory_certnames
217
+ end
218
+
219
+ def delete_certs(cadir, certnames, error_on_not_found = true)
220
+ deleted = 0
221
+ errored = false
222
+ certnames.each do |cert|
223
+ path = "#{cadir}/signed/#{cert}.pem"
224
+ if File.exist?(path)
225
+ @logger.inform("Deleting certificate at #{path}")
226
+ File.delete(path)
227
+ deleted += 1
228
+ else
229
+ if error_on_not_found
230
+ @logger.err("Could not find certificate file at #{path}")
231
+ errored = true
232
+ end
233
+ end
234
+ end
235
+ [deleted, errored]
236
+ end
237
+
238
+ def delete_expired_certs(cadir, certnames)
239
+ deleted = 0
240
+ errored = false
241
+ files = certnames.map { |c| "#{cadir}/signed/#{c}.pem" }
242
+ files.each do |f|
243
+ # Shouldn't really be possible since we look for certs on disk
244
+ # before calling this function, but just in case.
245
+ unless File.exist?(f)
246
+ @logger.err("Could not find certificate file at #{f}")
247
+ errored = true
248
+ next
249
+ end
250
+ begin
251
+ cert = OpenSSL::X509::Certificate.new(File.read(f))
252
+ rescue OpenSSL::X509::CertificateError
253
+ @logger.err("Error reading certificate at #{f}")
254
+ errored = true
255
+ next
256
+ end
257
+ if cert.not_after < Time.now
258
+ @logger.inform("Deleting certificate at #{f}")
259
+ File.delete(f)
260
+ deleted += 1
261
+ end
262
+ end
263
+ [deleted, errored]
264
+ end
265
+
266
+ def get_cert_serial(file)
267
+ cert = OpenSSL::X509::Certificate.new(File.read(file))
268
+ cert.serial.to_i
269
+ end
270
+
271
+ def find_cert_with_serial(cadir, serial)
272
+ files = Dir.glob("#{cadir}/signed/*.pem")
273
+ files.each do |f|
274
+ begin
275
+ s = get_cert_serial(f)
276
+ return File.basename(f, '.pem') if s == serial # Remove .pem
277
+ rescue Exception => e
278
+ @logger.debug("Error reading certificate at #{f} with exception #{e}. Skipping this file.")
279
+ end
280
+ end
281
+ return nil
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -6,6 +6,7 @@ require 'puppetserver/ca/utils/cli_parsing'
6
6
  require 'puppetserver/ca/utils/file_system'
7
7
  require 'puppetserver/ca/utils/config'
8
8
  require 'puppetserver/ca/x509_loader'
9
+ require 'puppetserver/ca/config/puppet'
9
10
 
10
11
  module Puppetserver
11
12
  module Ca
@@ -13,15 +14,22 @@ module Puppetserver
13
14
  class Prune
14
15
  include Puppetserver::Ca::Utils
15
16
 
16
- SUMMARY = "Prune the local CRL on disk to remove any duplicated certificates"
17
+ SUMMARY = "Prune the local CRL on disk to remove certificate entries"
17
18
  BANNER = <<-BANNER
18
19
  Usage:
19
20
  puppetserver ca prune [--help]
20
21
  puppetserver ca prune [--config]
22
+ puppetserver ca prune [--config] [--remove-duplicates]
23
+ puppetserver ca prune [--config] [--remove-expired]
24
+ puppetserver ca prune [--config] [--remove-entries] [--serial NUMBER[,NUMBER]] [--certname NAME[,NAME]]
21
25
 
22
26
  Description:
23
- Prune the list of revoked certificates of any duplication within it. This command
24
- will only prune the CRL issued by Puppet's CA cert.
27
+ Prune the list of revoked certificates. If no options are provided or
28
+ --remove-duplicates is specified, prune CRL of any duplicate entries.
29
+ If --remove-expired is specified, remove expired entries from CRL.
30
+ If --remove-entries is specified, remove matching entries provided by
31
+ --serial and/or --certname values. This command will only prune the CRL
32
+ issued by Puppet's CA cert.
25
33
 
26
34
  Options:
27
35
  BANNER
@@ -32,6 +40,11 @@ BANNER
32
40
 
33
41
  def run(inputs)
34
42
  config_path = inputs['config']
43
+ remove_duplicates = inputs['remove-duplicates']
44
+ remove_expired = inputs['remove-expired']
45
+ remove_entries = inputs['remove-entries']
46
+ serialnumbers = inputs['serial']
47
+ certnames = inputs['certname']
35
48
  exit_code = 0
36
49
 
37
50
  # Validate the config path.
@@ -45,31 +58,57 @@ BANNER
45
58
  puppet.load(logger: @logger)
46
59
  return 1 if Errors.handle_with_usage(@logger, puppet.errors)
47
60
 
61
+ # Validate arguments
62
+ if (remove_entries && (!serialnumbers && !certnames))
63
+ return 1 if Errors.handle_with_usage(@logger,["--remove-entries option require --serial or --certname values"])
64
+ end
65
+
48
66
  # Validate that we are offline
49
67
  return 1 if HttpClient.check_server_online(puppet.settings, @logger)
50
68
 
51
69
  # Getting the CRL(s)
52
70
  loader = X509Loader.new(puppet.settings[:cacert], puppet.settings[:cakey], puppet.settings[:cacrl])
71
+ inventory_file = puppet.settings[:cert_inventory]
72
+ cadir = puppet.settings[:cadir]
53
73
 
54
74
  verified_crls = loader.crls.select { |crl| crl.verify(loader.key) }
75
+ number_of_removed_duplicates = 0
76
+ number_of_removed_crl_entries = 0
55
77
 
56
78
  if verified_crls.length == 1
57
79
  puppet_crl = verified_crls.first
58
80
  @logger.inform("Total number of certificates found in Puppet's CRL is: #{puppet_crl.revoked.length}.")
59
- number_of_removed_duplicates = prune_CRL(puppet_crl)
60
81
 
61
- if number_of_removed_duplicates > 0
82
+ if remove_entries
83
+ if serialnumbers
84
+ number_of_removed_crl_entries += prune_using_serial(puppet_crl, loader.key, serialnumbers)
85
+ end
86
+ if certnames
87
+ number_of_removed_crl_entries += prune_using_certname(puppet_crl, loader.key, inventory_file, cadir, certnames)
88
+ end
89
+ end
90
+
91
+ if remove_expired
92
+ number_of_removed_crl_entries += prune_expired(puppet_crl, loader.key, inventory_file, cadir)
93
+ end
94
+
95
+ if (remove_duplicates || (!remove_entries && !remove_expired))
96
+ number_of_removed_duplicates += prune_CRL(puppet_crl)
97
+ end
98
+
99
+
100
+ if (number_of_removed_duplicates > 0 || number_of_removed_crl_entries > 0)
62
101
  update_pruned_CRL(puppet_crl, loader.key)
63
102
  FileSystem.write_file(puppet.settings[:cacrl], loader.crls, 0644)
64
- @logger.inform("Removed #{number_of_removed_duplicates} duplicated certs from Puppet's CRL.")
103
+ @logger.inform("Removed #{number_of_removed_duplicates} duplicated certs from Puppet's CRL.") if number_of_removed_duplicates > 0
104
+ @logger.inform("Removed #{number_of_removed_crl_entries} certs from Puppet's CRL.") if number_of_removed_crl_entries > 0
65
105
  else
66
- @logger.inform("No duplicate revocations found in the CRL.")
106
+ @logger.inform("No matching revocations found in the CRL for pruning")
67
107
  end
68
108
  else
69
109
  @logger.err("Could not identify Puppet's CRL. Aborting prune action.")
70
110
  exit_code = 1
71
111
  end
72
-
73
112
  return exit_code
74
113
  end
75
114
 
@@ -106,6 +145,89 @@ BANNER
106
145
  crl.sign(pkey, OpenSSL::Digest::SHA256.new)
107
146
  end
108
147
 
148
+ def prune_using_serial(crl, key, serialnumbers)
149
+ removed_serials = []
150
+ revoked_list = crl.revoked
151
+ @logger.debug("Removing entries in CRL for issuer " \
152
+ "#{crl.issuer.to_s(OpenSSL::X509::Name::RFC2253)}") if @logger.debug?
153
+ serialnumbers.each do |serial|
154
+ if serial.match(/^(?:0[xX])?[A-Fa-f0-9]+$/)
155
+ revoked_list.delete_if do |revoked|
156
+ if revoked.serial == OpenSSL::BN.new(serial.hex)
157
+ removed_serials.push(serial)
158
+ true
159
+ end
160
+ end
161
+ end
162
+ end
163
+ crl.revoked = (revoked_list)
164
+ @logger.debug("Removed these CRL entries : #{removed_serials}") if @logger.debug?
165
+ return removed_serials.length
166
+ end
167
+
168
+ def prune_using_certname(crl, key, inventory_file, cadir, certnames)
169
+ serialnumbers = []
170
+ @logger.debug("Checking inventory file #{inventory_file} for matching cert names") if @logger.debug?
171
+ errors = FileSystem.validate_file_paths(inventory_file)
172
+ if errors.empty?
173
+ File.open(inventory_file).each_line do |line|
174
+ certnames.each do |certname|
175
+ if line.match(/\/CN=#{certname}$/) && line.split.length == 4
176
+ serialnumbers.push(line.split.first)
177
+ certnames.delete(certname)
178
+ end
179
+ end
180
+ end
181
+ else
182
+ @logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
183
+ end
184
+ if certnames
185
+ @logger.debug("Checking CA dir #{cadir} for matching cert names")
186
+ certnames.each do |certname|
187
+ cert_file = "#{cadir}/signed/#{certname}.pem"
188
+ if File.file?(cert_file)
189
+ raw = File.read(cert_file)
190
+ certificate = OpenSSL::X509::Certificate.new(raw)
191
+ serial = certificate.serial
192
+ serialnumbers.push(serial.to_s(16))
193
+ end
194
+ end
195
+ prune_using_serial(crl, key, serialnumbers)
196
+ end
197
+ end
198
+
199
+ def prune_expired (crl, key, inventory_file, cadir)
200
+ serialnumbers = []
201
+ signed_dir = "#{cadir}/signed"
202
+ @logger.debug("Checking inventory file #{inventory_file} for expired entries") if @logger.debug?
203
+ errors = FileSystem.validate_file_paths(inventory_file)
204
+ if errors.empty?
205
+ File.open(inventory_file).each_line do |line|
206
+ if line.match(/\/CN=.*$/) && line.split.length == 4
207
+ not_after = line.split[2]
208
+ begin
209
+ not_after = Time.parse(line.split[2])
210
+ serialnumbers.push(line.split.first) if not_after < Time.now
211
+ rescue ArgumentError
212
+ @logger.warn "Invalid not_after time found in inventory.txt file at #{line}"
213
+ next
214
+ end
215
+ end
216
+ end
217
+ else
218
+ @logger.warn "Reading inventory file at #{inventory_file} failed with error #{errors}"
219
+ end
220
+ @logger.debug("Checking CA dir #{cadir} for expired certs")
221
+ Dir.foreach(signed_dir) do |filename|
222
+ if File.extname(filename) == '.pem'
223
+ raw = File.read("#{signed_dir}/#{filename}")
224
+ certificate = OpenSSL::X509::Certificate.new(raw)
225
+ serialnumbers.push(certificate.serial.to_s(16)) if certificate.not_after < Time.now
226
+ end
227
+ end
228
+ prune_using_serial(crl, key, serialnumbers)
229
+ end
230
+
109
231
  def self.parser(parsed = {})
110
232
  OptionParser.new do |opts|
111
233
  opts.banner = BANNER
@@ -115,6 +237,21 @@ BANNER
115
237
  opts.on('--config CONF', 'Path to the puppet.conf file on disk') do |conf|
116
238
  parsed['config'] = conf
117
239
  end
240
+ opts.on('--remove-duplicates', 'Remove duplicate entries from CRL(default)') do |remove_duplicates|
241
+ parsed['remove-duplicates'] = true
242
+ end
243
+ opts.on('--remove-expired', 'Remove expired entries from CRL') do |remove_expired|
244
+ parsed['remove-expired'] = true
245
+ end
246
+ opts.on('--remove-entries', 'Remove entries from CRL') do |remove_entries|
247
+ parsed['remove-entries'] = true
248
+ end
249
+ opts.on('--serial NUMBER[,NUMBER]', Array, 'Serial numbers(s) in HEX to be removed from CRL') do |serialnumbers|
250
+ parsed['serial'] = serialnumbers
251
+ end
252
+ opts.on('--certname NAME[,NAME]', Array, 'Name(s) of the cert(s) to be removed from CRL') do |certnames|
253
+ parsed['certname'] = certnames
254
+ end
118
255
  end
119
256
  end
120
257
 
@@ -134,4 +271,4 @@ BANNER
134
271
  end
135
272
  end
136
273
  end
137
- end
274
+ end
@@ -1,6 +1,7 @@
1
1
  require 'optparse'
2
2
 
3
3
  require 'puppetserver/ca/action/clean'
4
+ require 'puppetserver/ca/action/delete'
4
5
  require 'puppetserver/ca/action/generate'
5
6
  require 'puppetserver/ca/action/import'
6
7
  require 'puppetserver/ca/action/enable'
@@ -27,6 +28,7 @@ Puppet Server's built-in Certificate Authority
27
28
  BANNER
28
29
 
29
30
  ADMIN_ACTIONS = {
31
+ 'delete' => Action::Delete,
30
32
  'import' => Action::Import,
31
33
  'setup' => Action::Setup,
32
34
  'enable' => Action::Enable,
@@ -10,17 +10,12 @@ module Puppetserver
10
10
  # Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs
11
11
  class HttpClient
12
12
 
13
- DEFAULT_HEADERS = {
14
- 'User-Agent' => 'PuppetserverCaCli',
15
- 'Content-Type' => 'application/json',
16
- 'Accept' => 'application/json'
17
- }
18
-
19
13
  attr_reader :store
20
14
 
21
15
  # Not all connections require a client cert to be present.
22
16
  # For example, when querying the status endpoint.
23
17
  def initialize(logger, settings, with_client_cert: true)
18
+ @default_headers = make_headers(ENV['HOME'])
24
19
  @logger = logger
25
20
  @store = make_store(settings[:localcacert],
26
21
  settings[:certificate_revocation],
@@ -52,7 +47,7 @@ module Puppetserver
52
47
  # The Connection object should have HTTP verbs defined on it that take
53
48
  # a body (and optional overrides). Returns whatever the block given returned.
54
49
  def with_connection(url, &block)
55
- request = ->(conn) { block.call(Connection.new(conn, url, @logger)) }
50
+ request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }
56
51
 
57
52
  begin
58
53
  Net::HTTP.start(url.host, url.port,
@@ -68,7 +63,22 @@ module Puppetserver
68
63
 
69
64
  private
70
65
 
71
- def load_with_errors(path, setting, &block)
66
+ def make_headers(home)
67
+ headers = {
68
+ 'User-Agent' => 'PuppetserverCaCli',
69
+ 'Content-Type' => 'application/json',
70
+ 'Accept' => 'application/json'
71
+ }
72
+
73
+ token_path = "#{home}/.puppetlabs/token"
74
+ if File.exist?(token_path)
75
+ headers['X-Authentication'] = File.read(token_path)
76
+ end
77
+
78
+ headers
79
+ end
80
+
81
+ def load_with_errors(path, setting, &block)
72
82
  begin
73
83
  content = File.read(path)
74
84
  block.call(content)
@@ -81,21 +91,23 @@ module Puppetserver
81
91
  "Could not parse '#{setting}' at '#{path}'.\n" +
82
92
  " OpenSSL returned: #{e.message}")
83
93
  end
84
- end
94
+ end
85
95
 
86
96
  # Helper class that wraps a Net::HTTP connection, a HttpClient::URL
87
97
  # and defines methods named after HTTP verbs that are called on the
88
98
  # saved connection, returning a Result.
89
99
  class Connection
90
- def initialize(net_http_connection, url_struct, logger)
100
+
101
+ def initialize(net_http_connection, url_struct, logger, default_headers)
91
102
  @conn = net_http_connection
92
103
  @url = url_struct
93
104
  @logger = logger
105
+ @default_headers = default_headers
94
106
  end
95
107
 
96
- def get(url_overide = nil, headers = {})
108
+ def get(url_overide = nil, header_overrides = {})
97
109
  url = url_overide || @url
98
- headers = DEFAULT_HEADERS.merge(headers)
110
+ headers = @default_headers.merge(header_overrides)
99
111
 
100
112
  @logger.debug("Making a GET request at #{url.full_url}")
101
113
 
@@ -105,9 +117,9 @@ module Puppetserver
105
117
 
106
118
  end
107
119
 
108
- def put(body, url_override = nil, headers = {})
120
+ def put(body, url_override = nil, header_overrides = {})
109
121
  url = url_override || @url
110
- headers = DEFAULT_HEADERS.merge(headers)
122
+ headers = @default_headers.merge(header_overrides)
111
123
 
112
124
  @logger.debug("Making a PUT request at #{url.full_url}")
113
125
 
@@ -118,9 +130,9 @@ module Puppetserver
118
130
  Result.new(result.code, result.body)
119
131
  end
120
132
 
121
- def delete(url_override = nil, headers = {})
133
+ def delete(url_override = nil, header_overrides = {})
122
134
  url = url_override || @url
123
- headers = DEFAULT_HEADERS.merge(headers)
135
+ headers = @default_headers.merge(header_overrides)
124
136
 
125
137
  @logger.debug("Making a DELETE request at #{url.full_url}")
126
138
 
@@ -0,0 +1,84 @@
1
+ require 'time'
2
+
3
+ module Puppetserver
4
+ module Ca
5
+ module Utils
6
+ module Inventory
7
+
8
+ # Note that the inventory file may have multiple entries for the same certname,
9
+ # so it should only provide the latest cert for the given certname.
10
+ def self.parse_inventory_file(path, logger)
11
+ unless File.exist?(path)
12
+ logger.err("Could not find inventory at #{path}")
13
+ return [{}, true]
14
+ end
15
+ inventory = {}
16
+ errored = false
17
+ File.readlines(path).each do |line|
18
+ # Shouldn't be any blank lines, but skip them if there are
19
+ next if line.strip.empty?
20
+
21
+ items = line.strip.split
22
+ if items.count != 4
23
+ logger.err("Invalid entry found in inventory.txt: #{line}")
24
+ errored = true
25
+ next
26
+ end
27
+ unless items[0].match(/^(?:0x)?[A-Fa-f0-9]+$/)
28
+ logger.err("Invalid serial found in inventory.txt line: #{line}")
29
+ errored = true
30
+ next
31
+ end
32
+ serial = items[0].hex
33
+ not_before = nil
34
+ not_after = nil
35
+ begin
36
+ not_before = Time.parse(items[1])
37
+ rescue ArgumentError
38
+ logger.err("Invalid not_before time found in inventory.txt line: #{line}")
39
+ errored = true
40
+ next
41
+ end
42
+ begin
43
+ not_after = Time.parse(items[2])
44
+ rescue ArgumentError
45
+ logger.err("Invalid not_after time found in inventory.txt line: #{line}")
46
+ errored = true
47
+ next
48
+ end
49
+ unless items[3].start_with?('/CN=')
50
+ logger.err("Invalid certname found in inventory.txt line: #{line}")
51
+ errored = true
52
+ next
53
+ end
54
+ certname = items[3][4..-1]
55
+
56
+ if !inventory.keys.include?(certname)
57
+ inventory[certname] = {
58
+ :serial => serial,
59
+ :old_serials => [],
60
+ :not_before => not_before,
61
+ :not_after => not_after,
62
+ }
63
+ else
64
+ if not_after >= inventory[certname][:not_after]
65
+ # This is a newer cert than the one we currently have recorded,
66
+ # so save the previous serial in :old_serials
67
+ inventory[certname][:old_serials] << inventory[certname][:serial]
68
+ inventory[certname][:serial] = serial
69
+ inventory[certname][:not_before] = not_before
70
+ inventory[certname][:not_after] = not_after
71
+ else
72
+ # This somehow is an older cert (shouldn't really be possible as we just append
73
+ # to the file with each new cert and we are reading it order)
74
+ inventory[certname][:old_serials] << serial
75
+ end
76
+ end
77
+ end
78
+ [inventory, errored]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -1,5 +1,5 @@
1
1
  module Puppetserver
2
2
  module Ca
3
- VERSION = "2.4.0"
3
+ VERSION = "2.6.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppetserver-ca
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-01-17 00:00:00.000000000 Z
11
+ date: 2023-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: facter
@@ -80,7 +80,7 @@ executables:
80
80
  extensions: []
81
81
  extra_rdoc_files: []
82
82
  files:
83
- - ".github/workflows/snyk.yaml"
83
+ - ".github/workflows/mend.yaml"
84
84
  - ".gitignore"
85
85
  - ".rspec"
86
86
  - ".travis.yml"
@@ -96,6 +96,7 @@ files:
96
96
  - exe/puppetserver-ca
97
97
  - lib/puppetserver/ca.rb
98
98
  - lib/puppetserver/ca/action/clean.rb
99
+ - lib/puppetserver/ca/action/delete.rb
99
100
  - lib/puppetserver/ca/action/enable.rb
100
101
  - lib/puppetserver/ca/action/generate.rb
101
102
  - lib/puppetserver/ca/action/import.rb
@@ -118,6 +119,7 @@ files:
118
119
  - lib/puppetserver/ca/utils/config.rb
119
120
  - lib/puppetserver/ca/utils/file_system.rb
120
121
  - lib/puppetserver/ca/utils/http_client.rb
122
+ - lib/puppetserver/ca/utils/inventory.rb
121
123
  - lib/puppetserver/ca/utils/signing_digest.rb
122
124
  - lib/puppetserver/ca/version.rb
123
125
  - lib/puppetserver/ca/x509_loader.rb
@@ -141,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
143
  - !ruby/object:Gem::Version
142
144
  version: '0'
143
145
  requirements: []
144
- rubygems_version: 3.0.9
146
+ rubygems_version: 3.4.12
145
147
  signing_key:
146
148
  specification_version: 4
147
149
  summary: A simple CLI tool for interacting with Puppet Server's Certificate Authority
@@ -1,31 +0,0 @@
1
- ---
2
- name: Snyk Monitor
3
- on:
4
- push:
5
- branches:
6
- - main
7
- jobs:
8
- snyk_monitor:
9
- if: ${{ github.repository_owner == 'puppetlabs' }}
10
- runs-on: ubuntu-latest
11
- name: Snyk Monitor
12
- steps:
13
- - name: Checkout current PR
14
- uses: actions/checkout@v2
15
- - name: Setup Ruby
16
- uses: ruby/setup-ruby@v1
17
- with:
18
- ruby-version: 2.7
19
- - name: Install dependencies
20
- run: bundle install --jobs 3 --retry 3
21
- - name: Extract branch name
22
- shell: bash
23
- run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
24
- id: extract_branch
25
- - name: Run Snyk to check for vulnerabilities
26
- uses: snyk/actions/ruby@master
27
- env:
28
- SNYK_TOKEN: ${{ secrets.SNYK_FOSS_KEY }}
29
- with:
30
- command: monitor
31
- args: --org=puppet-foss --project-name=${{ github.repository }}#${{ steps.extract_branch.outputs.branch }}