puppetserver-ca 2.4.0 → 2.6.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
  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 }}