pan-chainguard 0.0.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ pan-chainguard is distributed with an ISC license
2
+ (https://cvsweb.openbsd.org/src/share/misc/license.template?rev=HEAD):
3
+
4
+ Copyright (c) 2024 Palo Alto Networks, Inc.
5
+
6
+ Permission to use, copy, modify, and distribute this software for any
7
+ purpose with or without fee is hereby granted, provided that the above
8
+ copyright notice and this permission notice appear in all copies.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.1
2
+ Name: pan-chainguard
3
+ Version: 0.0.0
4
+ Summary: Preload Trusted CA Intermediate Certificate Chains on PAN-OS
5
+ Home-page: https://github.com/PaloAltoNetworks/pan-chainguard
6
+ Author: Palo Alto Networks, Inc.
7
+ Author-email: devrel@paloaltonetworks.com
8
+ License: ISC
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/x-rst
13
+ License-File: LICENSE.txt
14
+ Requires-Dist: aiohttp>=3.9.0
15
+ Requires-Dist: pan-python>=0.25.0
16
+
17
+ pan-chainguard - Preload Trusted CA Intermediate Certificate Chains on PAN-OS
18
+ =============================================================================
19
+
20
+ ``pan-chainguard`` is a Python3 application which uses
21
+ `CCADB data
22
+ <https://www.ccadb.org/resources>`_
23
+ to derive intermediate certificate chains for trusted
24
+ certificate authorities in PAN-OS so they can be
25
+ `preloaded
26
+ <https://wiki.mozilla.org/Security/CryptoEngineering/Intermediate_Preloading>`_
27
+ as device certificates.
28
+
29
+ Problem
30
+ -------
31
+
32
+ Many TLS enabled origin servers suffer from a misconfiguration in
33
+ which they:
34
+
35
+ #. Do not return intermediate CA certificates.
36
+ #. Return certificates out of order.
37
+ #. Return intermediate certificates which are not related to the CA
38
+ which signed the server certificate.
39
+
40
+ The impact for PAN-OS SSL decryption administrators is end users will
41
+ see errors such as *unable to get local issuer certificate* until the
42
+ sites that are misconfigured are
43
+ `identified
44
+ <https://docs.paloaltonetworks.com/pan-os/11-1/pan-os-admin/decryption/troubleshoot-and-monitor-decryption/decryption-logs/repair-incomplete-certificate-chains>`_,
45
+ the required intermediate certificates are obtained, and the
46
+ certificates are imported into PAN-OS.
47
+
48
+ Solution: Intermediate CA Preloading
49
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
50
+
51
+ ``pan-chainguard`` uses the PAN-OS default trusted CA store and the
52
+ *All Certificate Information* CCADB data file as input, and determines
53
+ the intermediate certificate chains, if available, for each root CA
54
+ certificate. These can then be added to PAN-OS as trusted CA device
55
+ certificates.
56
+
57
+ By preloading known intermediates for the trusted CAs, the number of
58
+ TLS connection errors that users encounter for misconfigured servers
59
+ can be reduced, without reactive actions by an administrator.
60
+
61
+ Documentation
62
+ -------------
63
+
64
+ - Administrator's Guide:
65
+
66
+ https://github.com/PaloAltoNetworks/pan-chainguard/blob/main/doc/admin-guide.rst
67
+
68
+ Install ``pan-chainguard``
69
+ --------------------------
70
+
71
+ ``pan-chainguard`` is available as a
72
+ `release
73
+ <https://github.com/PaloAltoNetworks/pan-chainguard/releases/>`_
74
+ on GitHub and as a
75
+ `package
76
+ <https://pypi.org/project/pan-chainguard/>`_
77
+ on PyPi.
@@ -0,0 +1,61 @@
1
+ pan-chainguard - Preload Trusted CA Intermediate Certificate Chains on PAN-OS
2
+ =============================================================================
3
+
4
+ ``pan-chainguard`` is a Python3 application which uses
5
+ `CCADB data
6
+ <https://www.ccadb.org/resources>`_
7
+ to derive intermediate certificate chains for trusted
8
+ certificate authorities in PAN-OS so they can be
9
+ `preloaded
10
+ <https://wiki.mozilla.org/Security/CryptoEngineering/Intermediate_Preloading>`_
11
+ as device certificates.
12
+
13
+ Problem
14
+ -------
15
+
16
+ Many TLS enabled origin servers suffer from a misconfiguration in
17
+ which they:
18
+
19
+ #. Do not return intermediate CA certificates.
20
+ #. Return certificates out of order.
21
+ #. Return intermediate certificates which are not related to the CA
22
+ which signed the server certificate.
23
+
24
+ The impact for PAN-OS SSL decryption administrators is end users will
25
+ see errors such as *unable to get local issuer certificate* until the
26
+ sites that are misconfigured are
27
+ `identified
28
+ <https://docs.paloaltonetworks.com/pan-os/11-1/pan-os-admin/decryption/troubleshoot-and-monitor-decryption/decryption-logs/repair-incomplete-certificate-chains>`_,
29
+ the required intermediate certificates are obtained, and the
30
+ certificates are imported into PAN-OS.
31
+
32
+ Solution: Intermediate CA Preloading
33
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
34
+
35
+ ``pan-chainguard`` uses the PAN-OS default trusted CA store and the
36
+ *All Certificate Information* CCADB data file as input, and determines
37
+ the intermediate certificate chains, if available, for each root CA
38
+ certificate. These can then be added to PAN-OS as trusted CA device
39
+ certificates.
40
+
41
+ By preloading known intermediates for the trusted CAs, the number of
42
+ TLS connection errors that users encounter for misconfigured servers
43
+ can be reduced, without reactive actions by an administrator.
44
+
45
+ Documentation
46
+ -------------
47
+
48
+ - Administrator's Guide:
49
+
50
+ https://github.com/PaloAltoNetworks/pan-chainguard/blob/main/doc/admin-guide.rst
51
+
52
+ Install ``pan-chainguard``
53
+ --------------------------
54
+
55
+ ``pan-chainguard`` is available as a
56
+ `release
57
+ <https://github.com/PaloAltoNetworks/pan-chainguard/releases/>`_
58
+ on GitHub and as a
59
+ `package
60
+ <https://pypi.org/project/pan-chainguard/>`_
61
+ on PyPi.
@@ -0,0 +1,29 @@
1
+ #!/bin/sh
2
+
3
+ usage() {
4
+ echo "usage: $(basename $0) cert-directory"
5
+ exit 1
6
+ }
7
+
8
+ # Usage: fingerprints directory
9
+ fingerprints() {
10
+ dir=$1
11
+
12
+ echo '"filename","sha256"'
13
+ for file in $(ls $dir/*.cer); do
14
+ fp=$(openssl x509 -noout -fingerprint -sha256 -in $file)
15
+ fp=$(echo $fp | sed -e 's/.*=//')
16
+ fp=$(echo $fp | sed -e 's/://g')
17
+ echo \"$(basename $file)\",\"$fp\"
18
+ done
19
+ }
20
+
21
+ if [ $# != 1 ] || [ $1 = '--help' ]; then
22
+ usage
23
+ fi
24
+ if [ ! -d $1 ]; then
25
+ echo "$1: not directory"
26
+ exit 1
27
+ fi
28
+
29
+ fingerprints $1
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env python3
2
+
3
+ #
4
+ # Copyright (c) 2024 Palo Alto Networks, Inc.
5
+ #
6
+ # Permission to use, copy, modify, and distribute this software for any
7
+ # purpose with or without fee is hereby granted, provided that the above
8
+ # copyright notice and this permission notice appear in all copies.
9
+ #
10
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
+ #
18
+
19
+ import aiohttp
20
+ import argparse
21
+ import asyncio
22
+ from collections import defaultdict
23
+ import csv
24
+ import datetime
25
+ import io
26
+ import os
27
+ import pprint
28
+ import sys
29
+ import tarfile
30
+ import time
31
+
32
+ libpath = os.path.dirname(os.path.abspath(__file__))
33
+ sys.path[:0] = [os.path.join(libpath, os.pardir)]
34
+
35
+ from pan_chainguard import (title, __version__)
36
+ from pan_chainguard.crtsh import (ArgsError, CrtShApi)
37
+
38
+ ROOT = 'root'
39
+ INTERMEDIATE = 'intermediate'
40
+ MAX_TASKS = 3 # concurrent crt.sh API requests
41
+ CRT_SH_TIMEOUT = 60
42
+
43
+ args = None
44
+
45
+
46
+ def main():
47
+ global args
48
+ args = parse_args()
49
+
50
+ asyncio.run(main_loop())
51
+
52
+ sys.exit(0)
53
+
54
+
55
+ async def main_loop():
56
+ create_archive(test=True)
57
+
58
+ invalid, roots, intermediates, parents = get_certs()
59
+ if args.verbose:
60
+ print('roots %d, intermediates %d, parents %d' %
61
+ (len(roots), len(intermediates), len(parents)))
62
+
63
+ chains = get_cert_chains(roots, intermediates, parents)
64
+ if args.verbose:
65
+ print('chains %d (root=>intermediates)' % len(chains),
66
+ pprint.pformat(chains),
67
+ file=sys.stderr)
68
+
69
+ total, total_invalid, panos = await get_panos_intermediates(
70
+ chains, invalid)
71
+ print('%d intermediate chains found for %d PAN-OS trusted CAs' % (
72
+ len(panos), total))
73
+ if args.verbose:
74
+ print('PAN-OS intermediate chains', pprint.pformat(panos),
75
+ file=sys.stderr)
76
+
77
+ errors, certificates = await get_cert_files(panos)
78
+ if errors:
79
+ print('Warning: %d certificates were not downloaded')
80
+
81
+ if False and args.verbose:
82
+ print('PAN-OS certificates', pprint.pformat(certificates),
83
+ file=sys.stderr)
84
+
85
+ create_archive(certificates)
86
+
87
+
88
+ def get_certs():
89
+ invalid = {}
90
+ roots = []
91
+ intermediates = []
92
+ parents = defaultdict(list)
93
+ certs = {}
94
+ duplicates = defaultdict(list)
95
+
96
+ now = datetime.datetime.now(datetime.timezone.utc)
97
+
98
+ try:
99
+ with open(args.ccadb, 'r', newline='') as csvfile:
100
+ reader = csv.DictReader(csvfile,
101
+ dialect='unix')
102
+ for row in reader:
103
+ sha256 = row['SHA-256 Fingerprint']
104
+
105
+ if row['Revocation Status'] not in ['', 'Not Revoked']:
106
+ x = '%s %s %s' % (row['Revocation Status'],
107
+ sha256,
108
+ row['Certificate Name'])
109
+ if args.verbose:
110
+ print(x, file=sys.stderr)
111
+ invalid[sha256] = x
112
+ continue
113
+
114
+ fmt = '%Y.%m.%d %z'
115
+ valid_from = datetime.datetime.strptime(
116
+ row['Valid From (GMT)'] + ' +0000', fmt)
117
+ valid_to = datetime.datetime.strptime(
118
+ row['Valid To (GMT)'] + ' +0000', fmt)
119
+ if now < valid_from:
120
+ x = 'Not yet valid (valid from %s) %s %s' % (
121
+ row['Valid From (GMT)'],
122
+ sha256,
123
+ row['Certificate Name'])
124
+ if args.verbose:
125
+ print(x, file=sys.stderr)
126
+ invalid[sha256] = x
127
+ continue
128
+
129
+ if valid_to < now:
130
+ x = 'Expired (valid to %s) %s %s' % (
131
+ row['Valid To (GMT)'],
132
+ sha256,
133
+ row['Certificate Name'])
134
+ if args.verbose:
135
+ print(x, file=sys.stderr)
136
+ invalid[sha256] = x
137
+ continue
138
+
139
+ derived_trust_bits = row['Derived Trust Bits']
140
+ if (derived_trust_bits and 'Server Authentication' not in
141
+ derived_trust_bits.split(';')):
142
+ x = 'Missing Server Authentication %s' % sha256
143
+ invalid[sha256] = x
144
+ continue
145
+
146
+ cert_type = row['Certificate Record Type']
147
+
148
+ if sha256 in certs:
149
+ if sha256 not in duplicates:
150
+ duplicates[sha256].append(certs[sha256])
151
+ duplicates[sha256].append(row)
152
+ continue
153
+
154
+ certs[sha256] = row
155
+
156
+ if cert_type == 'Root Certificate':
157
+ roots.append(sha256)
158
+
159
+ if cert_type == 'Intermediate Certificate':
160
+ parent_sha256 = row['Parent SHA-256 Fingerprint']
161
+ if not parent_sha256:
162
+ x = 'Intermediate with no parent: %s' % sha256
163
+ if args.verbose:
164
+ print(x, file=sys.stderr)
165
+ invalid[sha256] = x
166
+ continue
167
+
168
+ intermediates.append(sha256)
169
+ parents[parent_sha256].append(sha256)
170
+
171
+ except OSError as e:
172
+ print('%s: %s' % (args.ccadb, e), file=sys.stderr)
173
+ sys.exit(1)
174
+
175
+ if args.verbose:
176
+ for sha256 in duplicates:
177
+ print('Duplicate certificates %s' % sha256, file=sys.stderr)
178
+ for x in duplicates[sha256]:
179
+ print(' %s %s %s' % (x['Certificate Name'],
180
+ x['Certificate Record Type'],
181
+ x['Salesforce Record ID']),
182
+ file=sys.stderr)
183
+
184
+ return invalid, roots, intermediates, parents
185
+
186
+
187
+ def get_cert_chains(roots, intermediates, parents):
188
+ chains = {}
189
+
190
+ for k in roots:
191
+ if k not in parents:
192
+ if args.verbose:
193
+ print('Root with no child %s' % k,
194
+ file=sys.stderr)
195
+ continue
196
+
197
+ for child in parents[k]:
198
+ chain = [k]
199
+ follow(chain, child, parents)
200
+ if args.verbose:
201
+ print('chain[%d]:' % len(chain),
202
+ pprint.pformat(chain), file=sys.stderr)
203
+
204
+ chains[chain[0]] = chain
205
+
206
+ return chains
207
+
208
+
209
+ def follow(chain, k, parents):
210
+ if args.verbose:
211
+ print('follow[%d]:' % len(chain), k, file=sys.stderr)
212
+
213
+ chain.append(k)
214
+ if k in parents:
215
+ for child in parents[k]:
216
+ follow(chain, child, parents)
217
+
218
+
219
+ async def get_panos_intermediates(chains, invalid):
220
+ intermediates = {}
221
+ total = 0
222
+ total_invalid = 0
223
+
224
+ if args.fingerprints:
225
+ try:
226
+ with open(args.fingerprints, 'r', newline='') as csvfile:
227
+ reader = csv.DictReader(csvfile,
228
+ dialect='unix')
229
+ for row in reader:
230
+ sha256 = row['sha256']
231
+ if sha256 in invalid:
232
+ print('Invalid PAN-OS certificate %s: %s' % (
233
+ row['filename'],
234
+ invalid[sha256]))
235
+ total_invalid += 1
236
+ continue
237
+
238
+ total += 1
239
+ if sha256 in chains:
240
+ intermediates[row['filename']] = [(ROOT, sha256)]
241
+ for x in chains[sha256][1:]:
242
+ intermediates[row['filename']].append(
243
+ (INTERMEDIATE, x))
244
+
245
+ except OSError as e:
246
+ print('%s: %s' % (args.ccadb, e), file=sys.stderr)
247
+ sys.exit(1)
248
+
249
+ return total, total_invalid, intermediates
250
+
251
+
252
+ async def download(api, sequence, sha256):
253
+ tries = 0
254
+ MAX_TRIES = 5
255
+ RETRY_SLEEP = 5.0
256
+
257
+ while True:
258
+ tries += 1
259
+ try:
260
+ resp = await api.download(id=sha256[1])
261
+
262
+ if resp.status == 429:
263
+ x = '%d %s' % (resp.status, resp.reason)
264
+ if tries == MAX_TRIES:
265
+ print('no retry after try %d, %s' % (tries, x),
266
+ file=sys.stderr)
267
+ break
268
+ print('retry after try %d, %s' % (tries, x),
269
+ file=sys.stderr)
270
+ await asyncio.sleep(RETRY_SLEEP)
271
+ elif resp.status != 200:
272
+ x = 'download %d %s %s %s' % (
273
+ resp.status, resp.reason, sequence, sha256[1])
274
+ break
275
+ else:
276
+ filename, content = await api.content(resp=resp)
277
+ if filename is None:
278
+ filename = sha256[1] + '.crt'
279
+
280
+ start = '-----BEGIN CERTIFICATE-----'
281
+ end = '-----END CERTIFICATE-----\n'
282
+ if (content.startswith(start) and
283
+ content.endswith(end)):
284
+ return filename, content, sequence, sha256[0]
285
+ else:
286
+ # XXX ephemeral?
287
+ x = 'content malformed %s %s %s' % (
288
+ sequence, sha256[1], content)
289
+ if tries == MAX_TRIES:
290
+ print('no retry after try %d, %s' % (tries, x),
291
+ file=sys.stderr)
292
+ break
293
+ print('retry after try %d, %s' % (tries, x),
294
+ file=sys.stderr)
295
+ await asyncio.sleep(RETRY_SLEEP)
296
+
297
+ except (asyncio.TimeoutError,
298
+ asyncio.CancelledError, # XXX
299
+ aiohttp.ClientError) as e:
300
+ msg = e if str(e) else type(e).__name__
301
+ x = 'CrtShApi: %s: %s %s' % (msg, sequence, sha256[1])
302
+ if tries == MAX_TRIES:
303
+ print('no retry after try %d, %s' % (tries, x),
304
+ file=sys.stderr)
305
+ break
306
+ print('retry after try %d, %s' % (tries, x),
307
+ file=sys.stderr)
308
+ # no sleep
309
+
310
+ except ArgsError as e:
311
+ x = 'CrtShApi: %s' % e
312
+ break
313
+
314
+ return None, x, None, None
315
+
316
+
317
+ async def get_cert_files(panos):
318
+ try:
319
+ api = CrtShApi(timeout=CRT_SH_TIMEOUT)
320
+ except ArgsError as e:
321
+ print('CrtShApi: %s' % e, file=sys.stderr)
322
+ sys.exit(1)
323
+ else:
324
+ errors, certificates = await process_roots(api, panos)
325
+ finally:
326
+ await api.session.close()
327
+
328
+ return errors, certificates
329
+
330
+
331
+ async def process_roots(api, roots):
332
+ certificates = defaultdict(list)
333
+ tasks = []
334
+ start = time.time()
335
+ total = 0
336
+ errors = 0
337
+
338
+ for sequence in roots:
339
+ sequence_friendly = sequence
340
+ if sequence.endswith('.cer'):
341
+ sequence_friendly = sequence[:-4]
342
+ print('download CA %s intermediates %d' % (sequence_friendly,
343
+ len(roots[sequence][1:])),
344
+ flush=True)
345
+
346
+ for sha256 in roots[sequence]:
347
+ if not args.roots and sha256[0] == ROOT:
348
+ continue
349
+ total += 1
350
+ tasks.append(download(api, sequence_friendly, sha256))
351
+
352
+ if len(tasks) == MAX_TASKS:
353
+ errors += await run_tasks(tasks, certificates)
354
+ tasks = []
355
+
356
+ if len(tasks):
357
+ errors += await run_tasks(tasks, certificates)
358
+
359
+ if args.verbose:
360
+ end = time.time()
361
+ elapsed = end - start
362
+ mins = elapsed // 60
363
+ secs = elapsed % 60
364
+ avg = elapsed / total if total > 0 else 0
365
+ print('%d tasks, elapsed %.2f seconds, %dm%ds, avg %.2fs' %
366
+ (total, elapsed, mins, secs, avg),
367
+ file=sys.stderr)
368
+
369
+ return errors, certificates
370
+
371
+
372
+ async def run_tasks(tasks, certificates):
373
+ INTERVAL_WAIT = 3.3 # random throttle sweet spot to avoid TimeoutError
374
+ errors = 0
375
+
376
+ if args.verbose:
377
+ print('running %d tasks' % len(tasks),
378
+ file=sys.stderr)
379
+ start = time.time()
380
+
381
+ for coro in asyncio.as_completed(tasks):
382
+ filename, content, sequence, cert_type = await coro
383
+ if filename is None:
384
+ # error
385
+ print(content)
386
+ errors += 1
387
+ else:
388
+ certificates[sequence].append((cert_type, filename, content))
389
+
390
+ if args.verbose:
391
+ end = time.time()
392
+ elapsed = end - start
393
+ rate = len(tasks) / elapsed
394
+ rate2 = elapsed / len(tasks)
395
+ print('%d tasks complete, elapsed %.2f seconds, '
396
+ '%.2f tasks/sec, %.2f secs/task' %
397
+ (len(tasks), elapsed, rate, rate2), file=sys.stderr)
398
+ print('sleeping %.2fs' % INTERVAL_WAIT, file=sys.stderr)
399
+ await asyncio.sleep(INTERVAL_WAIT)
400
+
401
+ return errors
402
+
403
+
404
+ def create_archive(certificates=None, test=False):
405
+ if not certificates:
406
+ return
407
+
408
+ try:
409
+ with tarfile.open(name=args.certs, mode='w:gz') as tar:
410
+ if test:
411
+ os.unlink(args.certs)
412
+ return
413
+ for sequence in sorted(certificates.keys()):
414
+ for cert in certificates[sequence]:
415
+ name = os.path.join(sequence, cert[0], cert[1])
416
+ member = tarfile.TarInfo(name=name)
417
+ member.size = len(cert[2])
418
+ member.mtime = time.time()
419
+ f = io.BytesIO(cert[2].encode())
420
+ tar.addfile(member, fileobj=f)
421
+ except (tarfile.TarError, OSError) as e:
422
+ print("tarfile %s: %s" % (args.certs, e), file=sys.stderr)
423
+ sys.exit(1)
424
+
425
+
426
+ def parse_args():
427
+ parser = argparse.ArgumentParser(
428
+ usage='%(prog)s [options]',
429
+ description='generate PAN-OS intermediate CAs to preload')
430
+ # curl -OJ \
431
+ # https://ccadb.my.salesforce-sites.com/ccadb/AllCertificateRecordsCSVFormatv2
432
+ parser.add_argument('-c', '--ccadb',
433
+ required=True,
434
+ metavar='PATH',
435
+ help='CCADB AllCertificateRecordsCSVFormatv2 CSV path')
436
+ # sh cert-fingerprints.sh cert-dir
437
+ parser.add_argument('-f', '--fingerprints',
438
+ metavar='PATH',
439
+ help='PAN-OS trusted CAs fingerprints CSV path')
440
+ x = 'certificates.tgz'
441
+ parser.add_argument('--certs',
442
+ default=x,
443
+ metavar='PATH',
444
+ help='certificate archive path'
445
+ ' (default: %s)' % x)
446
+ parser.add_argument('--roots',
447
+ action='store_true',
448
+ help='also download root CAs (experimental)')
449
+ parser.add_argument('--verbose',
450
+ action='store_true',
451
+ help='enable verbosity')
452
+ x = '%s %s' % (title, __version__)
453
+ parser.add_argument('--version',
454
+ action='version',
455
+ help='display version',
456
+ version=x)
457
+ args = parser.parse_args()
458
+
459
+ if args.verbose:
460
+ print(args, file=sys.stderr)
461
+
462
+ return args
463
+
464
+
465
+ if __name__ == '__main__':
466
+ main()