From 9f2a5b2faaa10bfe768c12ade9c50e750d8ad333 Mon Sep 17 00:00:00 2001 From: yaokl Date: Tue, 13 Sep 2022 13:59:16 +0800 Subject: [PATCH] feat: add --- ruby/gitlabsos.rb | 348 +++++++++++++ shell/k8s/k8s-backup.sh | 36 +- shell/ssl/ssl-cert-check.sh | 955 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1331 insertions(+), 8 deletions(-) create mode 100644 ruby/gitlabsos.rb create mode 100644 shell/ssl/ssl-cert-check.sh diff --git a/ruby/gitlabsos.rb b/ruby/gitlabsos.rb new file mode 100644 index 0000000..9db38f2 --- /dev/null +++ b/ruby/gitlabsos.rb @@ -0,0 +1,348 @@ +#!/opt/gitlab/embedded/bin/ruby +# Authors: gitlab.com/cody +# This script provides a unified method of gathering system information and +# GitLab application information. Please consider this script to be in an Alpha +# state. + +require 'json' +require 'tmpdir' +require 'fileutils' +require 'open3' +require 'logger' +require 'optparse' +require 'pathname' + +# allows logging to stdout and a log file +# https://stackoverflow.com/a/6407200 +class MultiIO + def initialize(*targets) + @targets = targets + end + + def write(*args) + @targets.each { |t| t.write(*args) } + end + + def close + @targets.each(&:close) + end +end + +module GitLabSOS + # If you intend to add a large file to this list, you'll need to change the + # file.read call to something that streams rather than slurps + module Files + def list_files + [ + { source: '/opt/gitlab/version-manifest.json', destination: './opt/gitlab/version-manifest.json' }, + { source: '/opt/gitlab/version-manifest.txt', destination: './opt/gitlab/version-manifest.txt' }, + { source: '/var/log/messages', destination: './var/log/messages' }, + { source: '/var/log/syslog', destination: './var/log/syslog' }, + { source: '/proc/mounts', destination: 'mount' }, + { source: '/proc/meminfo', destination: 'meminfo' }, + { source: '/proc/cpuinfo', destination: 'cpuinfo' }, + { source: '/etc/selinux/config', destination: './etc/selinux/config' }, + { source: '/proc/sys/kernel/tainted', destination: 'tainted' }, + { source: '/etc/os-release', destination: './etc/os-release' }, + { source: '/etc/fstab', destination: './etc/fstab' }, + { source: '/etc/security/limits.conf', destination: './etc/security/limits.conf' }, + { source: '/proc/sys/vm/swappiness', destination: 'running_swappiness' }, + { source: '/proc/pressure/io', destination: 'pressure_io.txt' }, + { source: '/proc/pressure/memory', destination: 'pressure_mem.txt' }, + { source: '/proc/pressure/cpu', destination: 'pressure_cpu.txt' } + ] + end + + def run_files + list_files.each do |file_info| + dest = File.join(tmp_dir, file_info[:destination]) + logger.debug "processing #{file_info[:source]}.." + result = begin + # this works better than FileUtils.cp for stuff like /proc/mounts + `tail -c #{options[:max_file_size]} #{file_info[:source]}` + rescue Errno::ENOENT => e + # file doesn't exist + e.message + end + FileUtils.mkdir_p(File.dirname(dest)) + logger.debug "writing #{result.bytesize} bytes to #{dest}" + File.write(dest, result) + end + end + + def run_gitlab_rb + return unless options[:grab_config] + # don't run if __dir__ can't be resolved (i.e. downloaded via curl) + return unless __dir__ + + sanitizer = File.join(__dir__, 'sanitizer/sanitizer') + if File.file?(sanitizer) + logger.info 'Sanitizer module found. `gitlab.rb` file will be collected.' + logger.info 'A copy will be printed on the screen for you to review.' + else + logger.info 'Sanitizer not found. `gitlab.rb` file will not be collected' + return + end + + dest = File.join(tmp_dir, 'etc/gitlab/gitlab.rb') + FileUtils.mkdir_p(File.dirname(dest)) + + logger.info 'Sanitizing /etc/gitlab/gitlab.rb file' + `/opt/gitlab/embedded/bin/ruby #{sanitizer} --save #{dest}` + + # We use 'puts' to show the sanitized gitlab.rb file without + # logging it in gitlabsos.log + puts '' + puts '======================== Sanitized gitlab.rb ========================' + puts 'PLEASE CAREFULLY REVIEW THIS FILE FOR ANY SENSITIVE INFO' + puts 'THE BELOW INFO WILL BE INCLUDED (SANITIZED) IN YOUR GITLABSOS ARCHIVE' + puts '=====================================================================' + puts File.read(dest) + puts '=====================================================================' + puts 'NOTICE: You can skip this with --skip-config' + puts '=====================================================================' + puts '' + end + end + + module Commands + # Add commands to this list that could help collect useful information + # cmd is the command that you want to run, including its options + # result_path is the filename for the output of the cmd that you want to run. + def list_commands + [ + { cmd: 'dmesg -T', result_path: 'dmesg' }, + { cmd: 'uname -a', result_path: 'uname' }, + { cmd: 'su - git -c "ulimit -a"', result_path: 'ulimit' }, + { cmd: 'hostname --fqdn', result_path: 'hostname' }, + { cmd: 'getenforce', result_path: 'getenforce' }, + { cmd: 'sestatus', result_path: 'sestatus' }, + { cmd: 'systemctl list-unit-files', result_path: 'systemctl_unit_files' }, + { cmd: 'uptime', result_path: 'uptime' }, + { cmd: 'df -hT', result_path: 'df_hT' }, + { cmd: 'df -iT', result_path: 'df_inodes' }, + { cmd: 'free -m', result_path: 'free_m' }, + { cmd: 'ps -eo user,pid,%cpu,%mem,vsz,rss,stat,start,time,wchan:24,command', result_path: 'ps' }, + { cmd: 'netstat -txnpl', result_path: 'netstat' }, + { cmd: 'netstat -i', result_path: 'netstat_i' }, + { cmd: 'vmstat -w 1 10', result_path: 'vmstat' }, + { cmd: 'mpstat -P ALL 1 10', result_path: 'mpstat' }, + { cmd: 'pidstat -l 1 15', result_path: 'pidstat' }, + { cmd: 'iostat -xz 1 10', result_path: 'iostat' }, + { cmd: 'nfsiostat 1 10', result_path: 'nfsiostat' }, + { cmd: 'nfsstat -v', result_path: 'nfsstat' }, + { cmd: 'iotop -aoPqt -b -d 1 -n 10', result_path: 'iotop' }, + { cmd: 'top -c -b -n 1 -o %CPU', result_path: 'top_cpu' }, + { cmd: 'top -c -b -n 1 -o RES', result_path: 'top_res' }, + { cmd: 'rpm -vV gitlab-ee', result_path: 'rpm_verify' }, + { cmd: 'sar -n DEV 1 10', result_path: 'sar_dev' }, + { cmd: 'sar -n TCP,ETCP 1 10', result_path: 'sar_tcp' }, + { cmd: 'lscpu', result_path: 'lscpu' }, + { cmd: 'ntpq -pn', result_path: 'ntpq' }, + { cmd: 'timedatectl', result_path: 'timedatectl' }, + { cmd: 'gitlab-ctl status', result_path: 'gitlab_status' }, + { cmd: 'gitlab-rake db:migrate:status', result_path: 'gitlab_migrations' }, + { cmd: 'ss -paxioe', result_path: 'sockstat' }, + { cmd: 'sysctl -a', result_path: 'sysctl_a' }, + { cmd: 'ifconfig', result_path: 'ifconfig' }, + { cmd: 'ip address', result_path: 'ip_address' } + ] + end + + def run_commands + logger.info 'Collecting diagnostics. This will probably take a few minutes..' + list_commands.each do |cmd_info| + dest = File.join(tmp_dir, cmd_info[:result_path]) + full_cmd = "#{cmd_info[:cmd]} | tail -c #{options[:max_file_size]}" + logger.debug "exec: #{full_cmd}" + result = begin + out, err, _status = Open3.capture3(full_cmd) + out + err + end + File.write(dest, result) + end + end + end + + module LogDirectories + def run_log_dirs + logger.info 'Getting GitLab logs..' + logger.debug 'determining log directories..' + + # Ensure empty array if gitlab config file couldn't found or read + log_dirs = config.key?('normal') ? deep_fetch(config['normal'], 'log_directory') : [] + log_dirs << '/var/log/gitlab' + logger.debug "using #{log_dirs}" + + log_dirs.uniq.each do |log_dir| + unless Dir.exist?(log_dir) + logger.warn "log directory '#{log_dir}' does not exist or is not a directory" + next + end + + logger.debug "searching #{log_dir} for log files.." + + find_files(log_dir).each do |log| + process_log(log) if log.mtime > Time.now - (60 * 60 * 12) && log.basename.to_s !~ /.*.gz|^@|lock/ + end + end + end + + def process_log(log) + begin # rubocop:disable Style/RedundantBegin -- To maintain compatibility with Ruby < 2.5 + logger.debug "processing log - #{log}.." + content = `tail -c #{options[:max_file_size]} #{log}` + content = content.lines.drop(1).join unless content.lines.count < 2 + FileUtils.mkdir_p(File.dirname(File.join(tmp_dir, log))) + logger.debug "writing #{content.bytesize} bytes to #{File.join(tmp_dir, log)}" + File.write(File.join(tmp_dir, log), content) + rescue => e + logger.error "could not process log - #{log}" + logger.error e.message + end + end + + def find_files(*paths) + paths.flatten.map do |path| + path = Pathname.new(path) + path.file? ? [path] : find_files(path.children) + end.flatten + end + end + + # This is the first itteration designed to make + # https://gitlab.com/gitlab-com/support/toolbox/gitlabsos/issues/11 and + # https://gitlab.com/gitlab-com/support/toolbox/gitlabsos/issues/7 easier and + # any aditional options/filter we can think of in the futher. + class Client + attr_accessor :options, :logger, :log_file, :tmp_dir, :config + include Files + include Commands + include LogDirectories + + HOSTNAME = `hostname`.strip + REPORT_NAME = "gitlabsos.#{HOSTNAME}_#{Time.now.strftime('%Y%m%d%H%M%S')}".freeze + TMP_DIR = File.join(ENV['TMP'] || ENV['TMPDIR'] || '/tmp', REPORT_NAME) + + def initialize(args) + @args = args + parse_options! + setup_logger + root_check + setup_config + run + end + + def setup_config + self.config = {} + + config_file = Dir.glob('/opt/gitlab/embedded/nodes/*.json').max_by { |f| File.mtime(f) } + + # Ignore if missing + return nil unless config_file + + # Grab first file + self.config = JSON.parse File.read(config_file) + end + + def default_options + { + output_file: File.expand_path("./#{REPORT_NAME}.tar.gz"), + logs_only: false, + log_level: Logger::INFO, + root_check: true, + max_file_size: 10 * 1_000_000, # 10MB + grab_config: true + } + end + + def root_check + raise 'Script must be run as root' unless Process.uid.zero? || !options[:root_check] + end + + def create_temp_directory + self.tmp_dir = FileUtils.mkdir_p(TMP_DIR).join + rescue Errno::ENOENT => e + # TODO: Handle error Permission denied. + e.message + end + + def setup_logger + create_temp_directory + self.log_file ||= File.open(File.join(TMP_DIR, 'gitlabsos.log'), 'a') + self.logger = Logger.new MultiIO.new(STDOUT, log_file) + logger.level = options[:log_level] + logger.progname = 'gitlabsos' + logger.formatter = proc do |severity, datetime, progname, msg| + "[#{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')}] #{severity} -- #{progname}: #{msg}\n" + end + end + + # this method is used to fetch all values out of a hash for any given key + # I'm just using it to get custom log directories + def deep_fetch(hash, key) + hash.values.map do |obj| + next if obj.class != Hash + + if obj.key? key + obj[key] + else + deep_fetch(obj, key) + end + end.flatten.compact + end + + def parse_options! + self.options = default_options + + OptionParser.new do |opts| + opts.banner = 'Usage: gitlabsos.rb [options]' + + opts.on('-o FILE', '--output-file FILE', 'Write gitlabsos report to FILE') do |file| + options[:output_file] = File.expand_path(file) + end + + opts.on('--debug', 'Set the log level to debug') do + options[:log_level] = Logger::DEBUG + end + + opts.on('--skip-root-check', 'Run the script as non-root. Warning: script might fail') do + options[:root_check] = false + end + + opts.on('--skip-config', 'Don\'t include a sanitized copy of the gitlab.rb configuration file.') do + options[:grab_config] = false + end + + opts.on('--max-file-size MB', 'Set the max file size (in megabytes) for any file in the report') do |mb| + options[:max_file_size] = mb.to_i * 1_000_000 + end + + opts.on('-h', '--help', 'Prints this help') do + puts opts + exit + end + end.parse!(@args) + end + + def run + logger.info 'Starting gitlabsos report' + logger.info 'Gathering configuration and system info..' + + run_files + run_commands + run_log_dirs + run_gitlab_rb + + logger.info 'Report finished.' + log_file.close + + puts "Saving to: '#{options[:output_file]}'" + system("tar -czf #{options[:output_file]} #{File.basename(TMP_DIR)}", + chdir: File.dirname(TMP_DIR)) + FileUtils.remove_dir(TMP_DIR) + end + end +end + +GitLabSOS::Client.new(ARGV) diff --git a/shell/k8s/k8s-backup.sh b/shell/k8s/k8s-backup.sh index f476272..d965d66 100644 --- a/shell/k8s/k8s-backup.sh +++ b/shell/k8s/k8s-backup.sh @@ -19,8 +19,10 @@ set -o pipefail # Use last non-zero exit code in a pipeline # environment configuration ###################################################################################################### +KUBECONFIG="${HOME:-'~'}/.kube/config" # kubenetes config NAMESPACE="${NAMESPACE:-all}" RESOURCES="${RESOURCES:-all}" +WITH_CLUSTER="true" RESOURCES_PATH="/opt/k8s-backup_$(date +%s)" ###################################################################################################### @@ -29,33 +31,40 @@ RESOURCES_PATH="/opt/k8s-backup_$(date +%s)" function get::resource() { - ns=$1 + ns=${1:-cluster} + namespaced="true" + + if [[ "$ns" == "cluster" ]]; then + namespaced="false" + [ -d "$RESOURCES_PATH/cluster" ] || mkdir -p "$RESOURCES_PATH/cluster" + fi + if [[ "${RESOURCES}" == "all" ]]; then - RESOURCES=$(kubectl api-resources --verbs=list --namespaced -o name | grep -v "events.events.k8s.io" | grep -v "events" | sort |uniq) + RESOURCES=$($kubectl api-resources --verbs=list --namespaced=${namespaced} -o name | grep -v "events.events.k8s.io" | grep -v "events" | sort | uniq) fi for r in ${RESOURCES}; do echo "Resource:" $r - for l in $(kubectl -n ${ns} get --ignore-not-found ${r} -o jsonpath="{$.items[*].metadata.name}");do - kubectl -n ${ns} get --ignore-not-found ${r} ${l} -o yaml \ + for l in $($kubectl -n ${ns} get --ignore-not-found ${r} -o jsonpath="{$.items[*].metadata.name}");do + $kubectl -n ${ns} get --ignore-not-found ${r} ${l} -o yaml \ | sed -n "/ managedFields:/{p; :a; N; / name: ${l}/!ba; s/.*\\n//}; p" \ | sed -e 's/ uid:.*//g' \ -e 's/ resourceVersion:.*//g' \ -e 's/ selfLink:.*//g' \ -e 's/ creationTimestamp:.*//g' \ -e 's/ managedFields:.*//g' \ - -e '/^\s*$/d' > "$RESOURCES_PATH/${n}/${l}.${r}.yaml" + -e '/^\s*$/d' > "$RESOURCES_PATH/${ns}/${l}_${r}.yaml" done done } function get::namespace() { if [[ "${RESOURCES}" == "all" ]]; then - NAMESPACE=$(kubectl get ns -o jsonpath="{$.items[*].metadata.name}") + NAMESPACE=$($kubectl get ns -o jsonpath="{$.items[*].metadata.name}") fi for n in ${NAMESPACE};do echo "Namespace:" $n [ -d "$RESOURCES_PATH/$n" ] || mkdir -p "$RESOURCES_PATH/$n" - kubectl get ns ${n} --ignore-not-found -o yaml \ + $kubectl get ns ${n} --ignore-not-found -o yaml \ | sed -n "/ managedFields:/{p; :a; N; / name: ${n}/!ba; s/.*\\n//}; p" \ | sed -e 's/ uid:.*//g' \ -e 's/ resourceVersion:.*//g' \ @@ -78,8 +87,10 @@ Usage: $(basename $0) [flag] Flag: + -c,--kubeconfig Specify kubeconfig, default is ${HOME:-~}/.kube/config -ns,--namespace namespace, default: all -r,--resource resource, default: all + --with-cluster cluster resource. -h,--help help info. EOF @@ -93,12 +104,18 @@ EOF while [ "${1:-}" != "" ]; do case $1 in + -c | --kubeconfig ) shift + KUBECONFIG=${1:-$KUBECONFIG} + ;; -ns | --namespace ) shift NAMESPACE=${1:-$NAMESPACE} ;; -r | --resource ) shift RESOURCES=${1:-$RESOURCES} ;; + --with-cluster ) shift + WITH_CLUSTER=${1:-$WITH_CLUSTER} + ;; -h | --help ) help::usage ;; * ) help::usage @@ -106,6 +123,9 @@ while [ "${1:-}" != "" ]; do shift done -get::namespace +kubectl="kubectl --kubeconfig $KUBECONFIG" + +#get::namespace +[[ "$WITH_CLUSTER" == "true" ]] && get::resource cluster || echo echo "File: ${RESOURCES_PATH}" diff --git a/shell/ssl/ssl-cert-check.sh b/shell/ssl/ssl-cert-check.sh new file mode 100644 index 0000000..03819ef --- /dev/null +++ b/shell/ssl/ssl-cert-check.sh @@ -0,0 +1,955 @@ +#!/usr/bin/env bash +PROGRAMVERSION=4.14 +# +# Program: SSL Certificate Check +# +# Source code home: https://github.com/Matty9191/ssl-cert-check +# +# Documentation: http://prefetch.net/articles/checkcertificate.html +# +# Author: Matty < matty at prefetch dot net > +# +# Last Updated: 11-12-2020 +# +# Revision History: +# +# Version 4.14 +# - Fixed HOST / PORT discovery @mhow2 +# +# Version 4.13 +# - Reverted the file checking logic which breaks $RETCODE +# +# Version 4.12 +# - Fixed various logic errors and typos -- Daniel Lewart +# +# Version 4.10 +# - Replace tabs with spaces +# - More shllcheck cleanup work +# - Remove unused DEBUG variable +# - Fixed an innocuous whitespace bug in TLSFLAG variable creation +# - Set the default TLS version to 1.1 (can be overridden with -v) +# - Switched openssl CLI options to use an array. The reasons why +# are documented here: http://mywiki.wooledge.org/BashFAQ/050 +# +# Version 4.9 +# - Add a signal handler to call the cleanup funtion +# if the script doesn't exit() cleanly -- Timothe Litt +# +# Version 4.8 +# - More mail client fixes +# +# Version 4.7 +# - Revert SENDER to "" +# - More shellcheck cleanup +# +# Version 4.6 +# - Fixed programming logic error +# +# Version 4.5 +# - Re-work mailx support for FreeBSD +# - More shellcheck fixes +# +# Version 4.4 +# - Use command -v instead of which utility to satisfy shellcheck. +# - Fix unquoted MAIL and MAILMODE variables in help output +# - More shellcheck fixes +# +# Version 4.3 +# - Fixed a typo in the program version +# +# Version 4.2 +# - Change CERTDAYS to CERTDIFF in the e-mail subject. +# +# Version 4.1 +# - Fix usage output +# +# Version 4.0 +# - Updated the script syntax to align with UNIX shell programming +# - Check for DNS resolution failures +# - First round of updates to make shellcheck happy +# - Rework the logic to call mailx. +# - Print the version with the "-V" option. +# - Define the version in the PROGRAMVERSION variable +# +# Version 3.31 +# - Fixed the test for the -servername flag -- Kitson Consulting. +# +# Version 3.30 +# - Use highest returncode for Nagios output -- Marcel Pennewiss +# - Set RETCODE to 3 (unknown) if a certificate file does not exist -- Marcel Pennewiss +# - Add a "-d" option to specify a directory or file mask pattern -- Marcel Pennewiss +# - Add a "-N" option to create summarized Nagios output -- Marcel Pennewiss +# - Cleaned up many formatting -- Marcel Pennewiss +# +# Versione 3.29a +# - Added option to specify email sender address +# +# Version 3.29 +# - Add the openssl -servername flag if it shows up in help. +# +# Version 3.28 +# - Added a DEBUG option to assist with debugging folks who use the script +# +# Version 3.27 +# - Allow white spaces to exist in the certificate file list +# - Add an additional check to pick up bad / non-existent certificates +# - Add a check to look for the existence of a mail program. Error out if it's not present. +# - Enable the TLS -servername extension by default - Juergen Knaack & Johan Denoyer +# +# Version 3.26 +# - Allow the certificate type (PEM, DER, NET) to be passed on the command line +# +# Version 3.25 +# - Check for "no route to host" errors -- Dan Doyle +# - Set RETCODE to 3 (unknown) if a connection error occurs -- Dan Doyle +# - Documentation fixes +# +# Version 3.24 +# - Utilize the -clcerts option to limit the results to client certificates - Eitan Katznelson +# +# Version 3.23 +# - Fixed typo in date2julian routine -- Ken Cook +# +# Version 3.22 +# - Change the validation option to "-V" +# - Add a "-v" option to specify a specific protocol version (ssl2, ssl3 or tls) +# +# Version 3.21 +# - Adjust e-mail checking to avoid exiting if notifications aren't enabled -- Nick Anderson +# - Added the number of days until expiration to the Nagios output -- Nick Anderson +# +# Version 3.20 +# - Fixed a bug in certificate length checking -- Tim Nowaczyk +# +# Version 3.19 +# - Added check to verify the certificate retrieved is valid +# +# Version 3.18 +# - Add support for connecting to FTP servers -- Paul A Sand +# +# Version 3.17 +# - Add support for connecting to imap servers -- Joerg Pareigis +# +# Version 3.16 +# - Add support for connecting to the mail sbmission port -- Luis E. Munoz +# +# Version 3.15 +# - Adjusted the file checking logic to use the correct certificate -- Maciej Szudejko +# - Add sbin to the default search paths for OpenBSD compatibility -- Alex Popov +# - Use cut instead of substring processing to ensure compatibility -- Alex Popov +# +# Version 3.14 +# - Fixed the Common Name parser to handle DN's where the CN is not the last item +# eg. EmailAddr -- Jason Brothers +# - Added the ability to grab the serial number -- Jason Brothers +# - Added the "-b" option to print results without a header -- Jason Brothers +# - Added the "-v" option for certificate validation -- Jason Brothers +# +# Version 3.13 +# - Updated the subject line to include the hostname as well as +# the common name embedded in the X509 certificate (if it's +# available) -- idea proposed by Mike Burns +# +# Version 3.12 +# - Updated the license to allow redistribution and modification +# +# Version 3.11 +# - Added ability to comment out lines in files passed +# to the "-f" option -- Brett Stauner +# - Fixed comment next to file processing logic +# +# Version 3.10 +# - Fixed POP3 port -- Simon Matter +# +# Version 3.9 +# - Switched binary location logic to use which utility +# +# Version 3.8 +# - Fixed display on 80 column displays +# - Cleaned up the formatting +# +# Version 3.7 +# - Fixed bug in NAGIOS tests -- Ben Allen +# +# Version 3.6 +# - Added support for certificates stored in PKCS#12 databases -- Ken Gallo +# - Cleaned up comments +# - Adjusted variables to be more consistent +# +# Version 3.5 +# - Added support for NAGIOS -- Quanah Gibson-Mount +# - Added additional checks for mail -- Quanah Gibson-Mount +# - Convert tabs to spaces -- Quanah Gibson-Mount +# - Cleaned up usage() routine +# - Added additional checks for openssl +# +# Version 3.4 +# - Added a missing "{" to line 364 -- Ken Gallo +# - Move mktemp to the start of the main body to avoid errors +# - Adjusted default binary paths to make sure the script just works +# w/ Solaris, BSD and Linux hosts +# +# Version 3.3 +# - Added common name from X.509 certificate file to E-mail body / header -- Doug Curtis +# - Fixed several documentation errors +# - Use mktemp to create temporary files +# - Convert printf, sed and awk to variables +# - Check for printf, sed, awk and mktemp binaries +# - Add additional logic to make sure mktemp returned a valid temporary file +# +# Version 3.2 +# - Added option to list certificates in the file passed to "-f". +# +# Version 3.1 +# - Added handling for starttls for smtp -- Marco Amrein +# - Added handling for starttls for pop3 (without s) -- Marco Amrein +# - Removed extra spacing at end of script +# +# Version 3.0 +# - Added "-i" option to print certificate issuer +# - Removed $0 from Subject line of outbound e-mails +# - Fixed some typographical errors +# - Removed redundant "-b" option +# +# Version 2.0 +# - Fixed an issue with e-mails formatting incorrectly +# - Added additional space to host column -- Darren-Perot Spruell +# - Replaced GNU date dependency with CHRIS F. A. JOHNSON's +# date2julian shell function. This routine can be found on +# page 170 of Chris's book "Shell Scripting Recipes: A +# Problem-Solution Approach," ISBN #1590594711. Julian function +# was created based on a post to comp.unix.shell by Tapani Tarvainen. +# - Cleaned up function descriptions +# - Removed several lines of redundant code +# - Adjusted the help message +# +# Version 1.1 +# - Added "-c" flag to report expiration status of a PEM encoded +# certificate -- Hampus Lundqvist +# - Updated the prints messages to display the reason a connection +# failed (connection refused, connection timeout, bad cert, etc) +# - Updated the GNU date checking routines +# - Added checks for each binary required +# - Added checks for connection timeouts +# - Added checks for GNU date +# - Added a "-h" option +# - Cleaned up the documentation +# +# Version 1.0 +# Initial Release +# +# Purpose: +# ssl-cert-check checks to see if a digital certificate in X.509 format +# has expired. ssl-cert-check can be run in interactive and batch mode, +# and provides facilities to alarm if a certificate is about to expire. +# +# License: +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# Requirements: +# Requires openssl +# +# Installation: +# Copy the shell script to a suitable location +# +# Tested platforms: +# -- Solaris 9 using /bin/bash +# -- Solaris 10 using /bin/bash +# -- OS X 10.4.2 using /bin/bash +# -- OpenBSD using /bin/sh +# -- FreeBSD using /bin/sh +# -- Centos Linux 3, 4, 5 & 6 using /bin/bash +# -- Redhat Enterprise Linux 3, 4, 5 & 6 using /bin/bash +# -- Gentoo using /bin/bash +# +# Usage: +# Refer to the usage() sub-routine, or invoke ssl-cert-check +# with the "-h" option. +# +# Examples: +# Please refer to the following site for documentation and examples: +# http://prefetch.net/articles/checkcertificate.html + +# Cleanup temp files if they exist +trap cleanup EXIT INT TERM QUIT + +PATH=/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/ssl/bin:/usr/sfw/bin +export PATH + +# Who to page when an expired certificate is detected (cmdline: -e) +ADMIN="root" + +# Email sender address for alarm notifications +SENDER="" + +# Number of days in the warning threshhold (cmdline: -x) +WARNDAYS=30 + +# If QUIET is set to TRUE, don't print anything on the console (cmdline: -q) +QUIET="FALSE" + +# Don't send E-mail by default (cmdline: -a) +ALARM="FALSE" + +# Don't run as a Nagios plugin by default (cmdline: -n) +NAGIOS="FALSE" + +# Don't summarize Nagios output by default (cmdline: -N) +NAGIOSSUMMARY="FALSE" + +# NULL out the PKCSDBPASSWD variable for later use (cmdline: -k) +PKCSDBPASSWD="" + +# Type of certificate (PEM, DER, NET) (cmdline: -t) +CERTTYPE="pem" + +# Location of system binaries +AWK=$(command -v awk) +DATE=$(command -v date) +GREP=$(command -v grep) +OPENSSL=$(command -v openssl) +PRINTF=$(command -v printf) +SED=$(command -v sed) +MKTEMP=$(command -v mktemp) +FIND=$(command -v find) + +# Try to find a mail client +if [ -f /usr/bin/mailx ]; then + MAIL="/usr/bin/mailx" + MAILMODE="mailx" +elif [ -f /bin/mail ]; then + MAIL="/bin/mail" + MAILMODE="mail" +elif [ -f /usr/bin/mail ]; then + MAIL="/usr/bin/mail" + MAILMODE="mail" +elif [ -f /sbin/mail ]; then + MAIL="/sbin/mail" + MAILMODE="mail" +elif [ -f /usr/sbin/mail ]; then + MAIL="/usr/sbin/mail" + MAILMODE="mail" +elif [ -f /usr/sbin/sendmail ]; then + MAIL="/usr/sbin/sendmail" + MAILMODE="sendmail" +else + MAIL="cantfindit" + MAILMODE="cantfindit" +fi + +# Return code used by nagios. Initialize to 0. +RETCODE=0 + +# Certificate counters and minimum difference. Initialize to 0. +SUMMARY_VALID=0 +SUMMARY_WILL_EXPIRE=0 +SUMMARY_EXPIRED=0 +SUMMARY_MIN_DIFF=0 +SUMMARY_MIN_DATE= +SUMMARY_MIN_HOST= +SUMMARY_MIN_PORT= + +# Set the default umask to be somewhat restrictive +umask 077 + + +##################################################### +# Purpose: Remove temporary files if the script doesn't +# exit() cleanly +##################################################### +cleanup() { + if [ -f "${CERT_TMP}" ]; then + rm -f "${CERT_TMP}" + fi + + if [ -f "${ERROR_TMP}" ]; then + rm -f "${ERROR_TMP}" + fi +} + + +##################################################### +### Send email +### Accepts three parameters: +### $1 -> sender email address +### $2 -> email to send mail +### $3 -> Subject +### $4 -> Message +##################################################### +send_mail() { + + FROM="${1}" + TO="${2}" + SUBJECT="${3}" + MSG="${4}" + + case "${MAILMODE}" in + "mail") + echo "$MSG" | "${MAIL}" -r "$FROM" -s "$SUBJECT" "$TO" + ;; + "mailx") + echo "$MSG" | "${MAIL}" -s "$SUBJECT" "$TO" + ;; + "sendmail") + (echo "Subject:$SUBJECT" && echo "TO:$TO" && echo "FROM:$FROM" && echo "$MSG") | "${MAIL}" "$TO" + ;; + "*") + echo "ERROR: You enabled automated alerts, but the mail binary could not be found." + echo "FIX: Please modify the \${MAIL} and \${MAILMODE} variable in the program header." + exit 1 + ;; + esac +} + +############################################################################# +# Purpose: Convert a date from MONTH-DAY-YEAR to Julian format +# Acknowledgements: Code was adapted from examples in the book +# "Shell Scripting Recipes: A Problem-Solution Approach" +# ( ISBN 1590594711 ) +# Arguments: +# $1 -> Month (e.g., 06) +# $2 -> Day (e.g., 08) +# $3 -> Year (e.g., 2006) +############################################################################# +date2julian() { + + if [ "${1}" != "" ] && [ "${2}" != "" ] && [ "${3}" != "" ]; then + ## Since leap years add aday at the end of February, + ## calculations are done from 1 March 0000 (a fictional year) + d2j_tmpmonth=$((12 * $3 + $1 - 3)) + + ## If it is not yet March, the year is changed to the previous year + d2j_tmpyear=$(( d2j_tmpmonth / 12)) + + ## The number of days from 1 March 0000 is calculated + ## and the number of days from 1 Jan. 4713BC is added + echo $(( (734 * d2j_tmpmonth + 15) / 24 + - 2 * d2j_tmpyear + d2j_tmpyear/4 + - d2j_tmpyear/100 + d2j_tmpyear/400 + $2 + 1721119 )) + else + echo 0 + fi +} + +############################################################################# +# Purpose: Convert a string month into an integer representation +# Arguments: +# $1 -> Month name (e.g., Sep) +############################################################################# +getmonth() +{ + case ${1} in + Jan) echo 1 ;; + Feb) echo 2 ;; + Mar) echo 3 ;; + Apr) echo 4 ;; + May) echo 5 ;; + Jun) echo 6 ;; + Jul) echo 7 ;; + Aug) echo 8 ;; + Sep) echo 9 ;; + Oct) echo 10 ;; + Nov) echo 11 ;; + Dec) echo 12 ;; + *) echo 0 ;; + esac +} + +############################################################################# +# Purpose: Calculate the number of seconds between two dates +# Arguments: +# $1 -> Date #1 +# $2 -> Date #2 +############################################################################# +date_diff() +{ + if [ "${1}" != "" ] && [ "${2}" != "" ]; then + echo $((${2} - ${1})) + else + echo 0 + fi +} + +##################################################################### +# Purpose: Print a line with the expiraton interval +# Arguments: +# $1 -> Hostname +# $2 -> TCP Port +# $3 -> Status of certification (e.g., expired or valid) +# $4 -> Date when certificate will expire +# $5 -> Days left until the certificate will expire +# $6 -> Issuer of the certificate +# $7 -> Common Name +# $8 -> Serial Number +##################################################################### +prints() +{ + if [ "${NAGIOSSUMMARY}" = "TRUE" ]; then + return + fi + + if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then + MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d %4d", $1, $2, $4 }') + if [ "${NAGIOS}" = "TRUE" ]; then + ${PRINTF} "%-35s %-17s %-8s %-11s %s\n" "$1:$2" "$6" "$3" "$MIN_DATE" "|days=$5" + else + ${PRINTF} "%-35s %-17s %-8s %-11s %4d\n" "$1:$2" "$6" "$3" "$MIN_DATE" "$5" + fi + elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then + ${PRINTF} "%-35s %-35s %-32s %-17s\n" "$1:$2" "$7" "$8" "$6" + + elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then + MIN_DATE=$(echo "$4" | "${AWK}" '{ printf "%3s %2d, %4d", $1, $2, $4 }') + if [ "${NAGIOS}" = "TRUE" ]; then + ${PRINTF} "%-47s %-12s %-12s %s\n" "$1:$2" "$3" "$MIN_DATE" "|days=$5" + else + ${PRINTF} "%-47s %-12s %-12s %4d\n" "$1:$2" "$3" "$MIN_DATE" "$5" + fi + elif [ "${QUIET}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then + ${PRINTF} "%-35s %-35s %-32s\n" "$1:$2" "$7" "$8" + fi +} + + +#################################################### +# Purpose: Print a heading with the relevant columns +# Arguments: +# None +#################################################### +print_heading() +{ + if [ "${NOHEADER}" != "TRUE" ]; then + if [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then + ${PRINTF} "\n%-35s %-17s %-8s %-11s %-4s\n" "Host" "Issuer" "Status" "Expires" "Days" + echo "----------------------------------- ----------------- -------- ----------- ----" + + elif [ "${QUIET}" != "TRUE" ] && [ "${ISSUER}" = "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then + ${PRINTF} "\n%-35s %-35s %-32s %-17s\n" "Host" "Common Name" "Serial #" "Issuer" + echo "----------------------------------- ----------------------------------- -------------------------------- -----------------" + + elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" != "TRUE" ]; then + ${PRINTF} "\n%-47s %-12s %-12s %-4s\n" "Host" "Status" "Expires" "Days" + echo "----------------------------------------------- ------------ ------------ ----" + + elif [ "${QUIET}" != "TRUE" ] && [ "${NAGIOS}" != "TRUE" ] && [ "${VALIDATION}" = "TRUE" ]; then + ${PRINTF} "\n%-35s %-35s %-32s\n" "Host" "Common Name" "Serial #" + echo "----------------------------------- ----------------------------------- --------------------------------" + fi + fi +} + +#################################################### +# Purpose: Print a summary for nagios +# Arguments: +# None +#################################################### +print_summary() +{ + if [ "${NAGIOSSUMMARY}" != "TRUE" ]; then + return + fi + + if [ ${SUMMARY_WILL_EXPIRE} -eq 0 ] && [ ${SUMMARY_EXPIRED} -eq 0 ]; then + ${PRINTF} "%s valid certificate(s)|days=%s\n" "${SUMMARY_VALID}" "${SUMMARY_MIN_DIFF}" + + elif [ ${SUMMARY_EXPIRED} -ne 0 ]; then + ${PRINTF} "%s certificate(s) expired (%s:%s on %s)|days=%s\n" "${SUMMARY_EXPIRED}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}" + + elif [ ${SUMMARY_WILL_EXPIRE} -ne 0 ]; then + ${PRINTF} "%s certificate(s) will expire (%s:%s on %s)|days=%s\n" "${SUMMARY_WILL_EXPIRE}" "${SUMMARY_MIN_HOST}" "${SUMMARY_MIN_PORT}" "${SUMMARY_MIN_DATE}" "${SUMMARY_MIN_DIFF}" + + fi +} + +############################################################# +# Purpose: Set returncode to value if current value is lower +# Arguments: +# $1 -> New returncorde +############################################################# +set_returncode() +{ + if [ "${RETCODE}" -lt "${1}" ]; then + RETCODE="${1}" + fi +} + +######################################################################## +# Purpose: Set certificate counters and informations for nagios summary +# Arguments: +# $1 -> Status of certificate (0: valid, 1: will expire, 2: expired) +# $2 -> Hostname +# $3 -> TCP Port +# $4 -> Date when certificate will expire +# $5 -> Days left until the certificate will expire +######################################################################## +set_summary() +{ + if [ "${1}" -eq 0 ]; then + SUMMARY_VALID=$((SUMMARY_VALID+1)) + elif [ "${1}" -eq 1 ]; then + SUMMARY_WILL_EXPIRE=$((SUMMARY_WILL_EXPIRE+1)) + else + SUMMARY_EXPIRED=$((SUMMARY_EXPIRED+1)) + fi + + if [ "${5}" -lt "${SUMMARY_MIN_DIFF}" ] || [ "${SUMMARY_MIN_DIFF}" -eq 0 ]; then + SUMMARY_MIN_DATE="${4}" + SUMMARY_MIN_DIFF="${5}" + SUMMARY_MIN_HOST="${2}" + SUMMARY_MIN_PORT="${3}" + fi +} + +########################################## +# Purpose: Describe how the script works +# Arguments: +# None +########################################## +usage() +{ + echo "Usage: $0 [ -e email address ] [-E sender email address] [ -x days ] [-q] [-a] [-b] [-h] [-i] [-n] [-N] [-v]" + echo " { [ -s common_name ] && [ -p port] } || { [ -f cert_file ] } || { [ -c cert file ] } || { [ -d cert dir ] }" + echo "" + echo " -a : Send a warning message through E-mail" + echo " -b : Will not print header" + echo " -c cert file : Print the expiration date for the PEM or PKCS12 formatted certificate in cert file" + echo " -d cert directory : Print the expiration date for the PEM or PKCS12 formatted certificates in cert directory" + echo " -e E-mail address : E-mail address to send expiration notices" + echo " -E E-mail sender : E-mail address of the sender" + echo " -f cert file : File with a list of FQDNs and ports" + echo " -h : Print this screen" + echo " -i : Print the issuer of the certificate" + echo " -k password : PKCS12 file password" + echo " -n : Run as a Nagios plugin" + echo " -N : Run as a Nagios plugin and output one line summary (implies -n, requires -f or -d)" + echo " -p port : Port to connect to (interactive mode)" + echo " -q : Don't print anything on the console" + echo " -s commmon name : Server to connect to (interactive mode)" + echo " -S : Print validation information" + echo " -t type : Specify the certificate type" + echo " -V : Print version information" + echo " -x days : Certificate expiration interval (eg. if cert_date < days)" + echo "" +} + + +########################################################################## +# Purpose: Connect to a server ($1) and port ($2) to see if a certificate +# has expired +# Arguments: +# $1 -> Server name +# $2 -> TCP port to connect to +########################################################################## +check_server_status() { + + PORT="$2" + case "$PORT" in + smtp|25|submission|587) TLSFLAG="-starttls smtp";; + pop3|110) TLSFLAG="-starttls pop3";; + imap|143) TLSFLAG="-starttls imap";; + ftp|21) TLSFLAG="-starttls ftp";; + xmpp|5222) TLSFLAG="-starttls xmpp";; + xmpp-server|5269) TLSFLAG="-starttls xmpp-server";; + irc|194) TLSFLAG="-starttls irc";; + postgres|5432) TLSFLAG="-starttls postgres";; + mysql|3306) TLSFLAG="-starttls mysql";; + lmtp|24) TLSFLAG="-starttls lmtp";; + nntp|119) TLSFLAG="-starttls nntp";; + sieve|4190) TLSFLAG="-starttls sieve";; + ldap|389) TLSFLAG="-starttls ldap";; + *) TLSFLAG="";; + esac + + if [ "${TLSSERVERNAME}" = "FALSE" ]; then + OPTIONS="-connect ${1}:${2} $TLSFLAG" + else + OPTIONS="-connect ${1}:${2} -servername ${1} $TLSFLAG" + fi + + echo "" | "${OPENSSL}" s_client $OPTIONS 2> "${ERROR_TMP}" 1> "${CERT_TMP}" + + if "${GREP}" -i "Connection refused" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "Connection refused" "Unknown" + set_returncode 3 + elif "${GREP}" -i "No route to host" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "No route to host" "Unknown" + set_returncode 3 + elif "${GREP}" -i "gethostbyname failure" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "Cannot resolve domain" "Unknown" + set_returncode 3 + elif "${GREP}" -i "Operation timed out" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "Operation timed out" "Unknown" + set_returncode 3 + elif "${GREP}" -i "ssl handshake failure" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "SSL handshake failed" "Unknown" + set_returncode 3 + elif "${GREP}" -i "connect: Connection timed out" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "Connection timed out" "Unknown" + set_returncode 3 + elif "${GREP}" -i "Name or service not known" "${ERROR_TMP}" > /dev/null; then + prints "${1}" "${2}" "Unable to resolve the DNS name ${1}" "Unknown" + set_returncode 3 + else + check_file_status "${CERT_TMP}" "${1}" "${2}" + fi +} + +##################################################### +### Check the expiration status of a certificate file +### Accepts three parameters: +### $1 -> certificate file to process +### $2 -> Server name +### $3 -> Port number of certificate +##################################################### +check_file_status() { + + CERTFILE="${1}" + HOST="${2}" + PORT="${3}" + + ### Check to make sure the certificate file exists + if [ ! -r "${CERTFILE}" ] || [ ! -s "${CERTFILE}" ]; then + echo "ERROR: The file named ${CERTFILE} is unreadable or doesn't exist" + echo "ERROR: Please check to make sure the certificate for ${HOST}:${PORT} is valid" + set_returncode 3 + return + fi + + ### Grab the expiration date from the X.509 certificate + if [ "${PKCSDBPASSWD}" != "" ]; then + # Extract the certificate from the PKCS#12 database, and + # send the informational message to /dev/null + "${OPENSSL}" pkcs12 -nokeys -in "${CERTFILE}" \ + -out "${CERT_TMP}" -clcerts -password pass:"${PKCSDBPASSWD}" 2> /dev/null + + # Extract the expiration date from the certificate + CERTDATE=$("${OPENSSL}" x509 -in "${CERT_TMP}" -enddate -noout | \ + "${SED}" 's/notAfter\=//') + + # Extract the issuer from the certificate + CERTISSUER=$("${OPENSSL}" x509 -in "${CERT_TMP}" -issuer -noout | \ + "${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/ + { print substr($0,5,17)}') + + ### Grab the common name (CN) from the X.509 certificate + COMMONNAME=$("${OPENSSL}" x509 -in "${CERT_TMP}" -subject -noout | \ + "${SED}" -e 's/.*CN = //' | \ + "${SED}" -e 's/, .*//') + + ### Grab the serial number from the X.509 certificate + SERIAL=$("${OPENSSL}" x509 -in "${CERT_TMP}" -serial -noout | \ + "${SED}" -e 's/serial=//') + else + # Extract the expiration date from the ceriticate + CERTDATE=$("${OPENSSL}" x509 -in "${CERTFILE}" -enddate -noout -inform "${CERTTYPE}" | \ + "${SED}" 's/notAfter\=//') + + # Extract the issuer from the certificate + CERTISSUER=$("${OPENSSL}" x509 -in "${CERTFILE}" -issuer -noout -inform "${CERTTYPE}" | \ + "${AWK}" 'BEGIN {RS=", " } $0 ~ /^O =/ { print substr($0,5,17)}') + + ### Grab the common name (CN) from the X.509 certificate + COMMONNAME=$("${OPENSSL}" x509 -in "${CERTFILE}" -subject -noout -inform "${CERTTYPE}" | \ + "${SED}" -e 's/.*CN = //' | \ + "${SED}" -e 's/, .*//') + + ### Grab the serial number from the X.509 certificate + SERIAL=$("${OPENSSL}" x509 -in "${CERTFILE}" -serial -noout -inform "${CERTTYPE}" | \ + "${SED}" -e 's/serial=//') + fi + + ### Split the result into parameters, and pass the relevant pieces to date2julian + set -- ${CERTDATE} + MONTH=$(getmonth "${1}") + + # Convert the date to seconds, and get the diff between NOW and the expiration date + CERTJULIAN=$(date2julian "${MONTH#0}" "${2#0}" "${4}") + CERTDIFF=$(date_diff "${NOWJULIAN}" "${CERTJULIAN}") + + if [ "${CERTDIFF}" -lt 0 ]; then + if [ "${ALARM}" = "TRUE" ]; then + send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!" \ + "The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" has expired!" + fi + + prints "${HOST}" "${PORT}" "Expired" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}" + RETCODE_LOCAL=2 + + elif [ "${CERTDIFF}" -lt "${WARNDAYS}" ]; then + if [ "${ALARM}" = "TRUE" ]; then + send_mail "${SENDER}" "${ADMIN}" "Certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire in ${CERTDIFF} days or less" \ + "The SSL certificate for ${HOST} \"(CN: ${COMMONNAME})\" will expire on ${CERTDATE}" + fi + prints "${HOST}" "${PORT}" "Expiring" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}" + RETCODE_LOCAL=1 + + else + prints "${HOST}" "${PORT}" "Valid" "${CERTDATE}" "${CERTDIFF}" "${CERTISSUER}" "${COMMONNAME}" "${SERIAL}" + RETCODE_LOCAL=0 + fi + + set_returncode "${RETCODE_LOCAL}" + MIN_DATE=$(echo "${CERTDATE}" | "${AWK}" '{ print $1, $2, $4 }') + set_summary "${RETCODE_LOCAL}" "${HOST}" "${PORT}" "${MIN_DATE}" "${CERTDIFF}" +} + +################################# +### Start of main program +################################# +while getopts abc:d:e:E:f:hik:nNp:qs:St:Vx: option +do + case "${option}" in + a) ALARM="TRUE";; + b) NOHEADER="TRUE";; + c) CERTFILE=${OPTARG};; + d) CERTDIRECTORY=${OPTARG};; + e) ADMIN=${OPTARG};; + E) SENDER=${OPTARG};; + f) SERVERFILE=$OPTARG;; + h) usage + exit 1;; + i) ISSUER="TRUE";; + k) PKCSDBPASSWD=${OPTARG};; + n) NAGIOS="TRUE";; + N) NAGIOS="TRUE" + NAGIOSSUMMARY="TRUE";; + p) PORT=$OPTARG;; + q) QUIET="TRUE";; + s) HOST=$OPTARG;; + S) VALIDATION="TRUE";; + t) CERTTYPE=$OPTARG;; + V) echo "${PROGRAMVERSION}" + exit 0 + ;; + x) WARNDAYS=$OPTARG;; + \?) usage + exit 1;; + esac +done + +### Check to make sure a openssl utility is available +if [ ! -f "${OPENSSL}" ]; then + echo "ERROR: The openssl binary does not exist in ${OPENSSL}." + echo "FIX: Please modify the \${OPENSSL} variable in the program header." + exit 1 +fi + +### Check to make sure a date utility is available +if [ ! -f "${DATE}" ]; then + echo "ERROR: The date binary does not exist in ${DATE} ." + echo "FIX: Please modify the \${DATE} variable in the program header." + exit 1 +fi + +### Check to make sure a grep and find utility is available +if [ ! -f "${GREP}" ] || [ ! -f "${FIND}" ]; then + echo "ERROR: Unable to locate the grep and find binary." + echo "FIX: Please modify the \${GREP} and \${FIND} variables in the program header." + exit 1 +fi + +### Check to make sure the mktemp and printf utilities are available +if [ ! -f "${MKTEMP}" ] || [ -z "${PRINTF}" ]; then + echo "ERROR: Unable to locate the mktemp or printf binary." + echo "FIX: Please modify the \${MKTEMP} and \${PRINTF} variables in the program header." + exit 1 +fi + +### Check to make sure the sed and awk binaries are available +if [ ! -f "${SED}" ] || [ ! -f "${AWK}" ]; then + echo "ERROR: Unable to locate the sed or awk binary." + echo "FIX: Please modify the \${SED} and \${AWK} variables in the program header." + exit 1 +fi + +### Check to make sure a mail client is available it automated notifications are requested +if [ "${ALARM}" = "TRUE" ] && [ ! -f "${MAIL}" ]; then + echo "ERROR: You enabled automated alerts, but the mail binary could not be found." + echo "FIX: Please modify the ${MAIL} variable in the program header." + exit 1 +fi + +# Send along the servername when TLS is used +if ${OPENSSL} s_client -help 2>&1 | grep '-servername' > /dev/null; then + TLSSERVERNAME="TRUE" +else + TLSSERVERNAME="FALSE" +fi + +# Place to stash temporary files +CERT_TMP=$($MKTEMP /var/tmp/cert.XXXXXX) +ERROR_TMP=$($MKTEMP /var/tmp/error.XXXXXX) + +### Baseline the dates so we have something to compare to +MONTH=$(${DATE} "+%m") +DAY=$(${DATE} "+%d") +YEAR=$(${DATE} "+%Y") +NOWJULIAN=$(date2julian "${MONTH#0}" "${DAY#0}" "${YEAR}") + +### Touch the files prior to using them +if [ -n "${CERT_TMP}" ] && [ -n "${ERROR_TMP}" ]; then + touch "${CERT_TMP}" "${ERROR_TMP}" +else + echo "ERROR: Problem creating temporary files" + echo "FIX: Check that mktemp works on your system" + exit 1 +fi + +### If a HOST was passed on the cmdline, use that value +if [ "${HOST}" != "" ]; then + print_heading + check_server_status "${HOST}" "${PORT:=443}" + print_summary +### If a file is passed to the "-f" option on the command line, check +### each certificate or server / port combination in the file to see if +### they are about to expire +elif [ -f "${SERVERFILE}" ]; then + print_heading + + IFS=$'\n' + for LINE in $(grep -E -v '(^#|^$)' "${SERVERFILE}") + do + HOST=${LINE%% *} + PORT=${LINE##* } + IFS=" " + if [ "$PORT" = "FILE" ]; then + check_file_status "${HOST}" "FILE" "${HOST}" + else + check_server_status "${HOST}" "${PORT}" + fi + done + IFS="${OLDIFS}" + print_summary +### Check to see if the certificate in CERTFILE is about to expire +elif [ "${CERTFILE}" != "" ]; then + print_heading + check_file_status "${CERTFILE}" "FILE" "${CERTFILE}" + print_summary + +### Check to see if the certificates in CERTDIRECTORY are about to expire +elif [ "${CERTDIRECTORY}" != "" ] && ("${FIND}" -L "${CERTDIRECTORY}" -type f > /dev/null 2>&1); then + print_heading + for FILE in $("${FIND}" -L "${CERTDIRECTORY}" -type f); do + check_file_status "${FILE}" "FILE" "${FILE}" + done + print_summary +### There was an error, so print a detailed usage message and exit +else + usage + exit 1 +fi + +### Exit with a success indicator +if [ "${NAGIOS}" = "TRUE" ]; then + exit "${RETCODE}" +else + exit 0 +fi