#!/bin/bash # certShow.sh - Look for SSL expiration time by running parallel requests # (C) 2022-2025 F-Hauri - http://www.f-hauri.ch # Version: 0.0.6 -- Last update: Thu Apr 10 13:12:25 CEST 2025 # Licensed under terms of LGPL v3. www.gnu.org # # This script require bash version >= 5.1 ! # # Sample use: # - Intented to be run periodically (by crontab or other time based # trigger), with `-q` switch: # $ cat /pathTo/siteList.txt | xargs /pathTo/certShow.sh -q # - Show alternatives names for google.com and all certs stored locally. # $ /pathTo/certShow.sh -a google.com /etc/ssl/certs/*.pem : "${maxProc:=10} ${timeFmt:=%F} ${warnDays:=14} ${showAlt:=false} ${delay=0} ${showAllStats:=false} ${showErrStats:=false} ${quiet:=false} ${fancy:=false}" usage() { cat <<-EOF Usage: ${0##*/} [OPTIONS] [IP ADDRESS|HOST|FILE] ... -p Integer Max number of concurrent parallel process to run ($maxProc) -f String Time format string ('$timeFmt') -d Integer Number of days left before warning ($warnDays) -n Float Nice: delay in seconds between requests (implie "-p 1") -a Show alternatives names -e Show error stats -s Show all stats -q Quiet (Don't show line with status ok) -u Use Unicode symbols -h Show this help Note: [-a] implie [-e] switches. EOF } readCmdLine(){ local opt OPTARG local -A 'iStrs=([p]=maxProc [f]=timeFmt [d]=warnDays [a]=showAlt [n]=delay [u]=fancy [s]=showAllStats [e]=showErrStats [q]=quiet )' while getopts "aehqsud:f:n:p:" opt; do case $opt in [dfpn] ) # shellcheck disable=SC2154 # Referenced but not assigned. printf -v "${iStrs["$opt"]}" %s "$OPTARG" ;; [aeqsu] ) printf -v "${iStrs["$opt"]}" %s true ;; h ) usage;exit 0 ;; * ) echo >&2 "ERROR ${0##*/}: Unknow arg $opt" usage;exit 1 ;; esac done } parseLine() { local field content x509fld=$1 local -n varname=$2 while IFS='=' read -r field content;do varname[${field% }]=${content# } done <<<"${x509[$x509fld]//, /$'\n'}" } show1cert() { local dates alts altline sign rescode=0 field content altline alts local -A x509 Subj Isur while IFS='=' read -r field content; do case $field in '' ) ;; ' '*) x509[$prev]+=${field};; *'Subject Alternative Name'* ) prev='Subject Alternative Name';; * ) x509[$field]="$content";prev=${field};; esac done < <( openssl x509 -noout -{dates,issuer,subject,ext} \ subjectAltName -in "$1" 2>/dev/null ) [[ -v x509[subject] ]] || { printf -- '%d|--|--|%b|--|--|%s|--|--\n' "$3" "${symbol[BAD]}" \ "$2" >&$col return 4 } parseLine subject Subj parseLine issuer Isur mapfile -t dates < <( IFS=$'\n';date -f - <<<"${x509[notBefore]}"$'\n'"${x509[notAfter]}" +%s) read -ra alts <<<"${x509[Subject Alternative Name]//,}" alts=("${alts[@]#*:}") altline=${alts[*]} if (( (dates[1]-EPOCHSECONDS) / 86400 < 0 )) ; then printf -v sign %b "${symbol[BAD]}" rescode=2 elif(( (dates[1]-EPOCHSECONDS) / 86400 < warnDays )); then printf -v sign %b "${symbol[WARN]}" rescode=1 else printf -v sign %b "${symbol[OK]}" fi $quiet && (( rescode == 0 )) && return 0 if $showAlt; then printf "%d|%($timeFmt)T|%($timeFmt)T|%s|%s|%s|%s|%s|%s|%s\n" "$3" \ "${dates[@]}" "$sign" $(((dates[1]-EPOCHSECONDS)/86400)) \ $(((EPOCHSECONDS-dates[0])/86400)) "${Subj[CN]}" "${Isur[O]}" \ "${#alts[@]}" "${altline}" >&$col else printf "%d|%($timeFmt)T|%($timeFmt)T|%s|%s|%s|%s|%s|%s\n" "$3" \ "${dates[@]}" "$sign" $(((dates[1]-EPOCHSECONDS)/86400)) \ $(((EPOCHSECONDS-dates[0])/86400)) "${Subj[CN]}" "${Isur[O]}" \ "${#alts[@]}" >&$col fi return $rescode } checkIsIpv4() { # throw an error if not valid IPv4 local _iPointer _i _a _vareq=() for _i; do case $_i in *[^0-9.]* ) return 1 ;; esac read -ra _a <<<"${_i//./ }" [ ${#_a[@]} -eq 4 ] || return 1 for _iPointer in "${_a[@]}"; do (( _iPointer == ( _iPointer & 255 ) )) || return 2 done done } checkIsLabel() { local -a _arr ((${#1}<4 || ${#1}>253)) && return 1 [[ -z ${1//[a-zA-Z0-9.-]} ]] || return 2 [[ -z ${1//.} ]] && return 3 read -ra _arr <<<"${1//./ }" (( ${#_arr[@]} < 2 )) && return 4 : } wait4oneTask(){ local pid thisRes arg start elap wait -np pid "${!spids[@]}" thisRes=$? (( result |= thisRes)) local -n list=res_${thisRes}_list IFS=: read -r arg start <<<"${spids[pid]}" elap=00000$(( ${EPOCHREALTIME/.} - start )) printf -v elap '%dm%07.4fs' $((10#$elap/60000000)) \ $((10#${elap::-6}%60)).${elap: -6} list+=( "${elap#0m0}" "$arg") unset "spids[pid]" case $delay in 0 | '' | . | *[!0-9.]* | *.*.* ) ;; * ) sleep $delay ;; esac } errStats() { $showAllStats || $showErrStats || return local resCodes=( [0]='No error' [1]="Warning: left than $warnDays day" [2]='BAD: end of life reached' [4]='Cannot reach host or address' [8]='Wrong argument' ) _i local -a list=("${!resCodes[@]}") $showAllStats || list=("${list[@]:1}") for _i in "${list[@]}"; do local -n argList=res_${_i}_list [[ -v argList[0] ]] || continue printf 'Rc=%d: %s (%dx)\n' "$_i" "${resCodes[_i]}" $((${#argList[@]}/2)) printf '%11s %s\n' "${argList[@]}" done } out2Col() { local cols=(p 'Not before' 'Not after' s le pa "Common Name" Issuer Alt) cAr $showAlt && cols+=( Alternative\ names ) && shift printf -v cAr '%s,' "${cols[@]}" if $showAlt; then exec {col}> >( sort -t\| -nk 1 | column -t -W Alternative\ names -s\| -R le,pa,Alt -Hp -N "$cAr") else exec {col}> >(sort -t\| -nk 1|column -t -s\| -Rle,pa,Alt -Hp -N "$cAr") fi } showCerts() { local pos=1 arg if read -t 0 _; then mapfile -t stdin2Arry set -- "$@" "${stdin2Arry[@]}" fi if $fancy; then declare -A symbol='([WARN]="\U26A0\UFE0F" [OK]="\U2705" [BAD]="\U274C")' else declare -A symbol='([WARN]="!" [OK]="-" [BAD]="X")' fi case $delay in 0 | '' | . | *[!0-9.]* | *.*.* ) ;; * ) maxProc=1 ;; esac for arg; do if [ -f "$arg" ]; then (( ${#spids[@]} >= maxProc )) && wait4oneTask; show1cert "$arg" "$arg" $pos & spids[$!]=$arg:${EPOCHREALTIME/.} ((pos+=1)) elif checkIsLabel "$arg" || checkIsIpv4 "$arg"; then (( ${#spids[@]} >= maxProc )) && wait4oneTask; show1cert <( openssl s_client -ign_eof -connect "${arg}:443" 2>/dev/null \ <<<$'HEAD / HTTP/1.0\r\nHost: '"$1"$'\r\n\r' ) "$arg" $pos & spids[$!]=$arg:${EPOCHREALTIME/.} ((pos+=1)) else printf -- '%d|--|--|%b|--|--|%s|--|--\n' $((pos++)) \ "${symbol[BAD]}" "$arg" >&$col res_8_list+=(0.0000s "$arg") (( result |= 8 )) fi done while (( ${#spids[@]} )); do wait4oneTask done } readCmdLine "$@" shift $((OPTIND-1)) out2Col result=0 showCerts "$@" exec {col}>&- wait errStats exit $result