# Ulf Tigerstedt <ulf.tigerstedt@csc.fi>
# NGI_FI and NGI_NDGF
# Work in progress since 2011
# secret value 246094


do_hashing=True

import os, re
import glob
import posixpath
try:
        import base64, hashlib
except ImportError:
        do_hashing=False
import datetime
import sys
import logging

log = logging.getLogger()
logging.basicConfig()

igtffilename="ca-policy-egi-core.list"
releasefilename="ca-policy-egi-core.release"
obsoletedfilename="ca-policy-egi-core.obsoleted"
if (len(sys.argv) == 2):
        ca_cert_location=sys.argv[1]
else:
        ca_cert_location = os.getenv('X509_CERT_DIR')
        if ca_cert_location is None:
                print('__status 3 Did not receive a $X509_CERT_DIR, ' \
                      'missing runtime environment?')
                sys.exit(3)
if not os.path.exists(ca_cert_location):
        print('__status 3 Missing directory $X509_CERT_DIR (%s).'%ca_cert_location)
        sys.exit(3)

# What are the thresholds?

days_warning=0
days_critical=7


try:
        igtflist = open(igtffilename, 'r')
except:
        print('__status 3 Missing %s.'%igtffilename)
        sys.exit(3)
try:
        obsoletedlist = open(obsoletedfilename, 'r')
except:
        print('__status 3 Missing %s.'%obsoletedlist)
        sys.exit(3)

currentonlynames=[]
igtfonlynames=[]
obsoletedonlynames=[]

nagios_warning=0
nagios_critical=0
nagios_messages = []

def counted_noun(n, sg, pl = None):
        return '%d %s'%(n, n == 1 and sg or pl or sg + 's')

def short_list(xs, limit = 4):
    if len(xs) <= limit:
        return ', '.join(xs)
    else:
        return ', '.join(xs[0:limit] + ['...'])

def getreleasedateandversion(filename):
        """ Parse a EGI Trustanchor release file
            to get the release date and version 
        """
        try:
                releasefile = open(filename,'r')
        except:
                print("__status 3 Missing trustanchor release file %s."%filename)
                sys.exit(3)
        for line in releasefile:
                if re.match("^.*<Date>",line):
                        [crud,spaces,xml1,fulldate,xml2,crud2]=re.split("^(.*)(<Date>)([0-9]{8})(</Date>).*$",line)
                if re.match("^.*<Version>",line):
                        [crud,spaces,xml1,releaseversion,packaging,xml2,crud2]=re.split("^(.*)(<Version>)([0-9]+.[0-9]+)(-[0-9]+)(</Version>).*$",line)
                        
        return [fulldate,releaseversion]

def getnameversion(filename):
        """ Read the alias and version number from a """
        """ CA distribution .info file               """
        if posixpath.islink(filename):
                return ["","",""]
        caname = ""
        version = ""
        sha1fp0 = ""
        try:
                inf = open(filename,'r')
        except:
                print("__status 3 Missing CA info file %s."%filename)
                sys.exit(3)
        for line in inf:
                if re.match('^alias.*',line):
                        (junk, caname, _) = re.split(r'^alias\s*=\s*(.*)', line)
                        #print caname
                if re.match('^version.*',line):
                        (junk, version, _) = re.split(r'version\s*=\s*([0-9]+.[0-9]+)', line)
                        #print version
                if re.match("^sha1fp\.0",line):
                        (junk, sha1fp0, _) = re.split(r'sha1fp\.0\s*=\s*([0-9A-F:]{59})', line)
                        #print sha1fp0
        if ((caname != "") and (version != "") and (sha1fp0 != "")):
                return [caname,version,sha1fp0]
        return ["","",""]

def load_pem(pem_file):
    with open(pem_file) as fp:
        current_type = None
        current_data = None
        for line in fp:
            line = line.strip()
            if line.startswith('-----BEGIN ') and line.endswith('-----'):
                if not current_type is None:
                    raise ValueError(
                            'Invalid PEM file %s, nested BEGIN.', pem_file)
                current_type = line[11:-5]
                current_data = ''
            elif line.startswith('-----END ') and line.endswith('-----'):
                if not current_type == line[9:-5]:
                    raise ValueError(
                            'Invalid PEM file %s, END %s should be END %s'
                            % (pem_file, line[9:-5], current_type))
                data = base64.b64decode(current_data.encode('ASCII', 'strict'))
                yield (current_type, data)
                current_type = None
                current_data = None
            else:
                if not current_type is None:
                    current_data += line
    if not current_type is None:
        raise ValueError(
                'Invalid PEM file %s, missing END %s.', pem_file, current_type)

def load_pem_cert(pem_file):
    data = [d for t, d in load_pem(pem_file) if t == 'CERTIFICATE']
    if len(data) == 0:
        raise ValueError('%s contains no certificate' % pem_file)
    if len(data) > 1:
        raise ValueError('%s contains multiple certificates' % pem_file)
    return data[0]

def checksha1fp(infofile,infohash):
        """ Hunt down a certificate that matches the .info 
            file (which might be a .0 or a .pem). 
            Then read the data and compute the SHA1 fingerprint
            of it 
        """
        # If this is and old python, don't do hashing
        if not do_hashing:
                return True

        ifile=""
        newfilepem = re.sub('\.info$','.pem',infofile)
        newfile0 = re.sub('\.info$','.0',infofile)
        if (posixpath.exists(newfilepem)):
                ifile=newfilepem
        elif (posixpath.exists(newfile0)):
                ifile=newfile0
        if (ifile == ""):
                return False
        cleanedhash = re.sub(':','',infohash.lower())
        try:
                data = load_pem_cert(ifile)
        except Exception as xc:
                log.warn('%s: %s'%(ifile, xc))
                return False
        hash = hashlib.sha1(data)
        if (hash.hexdigest() == cleanedhash):
                return True
        else:
                return False


[releasedate,releaseversion] = getreleasedateandversion(releasefilename)

for line in igtflist:
        if re.match('^ca_.*',line):
                [nonce, ca, caname, version, _endline] = re.split('(^ca_)(.*)-([0-9]+.[0-9]+-[0-9]+)$',line)
                igtfonlynames.append(caname)

for line in obsoletedlist:
        if re.match('^[A-Za-z0-9].*',line):
                [nonce, obsolete, endline] = re.split('(^[A-Za-z0-9].*$)',line)
                obsoletedonlynames.append(obsolete)


[junk,ryear,rmonth,rdate,_] = re.split("([0-9]{4})([0-9]{2})([0-9]{2})",releasedate)

today=datetime.date.today()
releasedate=datetime.date(int(ryear),int(rmonth),int(rdate))
difference=today-releasedate

allinfos=glob.glob(ca_cert_location+'/*.info')

present_obsolete = []
present_by_version = {}
for cfile in allinfos:
        [caname,version,sha1fp0]=getnameversion(cfile);
        if (caname != ""):
                currentonlynames.append(caname)
                if ((caname in igtfonlynames) and (difference.days > days_warning) and (version < releaseversion)):
                        if (difference.days > days_warning):
                                nagios_warning += 1
                        if (difference.days > days_critical):
                                nagios_critical += 1
                        if not version in present_by_version:
                                present_by_version[version] = []
                        present_by_version[version].append(caname)
                if (caname in obsoletedonlynames):
                        if (difference.days > days_warning):
                                nagios_warning += 1
                        present_obsolete.append(caname)

                result=checksha1fp(cfile,sha1fp0)
                if (not result):
                        nagios_critical += 1
                        nagios_messages.append("SHA Fingerprint failed for %s."%caname)

nagios_issues = []
for version, canames in present_by_version.items():
        nagios_issues.append(
                'found %s from %s (%s)'
                % (counted_noun(len(canames), 'CA'), version,
                   short_list(canames)))
        log.error('CAs from %s: %s'%(version, ', '.join(canames)))
if present_obsolete:
        nagios_issues.append(
                '%s obsolete (%s)'
                % (counted_noun(len(present_obsolete), 'CA is', 'CAs are'),
                   short_list(present_obsolete)))
        log.error('Obsolete CAs: %s'%', '.join(present_obsolete))

missingcas=[i for i in igtfonlynames if i not in currentonlynames]

if (len(missingcas) > 0):
        if (difference.days > days_warning):
                nagios_warning += 1
        nagios_issues.append(
            'missing %s (%s)'
            % (counted_noun(len(missingcas), 'CA'),
               short_list(missingcas)))
        log.error('Missing CAs: %s'%', '.join(missingcas))

if not nagios_issues:
        nagios_issues.append('all present')

status = nagios_critical and 2 or nagios_warning and 1 or 0
msg = ', '.join(nagios_issues)
print("__status %d IGTF-%s, %s old, %s." \
      % (status, releaseversion, counted_noun(difference.days, 'day'), msg))
for msg in nagios_messages:
        print("__log 40 %s" % msg)
