#!/usr/bin/python3
SVER = '3.0.5-1'

##############################################################################
# scatool - Supportconfig Analysis (SCA) Tool
# Copyright (c) 2024 SUSE LLC
#
# Description:  Analyzes supportconfig archives for known issues
# Modified:     2024 Oct 07
#
##############################################################################
#
#  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; version 2 of the License.
#
#  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.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, see <http://www.gnu.org/licenses/>.
#
#  Authors/Contributors:
#    Jason Record <jason.record@suse.com>
#
##############################################################################

import os
import re
import sys
import json
import signal
import shutil
import datetime
import socket
import getopt
import smtplib
import subprocess
import configparser
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

config_file = '/etc/sca/scatool.conf'
width = 0
description_width = 0
progress_bar_active = True

def separator_line(use_char = '#'):
    print("{}".format(use_char*width))

def title():
    separator_line()
    print("#   SCA Tool v" + SVER)
    separator_line()
    print()

def usage():
    display = "  {:33} {}"
    print("Usage: scatool [OPTIONS] [/path/to/supportconfig]")
    print()
    print("OPTIONS")
    print(display.format('-h, --help', "Displays this screen"))
    print(display.format('-b, --batch', "Batch mode that disables the progress bar"))
    print(display.format('-d, --debug', "Use log level 4"))
    print(display.format('-e <list>, --email <list>', "Send the SCA Report file to email address(es) provided. Comma separated list"))
    print(display.format('-j, --json', "Set report --type json"))
    print(display.format('-n, --normal', "Use log level 2, default log level: Minimal"))
    print(display.format('-o <path>, --output <path>', "SCA Report file output directory"))
    print(display.format('-p, --summary', "Print a pattern summary"))
    print(display.format('-q, --quiet', "Use log level 0"))
    print(display.format('-r, --remove', "Remove archive files leaving only the SCA Report file. Ignored in debug mode."))
    print(display.format('-s, --server', "Analyze the local server. Root access needed."))
    print(display.format('-t, --type', "SCA Report file output type, options: json, html, all. Default: html"))
    print(display.format('-v, --verbose', "Use log level 3"))
    print(display.format('-x <path>, --extract_here <path>', "Extract any supportconfig archives to this alternate extraction directory"))
#    print(display.format('-l <level>, --log_level <level>', "Set log level, default: Minimal"))
#    print(display.format('', "0 Quiet, 1 Minimal, 2 Normal, 3 Verbose, 4 Debug"))
    print()

def option_error(msg):
    print(msg)
    print()
    usage()
    sys.exit(1)

def signal_handler(sig, frame):
    print("\n\nAborting...\n")
    sys.exit(1)

def config_entry(_entry, trailer = ''):
    formatted_entry = _entry.strip('\"\'')
    if( len(trailer) > 0 ):
        if len(formatted_entry) > 0:
            if not formatted_entry.endswith(trailer):
                formatted_entry = formatted_entry + str(trailer)
    return formatted_entry

class ProgressBar():
    """Initialize and update progress bar class"""

    def __init__(self, prefix, total):
        self.base_len = int(width)
        self.desc_width = int(description_width) + 1
        self.bar_width = self.base_len
        self.prefix = prefix
        self.prefix_size = len(self.prefix)
        self.total = int(total)
        self.count = 0
        self.out = sys.stdout
        if self.prefix_size > self.desc_width:
            self.bar_width = self.base_len - self.prefix_size - 2
        else:
            self.bar_width = self.base_len - self.desc_width - 2
        self.display = "{:" + str(self.desc_width) + "s}[{}{}] {:3g}% {:3g}/{}"

    def __str__(self):
        return 'class %s(\n  prefix=%r \n  bar_width=%r \n  total=%r\n)' % (self.__class__.__name__, self.prefix, self.bar_width, self.total)

    def set_prefix(self, _prefix):
        self.prefix = _prefix
        if ( self.bar_width_orig == self.base_len ):
            self.bar_width = self.base_len - self.prefix_size - 2
        else:
            self.bar_width = self.bar_width_orig

    def set_total(self, _new_total):
        self.total = _new_total

    def inc_count(self, increment = 1):
        """Increments one by default"""
        if self.count < self.total:
            self.count += increment

    def get_total(self):
        return self.total

    def get_count(self):
        return self.count

    def update(self):
        percent_complete = int(100*self.count/self.total)
        current_progress = int(self.bar_width*self.count/self.total)
        print(self.display.format(self.prefix, "#"*current_progress, "."*(self.bar_width-current_progress), percent_complete, self.count, self.total), end='\r', file=self.out, flush=True)

    def finish(self):
        if self.count != self.total:
            self.count = self.total
            self.update()
        print("", flush=True, file=self.out)

class DisplayMessages():
    "Display message string for a given log level"
    LOG_QUIET = 0    # turns off messages
    LOG_MIN = 1    # minimal messages
    LOG_NORMAL = 2    # normal, but significant, messages
    LOG_VERBOSE = 3    # detailed messages
    LOG_DEBUG = 4    # debug-level messages
    LOG_LEVELS = {0: "Quiet", 1: "Minimal", 2: "Normal", 3: "Verbose", 4: "Debug" }

    def __init__(self):
        self.level = self.LOG_MIN # instance default
        self.desc_width = 30 # instance default
        self.msg_display = "{:" + str(self.desc_width) + "}"
        self.msg_display_pair = self.msg_display + " = {}"

    def __str__ (self):
        return "class %s(level=%r)" % (self.__class__.__name__,self.level)

    def set_width(self, width_value):
        self.desc_width = width_value
        self.msg_display = "{:" + str(self.desc_width) + "}"
        self.msg_display_pair = self.msg_display + " = {}"

    def get_level(self):
        return self.level

    def get_level_str(self):
        return self.LOG_LEVELS[self.level]

    def set_level(self, level):
        if( level >= self.LOG_DEBUG ):
            self.level = self.LOG_DEBUG
        else:
            self.level = level

    def validate_level(self, level):
        validated_level = -1
        if( level.isdigit() ):
            validated_level = int(level)
        else:
            argstr = level.lower()
            if( argstr.startswith("qui") ):
                validated_level = self.LOG_QUIET
            elif( argstr.startswith("min") ):
                validated_level = self.LOG_MIN
            elif( argstr.startswith("norm") ):
                validated_level = self.LOG_NORMAL
            elif( argstr.startswith("verb") ):
                validated_level = self.LOG_VERBOSE
            elif( argstr.startswith("debug") ):
                validated_level = self.LOG_DEBUG

        return validated_level


    def __write_paired_msg(self, level, msgtag, msgstr):
        if( level <= self.level ):
            print(self.msg_display_pair.format(msgtag, msgstr))

    def __write_msg(self, level, msgtag):
        if( level <= self.level ):
            print(self.msg_display.format(msgtag))

    def quiet(self, msgtag = None, msgstr = None):
        "Write messages even if quiet is set"
        if msgtag:
            if msgstr:
                self.__write_paired_msg(self.LOG_QUIET, msgtag, msgstr)
            else:
                self.__write_msg(self.LOG_QUIET, msgtag)
        else:
            if( self.level >= self.LOG_QUIET ):
                print()

    def min(self, msgtag = None, msgstr = None):
        "Write the minium amount of messages"
        if msgtag:
            if msgstr:
                self.__write_paired_msg(self.LOG_MIN, msgtag, msgstr)
            else:
                self.__write_msg(self.LOG_MIN, msgtag)
        else:
            if( self.level >= self.LOG_MIN ):
                print()

    def normal(self, msgtag = None, msgstr = None):
        "Write normal, but significant, messages"
        if msgtag:
            if msgstr:
                self.__write_paired_msg(self.LOG_NORMAL, msgtag, msgstr)
            else:
                self.__write_msg(self.LOG_NORMAL, msgtag)
        else:
            if( self.level >= self.LOG_NORMAL ):
                print()

    def verbose(self, msgtag = None, msgstr = None):
        "Write more verbose informational messages"
        if msgtag:
            if msgstr:
                self.__write_paired_msg(self.LOG_VERBOSE, msgtag, msgstr)
            else:
                self.__write_msg(self.LOG_VERBOSE, msgtag)
        else:
            if( self.level >= self.LOG_VERBOSE ):
                print()

    def debug(self, msgtag = None, msgstr = None):
        "Write all messages, including debug level"
        if msgtag:
            updated_msgtag = "+ " + msgtag
            if msgstr:
                self.__write_paired_msg(self.LOG_DEBUG, updated_msgtag, msgstr)
            else:
                self.__write_msg(self.LOG_DEBUG, updated_msgtag)
        else:
            if( self.level >= self.LOG_DEBUG ):
                print()

    def separator(self, required_level, use_char = '#'):
        if self.level >= required_level:
            print("{}".format(use_char*width))



class SupportconfigAnalysis():
    '''
    Gathers information about the supportconfig. The information includes:
        Server, SUSE distribution, targeted OS products installed, applicable patterns, applicable pattern results
    '''
    REQUIRED_ELEMENTS = ["META_CLASS", "META_CATEGORY", "META_COMPONENT", "PATTERN_ID", "PRIMARY_LINK", "OVERALL", "OVERALL_INFO", "META_LINK_"]
    REQUIRED_JSON_KEYS = ['generation', 'class', 'category', 'component', 'id', 'primary_solution', 'severity', 'description', 'solution_links']
    REQUIRED_ELEMENT_CONV = {
    'META_CLASS': 'class', 
    'META_CATEGORY': 'category', 
    'META_COMPONENT': 'component', 
    'PATTERN_ID': 'id', 
    'PRIMARY_LINK': 'primary_solution', 
    'OVERALL': 'severity', 
    'OVERALL_INFO': 'description'
    }
    SEV_TABLE = {-2: 'temp', -1: 'partial', 0: 'success', 1: 'recommend', 2: 'promotion', 3: 'warning', 4: 'critical', 5: 'error', 6: 'ignore' }
    GITHUB_OWNER = 'https://github.com/openSUSE/'

    def __init__(self, msg, config, extracted_path):
        self.msg = msg
        self.config = config
        self.json_data = {}
        self.sca_library_path = config_entry(config.get("Common", "sca_library_path"), '/')
        self.sca_patterns_path = config_entry(config.get("Common", "sca_pattern_path"), '/')
        self.github_base = config_entry(config.get("Supportconfig", "github_base"), '/')
        self.location = extracted_path
        self.analysis_datetime = datetime.datetime.now()
        self.distro_info = {'valid': True, 'serverName': 'Unknown', 'hardWare': 'Unknown', 'virtualization': 'None', 'Summary': '', 'timeArchiveRun': "0000-00-00 00:00:00"}
        self.distro_info['timeAnalysis'] = str(self.analysis_datetime.year) + "-" + str(self.analysis_datetime.month).zfill(2) + "-" + str(self.analysis_datetime.day).zfill(2) + " " + str(self.analysis_datetime.hour).zfill(2) + ":" + str(self.analysis_datetime.minute).zfill(2) + ":" + str(self.analysis_datetime.second).zfill(2)

        self.required_element_count = len(self.REQUIRED_ELEMENTS)
        self.results = {}
        self.supportconfig_keys = []
        self.runtime_error_list = []
        self.pattern_stats = {
            'total': 0,
            'applicable': 0,
            'applied': 0,
            'runtime_errors': 0,
            self.SEV_TABLE[-2]: 0,
            self.SEV_TABLE[-1]: 0,
            self.SEV_TABLE[0]: 0,
            self.SEV_TABLE[1]: 0,
            self.SEV_TABLE[2]: 0,
            self.SEV_TABLE[3]: 0,
            self.SEV_TABLE[4]: 0,
            self.SEV_TABLE[5]: 0,
            self.SEV_TABLE[6]: 0,
            'filter': []
        }
        self.products_list = self.__get_products_list()
        if self.distro_info['valid']:
            self.all_patterns = self.__get_available_patterns()
            self.pattern_filtered_directories = self.__get_pattern_filtered_directories()
            self.applicable_patterns = self.__get_applicable_patterns()
            self.__apply_patterns()

    # runpats gets the pattern results. runpats calls patternPreProcessor
    # getHtml(htmlOutputFile, self.location, supportconfigPath.split("/")[-1])
    # getHtml calls getClasses
    # email the report file
    # delete the archive and directory as needed


    def __str__ (self):
        pattern = '''
Class instance of {}
  location = {}
  distro_info = {}\n
'''
        return pattern.format(self.__class__.__name__, self.location, self.distro_info)

    def get_results(self):
        return self.json_data

    def is_valid(self):
        return self.distro_info['valid']

    def __get_pattern_source_url(self, pat):
        # relative_pattern_path should start with 'patterns/'
        relative_pattern_path = pat.replace(self.sca_library_path, '')
        github_repo_name = ''
        if '/ALP/' in relative_pattern_path:
            if 'alp1' in relative_pattern_path:
                github_repo_name = 'sca-patterns-alp1'
        elif '/SLE/' in relative_pattern_path:
            if 'sle16' in relative_pattern_path:
                github_repo_name = 'sca-patterns-alp1'
            elif 'sle15' in relative_pattern_path:
                github_repo_name = 'sca-patterns-sle15'
            elif 'sle12' in relative_pattern_path:
                github_repo_name = 'sca-patterns-sle12'
            elif 'sle11' in relative_pattern_path:
                github_repo_name = 'sca-patterns-sle11'
            elif 'sle10' in relative_pattern_path:
                github_repo_name = 'sca-patterns-sle10'
            elif 'sle9' in relative_pattern_path:
                github_repo_name = 'sca-patterns-sle09'
        elif '/HAE/' in relative_pattern_path:
            github_repo_name = 'sca-patterns-hae'

        if len(github_repo_name) > 0:
            pattern_source_url = self.github_base  + github_repo_name + '/blob/master/' + relative_pattern_path
        else:
            pattern_source_url = self.github_base

        return pattern_source_url
        

    def __parse_results_output(self, out, error, pat):
        output = {}
        error_display = "{} -- {}: {}"
        output['valid'] = True
        output['error_tag'] = 'None'
        output['error_str'] = 'None'
        output['output_str'] = str(out)
        json_data = True

        try:
            json_object = json.loads(out)
        except ValueError as e:
            json_data = False

        missing_json_keys = []
        if json_data:
            msg.debug("+ Processing JSON output data")
            for key in self.REQUIRED_JSON_KEYS:
                if not key in json_object:
                    missing_json_keys.append(key)
            if len(missing_json_keys) > 0:
                output['valid'] = False
                output['error_tag'] = 'Missing JSON keys'
                output['error_str'] = ' '.join(missing_json_keys)
                self.runtime_error_list.append(error_display.format(pat, output['error_tag'], output['error_str']))
                self.pattern_stats['runtime_errors'] += 1
            else:
                dict1 = output
                output = {**dict1, **json_object}
                output['severity_str'] = self.SEV_TABLE[output['severity']]
                self.pattern_stats[self.SEV_TABLE[output['severity']]] += 1
                if output['severity'] >= 0 and output['severity'] < 5:
                    self.pattern_stats['applied'] += 1
            self.results[pat] = output
        else:
            msg.debug("Processing pipe separated output data")
            output['generation'] = 1
            output['solution_links'] = {}
            if error == "":
                pattern_return_list = out.strip().split("|")

                if( len(pattern_return_list) < self.required_element_count ):
                    output['valid'] = False
                    output['error_tag'] = 'Insufficient output elements'
                    output['error_str'] = "{} or more needed".format(self.required_element_count)
                    self.runtime_error_list.append(error_display.format(pat, output['error_tag'], output['error_str']))
                    self.pattern_stats['runtime_errors'] += 1
                    self.results[pat] = output
                    return False

                missing_elements = []
                for output_element in pattern_return_list:
                    found = False
                    for i in range(0, self.required_element_count):
                        if output_element.startswith(self.REQUIRED_ELEMENTS[i]):
                            found = True
                    if not found:
                        output['valid'] = False
                        output['error_tag'] = 'Invalid output element'
                        output['error_str'] = output_element
                        self.runtime_error_list.append(error_display.format(pat, output['error_tag'], output['error_str']))
                        self.pattern_stats['runtime_errors'] += 1
                        self.results[pat] = output
                        return False

                for o in pattern_return_list:
                    key, value = o.split("=", 1)
                    if key.startswith('META_LINK_'):
                        tag = key.replace('META_LINK_', '')
                        output['solution_links'][tag] = value
                    else:
                        output[self.REQUIRED_ELEMENT_CONV[key]] = value
                output['severity'] = int(output['severity'])
                output['severity_str'] = self.SEV_TABLE[output['severity']]
                output['primary_solution'] = output['primary_solution'].replace('META_LINK_', '')
                self.pattern_stats[self.SEV_TABLE[output['severity']]] += 1
                if output['severity'] >= 0 and output['severity'] < 5:
                    self.pattern_stats['applied'] += 1
                self.results[pat] = output
            else:
                output['valid'] = False
                output['error_tag'] = 'Output error'
                output['error_str'] = error
                self.runtime_error_list.append(error_display.format(pat, output['error_tag'], output['error_str']))
                self.pattern_stats['runtime_errors'] += 1
                self.results[pat] = output

        return output['valid']


    def __apply_patterns(self):
        pattern_count = 0
        output = {}
        self.pattern_stats['applicable'] = len(self.applicable_patterns)
        verbose_line = '{0:6} {1:>5} of {2} {3}'
        pattern_skipped = False

        self.msg.min('Pattern Filter', ' '.join(self.pattern_stats['filter']))

        if( msg.get_level() >= msg.LOG_VERBOSE ):
            msg.verbose('Analyzing Supportconfig', 'In Progress')
        elif( msg.get_level() >= msg.LOG_MIN ):
            if progress_bar_active:
                ascbar = ProgressBar("Analyzing Supportconfig:", self.pattern_stats['applicable'])
            else:
                msg.min('Analyzing Supportconfig', 'In Progress')

        for test_pattern, generational_value in self.applicable_patterns.items():
            pattern_count += 1
            try:
                if test_pattern.endswith("README"):
                    pattern_skipped = True
                else:
                    if generational_value > 1:
                        cmd = [test_pattern, self.location]
                    elif generational_value == 1:
                        cmd = [test_pattern, "-p", self.location]
                    else:
                        output['valid'] = False
                        output['error_tag'] = 'Pattern error'
                        output['error_str'] = 'Invalid pattern generational value: {}'.format(generational_value)
                        self.results[test_pattern] = output
                        self.results[test_pattern]['source'] = self.__get_pattern_source_url(test_pattern)
                        self.runtime_error_list.append(test_pattern + "{0} -- {1}: {2}".format(test_pattern, output['error_tag'], output['error_str']))
                        self.pattern_stats['runtime_errors'] += 1
                        self.msg.verbose(verbose_line.format('ERROR:', pattern_count, self.pattern_stats['applicable'], self.runtime_error_list[-1]))
                        continue

                    self.msg.debug()
                    self.msg.debug('Process Command', ' '.join(cmd))
                    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
                    out, error = p.communicate()
                    pattern_is_valid = self.__parse_results_output(out, error, test_pattern)
                    self.results[test_pattern]['source'] = self.__get_pattern_source_url(test_pattern)

                #call parseOutput to see if output was expected
                if( msg.get_level() >= msg.LOG_VERBOSE ):
                    if pattern_skipped:
                        self.msg.verbose(verbose_line.format('Skip:', pattern_count, self.pattern_stats['total'], test_pattern))
                        pattern_skipped = False
                    else:
                        if pattern_is_valid:
                            self.msg.debug("+ Output", self.results[test_pattern]['output_str'])
                            self.msg.verbose(verbose_line.format('Done:', pattern_count, self.pattern_stats['applicable'], test_pattern))
                        else:
                            self.msg.debug("+ Output", self.results[test_pattern]['error_str'])
                            self.msg.verbose(verbose_line.format('ERROR:', pattern_count, self.pattern_stats['applicable'], test_pattern + " - " + str(self.results[test_pattern]['error_tag'])))
                elif( msg.get_level() >= msg.LOG_MIN ):
                    if progress_bar_active:
                        ascbar.inc_count()
                        ascbar.update()
            except Exception as e:
                output['valid'] = False
                output['error_tag'] = 'Runtime error'
                output['error_str'] = str(e)
                self.results[test_pattern] = output
                self.results[test_pattern]['source'] = self.__get_pattern_source_url(test_pattern)
                self.runtime_error_list.append(test_pattern + "{0} -- {1}: {2}".format(test_pattern, output['error_tag'], output['error_str']))
                self.pattern_stats['runtime_errors'] += 1
                self.msg.verbose(verbose_line.format('ERROR:', pattern_count, self.pattern_stats['applicable'], self.runtime_error_list[-1]))

        #make output look nice
        if( msg.get_level() > msg.LOG_QUIET and msg.get_level() <= msg.LOG_NORMAL ):
            if progress_bar_active:
                ascbar.finish()

        self.msg.verbose()
        self.msg.separator(msg.LOG_VERBOSE, '-')
        self.msg.normal('Total Patterns', str(self.pattern_stats['total']))
        self.msg.normal('Patterns Evaluated', str(self.pattern_stats['applicable']))
        self.msg.normal('- Critical', str(self.pattern_stats['critical']))
        self.msg.normal('- Warning', str(self.pattern_stats['warning']))
        self.msg.normal('- Recommended', str(self.pattern_stats['recommend']))
        self.msg.verbose('- Promotion', str(self.pattern_stats['promotion']))
        self.msg.normal('- Success', str(self.pattern_stats['success']))
        self.msg.verbose('- Error', str(self.pattern_stats['error']))
        self.msg.verbose('- Ignore', str(self.pattern_stats['ignore']))
        self.msg.verbose('- Partial', str(self.pattern_stats['partial']))
        self.msg.verbose('- Temporary', str(self.pattern_stats['temp']))
        self.msg.verbose('- Exec Errors', str(self.pattern_stats['runtime_errors']))
        self.msg.min('Applicable to Server', str(self.pattern_stats['applied']))
        level_now = msg.get_level()
        if( level_now >= msg.LOG_MIN ):
            if self.pattern_stats['runtime_errors'] > 0:
                self.msg.min('Pattern Execution Errors:', str(self.pattern_stats['runtime_errors']))
                self.msg.separator(self.msg.LOG_MIN, '-')
                for pattern_error_str in self.runtime_error_list:
                    print(pattern_error_str)
                    print()
        self.json_data['pattern_stats'] = self.pattern_stats
        self.json_data['results'] = self.results

    def __get_available_patterns(self):
        file_list = []
        for root, dirs, files in os.walk(self.sca_patterns_path):
            for name in files:
                if "README" in name:
                    continue
                else:
                    file_list.append(os.path.join(root, name))
        file_list.sort()
        self.pattern_stats['total'] = len(file_list)
        return file_list

    def __get_pattern_filtered_directories(self):
        pattern_directories = [self.sca_patterns_path + 'local/']
        for product_included in self.products_list:
            base_pattern_path = str(self.sca_patterns_path) + str(product_included['patternTag']) + "/"
            pattern_filter_path = base_pattern_path + str(product_included['use_tag']) + str(product_included['use_vermajor']) + "all/"
            if os.path.isdir(pattern_filter_path):
                pattern_directories.append(pattern_filter_path)
            pattern_filter_path = base_pattern_path + str(product_included['use_tag']) + str(product_included['use_vermajor']) + "sp" + str(product_included['use_verminor']) + "/"
            if os.path.isdir(pattern_filter_path):
                pattern_directories.append(pattern_filter_path)

        pattern_directories = list(set(pattern_directories)) #create a unique sorted list
        self.msg.debug('Pattern Filtered Directories', ' '.join(pattern_directories))
        pattern_definition_filter = []
        for test_directory in pattern_directories:
            pattern_definition_filter.append(test_directory.split("/")[-2])
        self.pattern_stats['filter'] = sorted(pattern_definition_filter)

        return pattern_directories

    def __get_applicable_patterns(self):
        pattern_file_dict = {}
        scapattern_gen1 = re.compile('^Core.init\(META_CLASS|^\@PATTERN_RESULTS = \(', re.IGNORECASE)
        scapattern_gen2 = re.compile('SCAPatternGen2\(')
        for check_pattern in self.all_patterns:
            for filtered_directory in self.pattern_filtered_directories:
                if filtered_directory in check_pattern:
                    generational_value = -1
                    with open(check_pattern) as f:
                        for line in f:
                            if scapattern_gen2.search(line):
                                generational_value = 2
                            elif scapattern_gen1.search(line):
                                generational_value = 1
                    pattern_file_dict[check_pattern] = generational_value
        return pattern_file_dict

    def __get_products_summary_simple(self, this_pattern_tag, this_tag, re_name, summary_file):
        # Extracts product information in the summary.xml file from supportconfig
        # Pattern directory simple SP format: patterns/<patternTag>/<tag><vermajor>sp<verminor>
        re_start = re.compile(r'<product\s|<product>', re.IGNORECASE)
        re_end = re.compile(r'</product>', re.IGNORECASE)
        re_version = re.compile(r'<version>.*</version>', re.IGNORECASE)
        these_products = []
        in_product = False
        summary_info = {
            'patternTag': this_pattern_tag, # Used to filter patterns for analysis
            'tag': this_tag,                # The product's identity tag
            'vermajor': '',                 # The product's major version
            'verminor': '',                 # The product's minor version
            'use_tag': this_tag,            # The tag used to select patterns
            'use_vermajor': '',             # The major version used to select patterns
            'use_verminor': '',             # The minor version used to select patterns
            'supportconfigKey': '',         # Identifies the current supportconfig product
            'nameTag': 'Product:',          # HTML report's nameTag, name pair
            'name': '',                     # HTML report's nameTag, name pair 
            'versionTag': 'Version:',       # HTML report's versionTag, version pair
            'version': '',                  # HTML report's versionTag, version pair
            }
        for line in summary_file:
            if( in_product ):
                if re_end.search(line):
                    in_product = False
                elif re_name.search(line):
                    try:
                        summary_info['name'] = re.search(r'>(.+?)<', line).group(1).replace('-', ' ')
                    except:
                        True
                elif re_version.search(line):
                    try:
                        summary_info['version'] = re.search(r'>(.+?)<', line).group(1)
                        if( "." in summary_info['version'] ):
                            (summary_info['vermajor'], summary_info['verminor']) = summary_info['version'].split(".")
                        else:
                            summary_info['vermajor'] = summary_info['version']
                            summary_info['verminor'] = "0"
                    except:
                        True
                if( summary_info['name'] and summary_info['version'] ):
                    in_product = False
                    summary_info['use_vermajor'] = summary_info['vermajor']
                    summary_info['use_verminor'] = summary_info['verminor']
                    summary_info['supportconfigKey'] = str(summary_info['tag']) + str(summary_info['vermajor']) + "sp" + str(summary_info['verminor'])
                    self.supportconfig_keys.append(summary_info['supportconfigKey'])
                    these_products.append(summary_info)
                    break
            elif re_start.search(line):
                in_product = True
        return these_products

    def __get_products_summary_flat(self, this_pattern_tag, this_tag, use_this_tag, re_name, summary_file):
        # Extracts product information in the summary.xml file from supportconfig
        # Pattern directory flat format: patterns/<patternTag>/<use_tag><vermajor><verminor>
        # Support
        re_start = re.compile(r'<product\s|<product>', re.IGNORECASE)
        re_end = re.compile(r'</product>', re.IGNORECASE)
        re_version = re.compile(r'<version>.*</version>', re.IGNORECASE)
        these_products = []
        in_product = False
        summary_info = {
            'patternTag': this_pattern_tag, # Used to filter patterns for analysis
            'tag': this_tag,                # The product's identity tag
            'vermajor': '',                 # The product's major version
            'verminor': '',                 # The product's minor version
            'use_tag': use_this_tag,        # The tag used to select patterns
            'use_vermajor': '',             # The major version used to select patterns
            'use_verminor': '',             # The minor version used to select patterns
            'supportconfigKey': '',         # Identifies the current supportconfig product
            'nameTag': 'Product:',          # HTML report's nameTag, name pair
            'name': '',                     # HTML report's nameTag, name pair 
            'versionTag': 'Version:',       # HTML report's versionTag, version pair
            'version': '',                  # HTML report's versionTag, version pair
            }
        for line in summary_file:
            if( in_product ):
                if re_end.search(line):
                    in_product = False
                elif re_name.search(line):
                    try:
                        summary_info['name'] = re.search(r'>(.+?)<', line).group(1).replace('-', ' ')
                    except:
                        True
                elif re_version.search(line):
                    try:
                        summary_info['version'] = re.search(r'>(.+?)<', line).group(1)
                        if( "." in summary_info['version'] ):
                            (summary_info['vermajor'], summary_info['verminor']) = summary_info['version'].split(".")
                        else:
                            summary_info['vermajor'] = summary_info['version']
                            summary_info['verminor'] = "0"
                    except:
                        True
                if( summary_info['name'] and summary_info['version'] ):
                    in_product = False
                    summary_info['use_vermajor'] = summary_info['vermajor']
                    summary_info['use_verminor'] = summary_info['verminor']
                    summary_info['supportconfigKey'] = str(summary_info['tag']) + str(summary_info['vermajor']) + str(summary_info['verminor'])
                    self.supportconfig_keys.append(summary_info['supportconfigKey'])
                    these_products.append(summary_info)
                    break
            elif re_start.search(line):
                in_product = True
        return these_products

    def __get_products_list(self):
        products_found = []

        #load summary.xml
        try:
            with open(self.location + "/summary.xml") as f:
                summary_file = f.read().splitlines()
                f.close()
        except:
            summary_file = []

        #detect SLE for VMWARE
        product_name = re.compile(r'<summary>SUSE Linux Enterprise Server .* for VMware</summary>', re.IGNORECASE)
        product_list = self.__get_products_summary_simple('VMware', 'vmw', product_name, summary_file)
        products_found = products_found + product_list

        #detect SLE for SAP
        product_name = re.compile(r'<summary>SUSE LINUX Enterprise Server for SAP Applications.*</summary>', re.IGNORECASE)
        product_list = self.__get_products_summary_simple('SAP', 'sap', product_name, summary_file)
        products_found = products_found + product_list

        #get HAE information
        product_name = re.compile(r'<summary>SUSE Linux Enterprise High Availability Extension.*</summary>', re.IGNORECASE)
        product_list = self.__get_products_summary_simple('HAE', 'hae', product_name, summary_file)
        products_found = products_found + product_list

        #get SUSE Manager Server information
        product_name = re.compile(r'<summary>SUSE Manager Server.*</summary>', re.IGNORECASE)
        product_list = self.__get_products_summary_flat('suma', 'sumasrv', 'suma', product_name, summary_file)
        products_found = products_found + product_list
        
        #get SUSE Manager Retail Branch Server information
        product_name = re.compile(r'<summary>SUSE Manager Retail Branch Server.*</summary>', re.IGNORECASE)
        product_list = self.__get_products_summary_flat('suma', 'sumarbs', 'suma', product_name, summary_file)
        products_found = products_found + product_list
        
        # email_to DO
        del summary_file


        #load basic-environment.txt
        try:
            with open(self.location + "/basic-environment.txt") as f:
                basic_env_file = f.read().splitlines()
                f.close()
        except:
            basic_env_file = []

        product_info = {
            'patternTag': 'Unknown',        # Used to filter patterns for analysis, format: patterns/<patternTag>/<use_tag><use_vermajor>sp<use_verminor>
            'tag': 'Unknown',               # The product's identity tag
            'vermajor': '',                 # The product's major version
            'verminor': '',                 # The product's minor version
            'use_tag': 'Unknown',           # The tag used to select patterns
            'use_vermajor': '',             # The major version used to select patterns
            'use_verminor': '',             # The minor version used to select patterns
            'supportconfigKey': '',         # Identifies the current supportconfig product
            'nameTag': 'Distribution:',     # HTML report's nameTag, name pair
            'name': '',                     # HTML report's nameTag, name pair 
            'versionTag': 'Service Pack:',  # HTML report's versionTag, version pair
            'version': '',                  # HTML report's versionTag, version pair
            }

        #read basic-environment line by line to pull out data.
        in_date = False
        in_uname = False
        in_os_release = False
        in_suse_release = False
        for line in basic_env_file:
            if "Script Version:" in line:
                self.distro_info['supportconfigVersion'] = line.split(':')[-1].strip()
            elif line.startswith("Hardware:"):
                self.distro_info['hardWare'] = line.split(":")[1].strip()
            elif line.startswith("Hypervisor:"):
                self.distro_info['virtualization'] = line.split(":")[1].strip()
            elif line.startswith("Identity:"):
                self.distro_info['vmIdentity'] = line.split(":")[1].strip()
            elif "/bin/date" in line:
                in_date = True
            elif "/bin/uname -a" in line:
                in_uname = True
            elif "/etc/os-release" in line:
                in_os_release = True
            elif( in_date ):
                if "#==[" in line:
                    in_date = False
                else:
                    dateLine = line
                    dateLine = re.sub("\s+", " ", dateLine.rstrip("\n")) # replace multiple whitespace with single space
                    tmp_date = dateLine.split() # split into list based on a space
                    if( len(tmp_date) >= 4 ):
                        tmpMonth = tmp_date[1].strip()
                        if "Jan" in tmpMonth:
                            tmpMonth = "01"
                        elif "Feb" in tmpMonth:
                            tmpMonth = "02"
                        elif "Mar" in tmpMonth:
                            tmpMonth = "03"
                        elif "Apr" in tmpMonth:
                            tmpMonth = "04"
                        elif "May" in tmpMonth:
                            tmpMonth = "05"
                        elif "Jun" in tmpMonth:
                            tmpMonth = "06"
                        elif "Jul" in tmpMonth:
                            tmpMonth = "07"
                        elif "Aug" in tmpMonth:
                            tmpMonth = "08"
                        elif "Sep" in tmpMonth:
                            tmpMonth = "09"
                        elif "Oct" in tmpMonth:
                            tmpMonth = "10"
                        elif "Nov" in tmpMonth:
                            tmpMonth = "11"
                        elif "Dec" in tmpMonth:
                            tmpMonth = "12"
                        self.distro_info['timeArchiveRun'] = tmp_date[-1].strip() + "-" + tmpMonth + "-" + tmp_date[2].strip().zfill(2) + " " + tmp_date[3].strip()
                        in_date = False
            elif( in_uname ):
                if "#==[" in line:
                    in_uname = False
                else:
                    tmp_uname = line.split()
                    if( len(tmp_uname) >= 3 ):
                        self.distro_info['kernelVersion'] = tmp_uname[2].strip()
                        self.distro_info['serverName'] = tmp_uname[1].strip()
                        self.distro_info['osArch'] = tmp_uname[-2].strip()
                        in_uname = False
            elif( in_os_release ):
                if "#==[" in line:
                    in_os_release = False
                    product_info['name'] = str(self.distro_info['Summary']) + " (" + self.distro_info['osArch'] + ")"
                else:
                    if line.lower().startswith("pretty_name"):
                        self.distro_info['Summary'] = line.split('=')[-1].replace('"', '').strip()
                        tmp_pretty_name = line.lower()
                        if "suse linux enterprise micro" in tmp_pretty_name:
                            product_info['tag'] = 'slem'
                            product_info['use_tag'] = 'sle'
                            product_info['patternTag'] = 'SLE'
                        elif "suse linux micro" in tmp_pretty_name:
                            product_info['tag'] = 'slem'
                            product_info['use_tag'] = 'sle'
                            product_info['patternTag'] = 'SLE'
                        elif "suse linux enterprise high performance computing" in tmp_pretty_name:
                            product_info['tag'] = 'hpc'
                            product_info['use_tag'] = 'sle'
                            product_info['patternTag'] = 'SLE'
                        elif "suse linux enterprise" in tmp_pretty_name:
                            product_info['tag'] = 'sle'
                            product_info['use_tag'] = product_info['tag']
                            product_info['patternTag'] = 'SLE'
                        elif "opensuse leap" in tmp_pretty_name:
                            product_info['tag'] = 'sle'
                            product_info['use_tag'] = product_info['tag']
                            product_info['patternTag'] = 'SLE'
                    elif line.lower().startswith("version_id"):
                        version_id = line.replace('"', "").strip().split('=')[1].split('.')
                        product_info['vermajor'] = str(version_id[0])
                        if( len(version_id) > 1 ):
                            product_info['verminor'] = str(version_id[1])
                        else:
                            product_info['verminor'] = "0"
                        product_info['version'] = product_info['verminor']

        # Look for SUSE release as a last resort
        if( len(self.distro_info['Summary']) == 0 ):
            for line in basic_env_file:
                if "/etc/SuSE-release" in line:
                    in_suse_release = True
                elif( in_suse_release ):
                    if "#==[" in line:
                        in_suse_release = False
                        product_info['name'] = str(self.distro_info['Summary'])
                    else:
                        if( len(self.distro_info['Summary']) > 0 ):
                            if line.lower().startswith("version"):
                                product_info['vermajor'] = line.split('=')[-1].replace('"', '').strip()
                            elif line.lower().startswith("patchlevel"):
                                product_info['verminor'] = line.split('=')[-1].replace('"', '').strip()
                            product_info['version'] = product_info['verminor']
                        else:
                            self.distro_info['Summary'] = line.strip()

        if( product_info['tag'] == 'Unknown' ):
            self.distro_info['valid'] = False
            msg.debug("Distro Evaluation", self.distro_info)
            msg.debug("Product Evaluation", product_info)
            msg.min(" Error: Unknown OS", product_info['name'])
            return
        else:
            if( product_info['tag'] == 'slem' ):
                product_info['use_vermajor'] = "1" + str(product_info['vermajor'])
                product_info['use_verminor'] = product_info['verminor']
                product_info['nameTag'] = 'Product:'
                product_info['versionTag'] = 'Version:'
                product_info['version'] = str(product_info['vermajor']) + "." + str(product_info['verminor'])
                product_info['supportconfigKey'] = str(product_info['tag']) + str(product_info['vermajor']) + str(product_info['verminor'])
            else:
                product_info['use_vermajor'] = product_info['vermajor']
                product_info['use_verminor'] = product_info['verminor']
                product_info['supportconfigKey'] = str(product_info['tag']) + str(product_info['vermajor']) + "sp" + str(product_info['verminor'])

        self.supportconfig_keys.append(product_info['supportconfigKey'])
        products_found.append(product_info)

        del basic_env_file

        self.msg.min('Supportconfig Products', ' '.join(self.supportconfig_keys))

        self.distro_info['name'] = os.path.basename(self.location)
        self.distro_info['path'] = os.path.dirname(self.location)
        self.json_data['sc_info'] = self.distro_info
        self.json_data['prod_info'] = products_found
        self.msg.debug('Distro Info', self.distro_info)
        self.msg.debug('Products Found', products_found)

        return products_found

def show_pattern_library(msg, location):
    total_count=0
    directory = {}
    file_list = []
    display = '{0:>17} : {1}'
    msg.min("Pattern Library Summary\n")
    msg.min(display.format('Pattern Directory', 'Count'))
    msg.min(display.format('=================', '====='))
    for root, dirs, files in os.walk(location):
        file_list = []
        for name in files:
            if "README" in name:
                continue
            else:
                file_list.append(os.path.join(root, name))
        if len(file_list) > 0:
            file_list.sort()
            directory[root] = file_list
            total_count += len(file_list)

    for _dir, _list in sorted(directory.items()):
        if( msg.get_level() >= msg.LOG_NORMAL ):
            msg.min(display.format(_dir, len(_list)))
            for _pat in _list:
                msg.normal(_pat)
        elif( msg.get_level() >= msg.LOG_MIN ):
            msg.min(display.format(os.path.basename(_dir), len(_list)))
        msg.normal()

    msg.quiet(display.format(total_count, 'Total Available Patterns'))
    msg.min()

def valid_supportconfig_dir(msg, given_path):
    TEST_FILES = ['basic-environment.txt', 'rpm.txt']
    if not os.access(given_path, os.R_OK | os.X_OK):
        msg.min(" Error", "Directory permission denied: {0}".format(given_path))
        msg.min(" * Suggestion", "Try sudo scatool {0}".format(given_path))
        return False
    else:
        for test_file in TEST_FILES:
            file_path = given_path + '/' + test_file
            if not os.access(file_path, os.F_OK):
                msg.min(" Error", "Invalid supportconfig directory: {0}".format(given_path))
                msg.verbose(" * Missing", "{0}".format(file_path))
                return False
            elif not os.access(file_path, os.R_OK):
                msg.min(" Error", "Read file permission denied: {0}".format(file_path))
                msg.min(" * Suggestion", "Try sudo scatool {0}".format(given_path))
                return False
    return True

def evaluate_remote_server(msg, given_hostname):
    type_found = {'server_valid': False, 'given_hostname': given_hostname}
    msg.debug("Evaluating Server", given_hostname)
    cmd = "ping -c1 -w1 " + given_hostname
    msg.verbose("Pinging", given_hostname)
    msg.debug("Process Command", cmd)
    ping_server = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = ping_server.communicate()
    if ping_server.returncode == 0:
        type_found['server_valid'] = True
        type_found['ping'] = True
        try:
            socket.inet_aton(given_hostname)
            type_found['connection_id'] = given_hostname
            type_found['found_remote_server'] = True
        except socket.error:
            try:
                type_found['connection_id'] = socket.gethostbyname(given_hostname.strip("\n"))
                type_found['found_remote_server'] = True
            except:
                if type_found['found_remote_server']:
                    type_found['server_valid'] = False
                    type_found['error_str'] = "Error: Unable to connect to " + given_hostname
    else:
        type_found['server_valid'] = False
        type_found['ping'] = False
        type_found['found_remote_server'] = False
        type_found['error_str'] = "Error: Cannot ping " + given_hostname


    msg.debug("Remote Server Evaluation", type_found)
    return type_found

def extract_supportconfig(msg, tarball):
    path_in_tarball = ''
    archfile = tarball['path'] 
    archdir = tarball['extract_here']
    msg.verbose(" Extracting File", archfile)
    cmd = "tar -xvf "  + archfile + " -C " + archdir
    msg.debug('Process Command', cmd)
    process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    stdout, stderr = process.communicate()
    outfile = stdout.splitlines()[0]
    rc = process.returncode
    if( rc > 0 ):
        print(" Error: Cannot extract tar file", file=sys.stderr)
        print(stderr, file=sys.stderr)
        print(file=sys.stderr)
        sys.exit(7)
    else:
        path_in_tarball = archdir + '/' + os.path.dirname(outfile)
        msg.verbose(' Embedded Directory', path_in_tarball)

    return path_in_tarball

def i_am_root():
    if not os.environ.get("SUDO_UID") and os.geteuid() != 0:
        return False
    return True

class SCAReport():
    REQUIRED_ELEMENTS = ["META_CLASS", "META_CATEGORY", "META_COMPONENT", "PATTERN_ID", "PRIMARY_LINK", "OVERALL", "OVERALL_INFO", "META_LINK_"]
    SEV_TABLE = {'-2': 'temp', '-1': 'partial', '0': 'success', '1': 'recommend', '2': 'promotion', '3': 'warning', '4': 'critical', '5': 'error', '6': 'ignore' }
    VALID_REPORT_TYPES = ['html', 'json', 'all']
    default_report_type = 'html'
    # REMOVE GITHUB_OWNER when source url is added to results.pattern info
    GITHUB_OWNER = 'https://github.com/openSUSE/'

    def __init__(self, msg, config):
        self.msg = msg
        self.config = config
        self.data = {}
        self.report_type = config_entry(config.get("Common", "report_output_type")).lower()
        self.report_path = config_entry(config.get("Common", "report_output_path"), '/')
        self.report_name = ''
        self.report_file = ''
        self.email_list = ''
        self.meta_class_names = []
        self.content = ''
        self.__validate_report_type()

    def __str__ (self):
        pattern = '''
Class instance of {}
  report_type      = {}
  report_path      = {}
  report_name      = {}
  report_file      = {}
  email_list       = {}
  meta_class_names = {}
'''
        return pattern.format(self.__class__.__name__, self.report_type, self.report_path, self.report_name, self.report_file, self.email_list, self.meta_class_names)

    def __validate_report_type(self):
        valid = False
        for checking_type in self.VALID_REPORT_TYPES:
            if self.report_type == checking_type:
                valid = True
        if not valid:
            msg.min("\nWarning: Invalid report type - {}, using instance default ({})".format(self.report_type,self.default_report_type))
            self.report_type = self.default_report_type

    def __normalize_path(self, check_path):
        if check_path.endswith("/"):
            return check_path
        else:
            return check_path + "/"

    def set_type(self, updated_type):
        self.report_type = updated_type.lower()
        self.__validate_report_type()

    def set_path(self, updated_path):
        if os.path.exists(updated_path):
            self.report_path = self.__normalize_path(updated_path)
        else:
            msg.min("\nError: Path not found - {}, using default path".format(updated_path))

    def set_data(self, updated_data):
        self.data = updated_data
        if len(self.report_path) < 1:
            self.report_path = self.__normalize_path(self.data['sc_info']['path'])
        self.__set_report_type(self.report_type)

    def generate_report(self):
        '''Generate an HTML SCA Report'''
        if self.data['pattern_stats']['runtime_errors'] > 0:
            self.msg.separator(self.msg.LOG_MIN, '-')

        self.msg.normal("SCA Report Type", self.report_type)
        if self.report_type == "html":
            self.__generate_html_report()
        elif self.report_type == "json":
            self.__generate_json_report()
        elif self.report_type == "all":
            self.__set_report_type('html')
            self.__generate_html_report()
            self.__set_report_type('json')
            self.__generate_json_report()

    def __set_report_type(self, this_type):
        self.report_type = this_type
        self.report_name = self.data['sc_info']['name'] + "_report." + self.report_type
        self.report_file = self.report_path + self.report_name
        self.data['sc_info']['report_type'] = self.report_type
        self.data['sc_info']['report_file'] = self.report_file

    def __generate_json_report(self):
        self.msg.verbose("+ Building JSON report", "Saving File")
        try:
            with open(self.report_file, "w") as f:
                json.dump(self.data, f, indent = 4)
        except Exception as e:
            self.msg.min("Error: Cannot write {} file - {}".format(self.report_type, str(e)))
            self.msg.min()
            sys.exit(13)
        self.msg.min("SCA Report File", self.report_file)

    def __generate_html_report(self):
        '''Build the complete HTML report'''
        self.msg.verbose(" Building HTML report", "Header")
        self.__get_meta_class_names()
        self.content += self.__build_html_header()

        self.msg.verbose(" Building HTML report", "Body")
        #Critical table
        self.content += '<H2>Conditions Evaluated as Critical<A NAME="Critical"></A></H2>' + "\n"
        self.content += '<TABLE STYLE="border:3px solid black;border-collapse:collapse;" WIDTH="100%" CELLPADDING="2">' + "\n"
        self.content += '<TR COLOR="#000000"><TH BGCOLOR="#FF0000"></TH><TH BGCOLOR="#EEEEEE" COLSPAN="3">Category</TH><TH>Message</TH><TH>Solutions</TH><TH BGCOLOR="#FF0000"></TH></TR>' + "\n"
        self.content += self.__build_severity_table(4)
        self.content += "</TABLE>" + "\n"

        #Warning table
        self.content += '<H2>Conditions Evaluated as Warning<A NAME="Warning"></A></H2>' + "\n"
        self.content += '<TABLE STYLE="border:3px solid black;border-collapse:collapse;" WIDTH="100%" CELLPADDING="2">' + "\n"
        self.content += '<TR COLOR="#000000"><TH BGCOLOR="#FFFF00"></TH><TH BGCOLOR="#EEEEEE" COLSPAN="3">Category</TH><TH>Message</TH><TH>Solutions</TH><TH BGCOLOR="#FFFF00"></TH></TR>' + "\n"
        self.content += self.__build_severity_table(3)
        self.content += "</TABLE>" + "\n"

        #Recommended table
        self.content += '<H2>Conditions Evaluated as Recommended<A NAME="Recommended"></A></H2>' + "\n"
        self.content += '<TABLE STYLE="border:3px solid black;border-collapse:collapse;" WIDTH="100%" CELLPADDING="2">' + "\n"
        self.content += '<TR COLOR="#000000"><TH BGCOLOR="#1975FF"></TH><TH BGCOLOR="#EEEEEE" COLSPAN="3">Category</TH><TH>Message</TH><TH>Solutions</TH><TH BGCOLOR="#1975FF"></TH></TR>' + "\n"
        self.content += self.__build_severity_table(1)
        self.content += "</TABLE>" + "\n"

        #Success table
        self.content += '<H2>Conditions Evaluated as Success<A NAME="Success"></A></H2>' + "\n"
        self.content += '<TABLE STYLE="border:3px solid black;border-collapse:collapse;" WIDTH="100%" CELLPADDING="2">' + "\n"
        self.content += '<TR COLOR="#000000"><TH BGCOLOR="#00FF00"></TH><TH BGCOLOR="#EEEEEE" COLSPAN="3">Category</TH><TH>Message</TH><TH>Solutions</TH><TH BGCOLOR="#00FF00"></TH></TR>' + "\n"
        self.content += self.__build_severity_table(0)
        self.content += "</TABLE>" + "\n"

        self.msg.verbose(" Building HTML report", "Footer")
        self.content += self.__build_html_footer()

        self.msg.verbose(" Building HTML report", "Saving File")
        try:
            with open(self.report_file, "w") as f:
                f.write(self.content)
        except Exception as e:
            self.msg.min("Error: Cannot write {} file - {}".format(self.report_type, str(e)))
            self.msg.min()
            sys.exit(13)
        self.msg.min("SCA Report File", self.report_file)

    def __get_meta_class_names(self):
        '''Gathers all the unique class names used in all the applied results'''
        unique_classes = {}
        for this_pattern in self.data['results'].keys():
            if self.data['results'][this_pattern]['valid']:
                unique_classes[self.data['results'][this_pattern]['class']] = True
        self.meta_class_names = list(unique_classes.keys())

    def __build_html_header(self):
        '''Build the HTML report header'''
        #reset variables
        html_content = ""
        html_content += "<!DOCTYPE html>\n"
        html_content += "<HTML>\n"
        html_content += "<HEAD>\n"
        html_content += "<TITLE>SCA Report for " + self.data['sc_info']['serverName'] + "</TITLE>\n"
        html_content += "<STYLE TYPE=\"text/css\">\n"
        html_content += "  a {text-decoration: none}  /* no underlined links */\n"
        html_content += "  a:link {color:#0000FF;}  /* unvisited link */\n"
        html_content += "  a:visited {color:#0000FF;}  /* visited link */\n"
        html_content += "</STYLE>\n"
    

        html_script = "<SCRIPT>\n\
        function toggle(className)\n\
        {\n\
        className = className.replace(/ /g,\".\");\n\
        var elements = document.querySelectorAll(\".\" + className); for(var i=0; i<elements.length; i++)\n\
        {\n\
            if( elements[i].style.display=='none' )\n\
                {\n\
                    elements[i].style.display = '';\n\
                }\n\
                else\n\
                {\n\
                    elements[i].style.display = 'none';\n\
                }\n\
        }\n\
        }\n\n\
        function showPattern(patternOutput,patternLocation)\n\
        {\n\
        alert(patternOutput + \"\\n\\n\" + \"Pattern: \" + patternLocation);\n\
        }\n\
        </SCRIPT>"

        html_content += html_script + "\n"
        html_content += "</HEAD>\n"
        html_content += "<BODY BGPROPERTIES=FIXED BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\">\n"

        #create HTML from the data we just got
        html_content += '<H1>Supportconfig Analysis Report</H1>\n'
        html_content += '<H2><HR />Server Information</H2>\n'

        html_content += '<TABLE CELLPADDING="5">\n'
        html_content += '<TR><TD><B>Analysis Date:</B></TD><TD>'
        html_content += self.data['sc_info']['timeAnalysis']
        html_content += '</TD></TR>\n'
        html_content += '<TR><TD><B>Supportconfig Run Date:</B></TD><TD>'
        html_content += self.data['sc_info']['timeArchiveRun']
        html_content += '</TD></TR>\n'
        html_content += '<TR><TD><B>Supportconfig File:</B></TD><TD>'
        html_content += self.data['sc_info']['name']
        html_content += '</TD></TR>\n'
        html_content += '</TABLE>\n'

        html_content += '<TABLE CELLPADDING="5">\n'
        html_content += '<TR><TD>&nbsp;</TD></TR>\n'
        html_content += '<TR></TR>\n'

        #Server name and hardWare
        html_content += '<TR><TD><B>Server Name:</B></TD><TD>'
        html_content += self.data['sc_info']['serverName']
        html_content += '</TD><TD><B>Hardware:</B></TD><TD>'
        html_content += self.data['sc_info']['hardWare']
        html_content += '</TD></TR>\n'

        #Products included in supportconfig
        for product in self.data['prod_info']:
            html_content += '<TR><TD><B>'
            html_content += str(product['nameTag'])
            html_content += '</B></TD><TD>'
            html_content += str(product['name'])
            html_content += '</TD><TD><B>'
            html_content += str(product['versionTag'])
            html_content += '</B></TD><TD>'
            html_content += str(product['version'])
            html_content += '</TD></TR>\n'

        if self.data['sc_info']['virtualization'] != "None" and self.data['sc_info']['virtualization'] != "":
            #hypervisor stuff
            html_content += '<TR><TD><B>Hypervisor:</B></TD><TD>'
            html_content += self.data['sc_info']['virtualization']
            html_content += '</TD><TD><B>Identity:</B></TD><TD>'
            html_content += self.data['sc_info']['vmIdentity']
            html_content += '</TD></TR>\n'

        #kernel Version and Supportconfig version
        html_content += '<TR><TD><B>Kernel Version:</B></TD><TD>'
        html_content += self.data['sc_info']['kernelVersion']
        html_content += '</TD><TD><B>Supportconfig Version:</B></TD><TD>'
        html_content += self.data['sc_info']['supportconfigVersion']
        html_content += '</TD></TR>\n'
        html_content += '</TABLE>\n'
        html_content += '<HR />\n'
        return html_content

    def __build_severity_table(self, given_severity):
        '''Build the specified severity table for the body of the HTML report'''
        pattern_source_url = ""
        primary_link_url = ""
        severity_table = ""
        solution_links = ""
        expandable_class_entries = []
        class_entries_header = ""
        class_entries_footer = ""

        #set the color.
        if given_severity == 4:
            #red (critical)
            severity_tag = "Critical "
            stats_tag = 'critical'
            color = "FF0000"
        elif given_severity == 3:
            #yellow (warning)
            severity_tag = "Warning "
            stats_tag = 'warning'
            color = "FFFF00"
        elif given_severity == 1:
            #blue.. ish (recommended)
            severity_tag = "Recommended "
            stats_tag = 'recommend'
            color = "1975FF"
        elif given_severity == 0:
            #green (success)
            severity_tag = "Success "
            stats_tag = 'success'
            color ="00FF00"
        else:
            #fallback (gray)
            severity_tag = ""
            color = "222222"

        # for each meta_class found in all the applied patterns
        for meta_class in self.meta_class_names:
            expandable_class_entries = []
            class_entries_header = ""
            class_entries_footer = ""
            patterns_in_class_and_severity = 0
            # for each valid pattern that matches this meta_class and the given_severity
            for this_pattern in self.data['results'].keys():
                solution_links = ""
                if self.data['results'][this_pattern]['valid']:
                    if self.data['results'][this_pattern]['class'] == meta_class and self.data['results'][this_pattern]['severity'] == given_severity:
                        primary_link_id = self.data['results'][this_pattern]['primary_solution']
                        primary_link_url = self.data['results'][this_pattern]['solution_links'][primary_link_id]
                        pattern_source_url = self.data['results'][this_pattern]['source']

                        # get all the solutions tags and URL links for this pattern
                        for link_tag, link_url in self.data['results'][this_pattern]['solution_links'].items():
                            solution_links += '<A HREF="{}" TARGET="_blank">{}&nbsp;</A>'.format(link_url, link_tag)

                        expandable_class_entries.append('<TR STYLE="border:1px solid black; background: #FFFFFF; display:none;" CLASS="' + meta_class + '">'\
                            '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD>'\
                            '<TD BGCOLOR="#EEEEEE" WIDTH="6%">' + self.data['results'][this_pattern]['class'] + '</TD>'\
                            '<TD BGCOLOR="#EEEEEE" WIDTH="5%">' + self.data['results'][this_pattern]['category'] + '</TD>'\
                            '<TD BGCOLOR="#EEEEEE" WIDTH="5%">' + self.data['results'][this_pattern]['component'] + '</TD>'\
                            '<TD><A HREF="' + primary_link_url + '" TARGET="_blank">' + self.data['results'][this_pattern]['description'] + '</A>&nbsp;&nbsp;'\
                            '<A ID="PatternLocation" HREF="' + pattern_source_url + '" TARGET="_blank">&nbsp;</A></TD>'\
                            '<TD WIDTH="8%">' + solution_links + '&nbsp;&nbsp;</TD>'\
                            '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD></TR>\n')

            patterns_in_class_and_severity = len(expandable_class_entries)
            if patterns_in_class_and_severity > 0:
                class_entries_header = '<TR STYLE="border:1px solid black;color: #0000FF; background: #FFCC99; font-size:80%; font-weight:normal">'\
                '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD>'\
                '<TD BGCOLOR="#FFCC99" WIDTH="6%"><A ID="NewClass" TITLE="Click to Expand/Collapse" HREF="#" onClick="toggle(\''+ meta_class + '\');return false;">' + meta_class + '</A></TD>'\
                '<TD BGCOLOR="#FFCC99" WIDTH="5%">&nbsp;</TD>'\
                '<TD BGCOLOR="#FFCC99" WIDTH="5%">&nbsp;</TD>'\
                '<TD><A ID="NewClass" TITLE="Click to Expand/Collapse" HREF="#" onClick="toggle(\'' + meta_class + '\');return false;">' + str(patterns_in_class_and_severity) + " " + severity_tag + meta_class + " Message(s)" + '</A></TD>'\
                '<TD WIDTH="8%">&nbsp;</TD>'\
                '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD></TR>\n'

                severity_table += class_entries_header
                severity_table += ''.join(expandable_class_entries)

        if self.data['pattern_stats'][stats_tag] > 0:
            if self.data['pattern_stats'][stats_tag] > 1:
                footer_msg = "{} {} Conditions Found".format(self.data['pattern_stats'][stats_tag], severity_tag)
            else:
                footer_msg = "{} {} Condition Found".format(self.data['pattern_stats'][stats_tag], severity_tag)
            class_entries_footer = '<TR STYLE="border:1px solid black;color: #000000; background: #FFCC99; font-size:80%; font-weight:normal">'\
            '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD>'\
            '<TD BGCOLOR="#FFCC99" WIDTH="6%">TOTAL</TD>'\
            '<TD BGCOLOR="#FFCC99" WIDTH="5%">&nbsp;</TD>'\
            '<TD BGCOLOR="#FFCC99" WIDTH="5%">&nbsp;</TD>'\
            '<TD>' + footer_msg + '</TD>'\
            '<TD WIDTH="8%">&nbsp;</TD>'\
            '<TD BGCOLOR="#' + color + '" WIDTH="2%">&nbsp;</TD></TR>\n'

        severity_table += class_entries_footer

        return(severity_table)

    def __build_html_footer(self):
        '''Build the HTML report footer'''
        footer_content = '\n\n<HR />\n\n<TABLE WIDTH="100%">\n<TR>'\
            '<TD ALIGN="left" WIDTH="30%">Client: ' + self.data['file_data']['analyzer'] + ' (Report Generated by: SCA Tool)</TD>'\
            '<TD ALIGN="center">Patterns Evaluated: ' + str(self.data['pattern_stats']['applicable']) + ', Appliable to Server: ' + str(self.data['pattern_stats']['applied']) + '</TD>'\
            '<TD ALIGN="right" WIDTH="30%"><A HREF="https://www.suse.com/support/" ALT="SUSE Technical Support" TARGET="_blank">SUSE Technical Support</A></TD>'\
            '</TR>\n</TABLE>\n'

        return footer_content

    def email_report(self, updated_addrs):
        '''Email the SCA Report to the list of recipients'''
        self.generate_report()
        self.email_list = updated_addrs.split(',')
        self.msg.min("Email Report Recipients", ' '.join(self.email_list))

        email_server = 'localhost'
        email_to = self.email_list
        email_from = 'SCA Tool <root>'
        email_subject = "SCA Report for " + str(self.data['sc_info']['serverName']) + ": " + str(self.data['pattern_stats']['applied']) + "/" + str(self.data['pattern_stats']['applicable']) + ", " + str(self.data['pattern_stats']['critical']) + ":" + str(self.data['pattern_stats']['warning']) + ":" + str(self.data['pattern_stats']['recommend']) + ":" + str(self.data['pattern_stats']['success'])
        content_type = self.report_type

        # create text email
        text = "* Supportconfig Analysis Report *\n"
        text += "Analysis Date:            " + str(self.data['sc_info']['timeAnalysis']) + "\n"
        text += "Supportconfig Archive:    " + str(self.data['file_data']['embedded_dir']) + "\n"
        text += "Server Analyzed:          " + str(self.data['sc_info']['serverName']) + "\n"
        text += "Total Patterns Evaluated: " + str(self.data['pattern_stats']['applicable']) + "\n"
        text += "Applicable to Server:     " + str(self.data['pattern_stats']['applied']) + "\n"
        text += "  Critical:               " + str(self.data['pattern_stats']['critical']) + "\n"
        text += "  Warning:                " + str(self.data['pattern_stats']['warning']) + "\n"
        text += "  Recommended:            " + str(self.data['pattern_stats']['recommend']) + "\n"
        text += "  Success:                " + str(self.data['pattern_stats']['success']) + "\n"
        text += "Source:                   " + str(self.data['file_data']['analyzer']) + "\n"

        # create html email
        html = '<!DOCTYPE html PUBLIC "-//W3C//DTD Xself.content 1.0 Transitional//EN" '
        html += '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml">\n'
        html += '<body>\n'
        html += '<h1>Supportconfig Analysis Report</h1>\n'
        html += '<table>\n'
        html += "<tr><td>Analysis Date:</td><td>" + str(self.data['sc_info']['timeAnalysis']) + '</td></tr>\n'
        html += "<tr><td>Supportconfig Archive:</td><td>" + str(self.data['file_data']['embedded_dir']) + '</td></tr>\n'
        html += "<tr><td>Server Analyzed:</td><td>" + str(self.data['sc_info']['serverName']) + '</td></tr>\n'
        html += "<tr><td>Total Patterns Evaluated:</td><td>" + str(self.data['pattern_stats']['applicable']) + '</td></tr>\n'
        html += "<tr><td>Applicable to Server:</td><td>" + str(self.data['pattern_stats']['applied']) + '</td></tr>\n'
        html += "<tr><td>&nbsp;&nbsp;Critical:</td><td>" + str(self.data['pattern_stats']['critical']) + '</td></tr>\n'
        html += "<tr><td>&nbsp;&nbsp;Warning:</td><td>" + str(self.data['pattern_stats']['warning']) + '</td></tr>\n'
        html += "<tr><td>&nbsp;&nbsp;Recommended:</td><td>" + str(self.data['pattern_stats']['recommend']) + '</td></tr>\n'
        html += "<tr><td>&nbsp;&nbsp;Success:</td><td>" + str(self.data['pattern_stats']['success']) + '</td></tr>\n'
        html += "<tr><td>Source:</td><td>" + str(self.data['file_data']['analyzer']) + '</td></tr>\n'
        html += "</table>\n</body></html>\n\n"
        email_msg = MIMEMultipart()
        email_msg['Subject'] = email_subject
        email_msg['From'] = email_from
        email_msg['To'] = ', '.join(email_to)
        email_msg.attach(MIMEText(text,'plain'))
        email_msg.attach(MIMEText(html,'html'))

        # now attach the file
        with open(self.report_file, 'rb') as f:
            email_file_msg = MIMEApplication(f.read(), _subtype=content_type)
        email_file_msg.add_header('Content-Disposition','attachment;filename=' + self.report_name)
        email_msg.attach(email_file_msg)

        # send email
        smtp_server = None
        try:
            smtp_server = smtplib.SMTP(email_server, timeout=15)
            smtp_server.sendmail(email_from,email_to,email_msg.as_string())
            return True
        except Exception as error:
            print("  Error: Unable to send email: '%s'." % str(error), file=sys.stderr)
            pass
        finally:
            if smtp_server:
                smtp_server.quit()
        return False

    def clean_up(self):
        if self.data['file_data']['type'] == 'dir':
            if self.data['file_data']['remove_directory']:
                self.msg.verbose(" Removing Directory", self.data['file_data']['path'])
                shutil.rmtree(self.data['file_data']['path'])
        elif self.data['file_data']['type'] == 'file':
            if self.data['file_data']['remove_directory']:
                self.msg.verbose(" Removing Directory", self.data['file_data']['embedded_dir'])
                shutil.rmtree(self.data['file_data']['embedded_dir'])
            if self.data['file_data']['remove_tarball']:
                self.msg.verbose(" Removing File", self.data['file_data']['path'])
                os.remove(self.data['file_data']['path'])

def get_local_supportconfig(msg, config):
    '''Run supportconfig on the local server if root access available'''
    local_hostname = str(os.uname()[1])
    msg.min('Running Supportconfig On', local_hostname)
    analysis_datetime = datetime.datetime.now()
    date_stamp = analysis_datetime.strftime("%y%m%d")
    time_stamp = str(analysis_datetime.hour).zfill(2) + str(analysis_datetime.minute).zfill(2) + str(analysis_datetime.second).zfill(2)
    supportconfig_output_lines = config_entry(config.get("Supportconfig", "output_lines"))
    supportconfig_filename_prefix = config_entry(config.get("Supportconfig", "filename_prefix"))
    local_supportconfig_path = config_entry(config.get("Supportconfig", "path_local"), '/')
    local_supportconfig_name = local_hostname + "_" + str(date_stamp) + "_" + str(time_stamp)
    supportconfig_path = local_supportconfig_path + supportconfig_filename_prefix + local_supportconfig_name

    try:
        cmd = "supportconfig -bB " + local_supportconfig_name + " -t " + local_supportconfig_path
        msg.debug('Process Command', cmd)
        p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    #if we cannot run supportconfig
    except Exception:
        print("Error: Cannot run supportconfig\n", file=sys.stderr)
        return
    condition = True

    if( msg.get_level() == msg.LOG_MIN ):
        if progress_bar_active:
            scbar = ProgressBar("Gathering Supportconfig: ", supportconfig_output_lines)

    if( msg.get_level() >= msg.LOG_VERBOSE ):
        msg.verbose('Gathering Supportconfig', 'In Progress')
    elif( msg.get_level() >= msg.LOG_MIN ):
        if progress_bar_active:
            scbar = ProgressBar("Gathering Supportconfig: ", supportconfig_output_lines)
        else:
            msg.min('Gathering Supportconfig', 'In Progress')

    while condition:
        out = p.stdout.read(1)
        if out != '':
            if( msg.get_level() >= msg.LOG_VERBOSE ):
                sys.stdout.write(out)
                sys.stdout.flush()
            elif( msg.get_level() >= msg.LOG_MIN ):
                if out == "\n":
                    if progress_bar_active:
                        scbar.inc_count()
                        scbar.update()
        condition = not bool(out == "" and p.poll() != None)

    if( msg.get_level() > msg.LOG_QUIET and msg.get_level() <= msg.LOG_NORMAL ):
        if progress_bar_active:
            scbar.finish()
    
    return supportconfig_path

def evaluate_given_path(msg, given_path):
#    given_results = {'exists': False, 'given_path': given_path, 'path': '', 'head': '', 'tail': '', 'type': '', 'read_head': False, 'write_head': False, 'exec_head': False, 'read_tail': False, 'write_tail': False, 'exec_tail': False, 'mimetype': ''}
    given_results = {
        'exists': False, 
        'given_path': given_path, 
        'path': '', 
        'head': '', 
        'tail': '', 
        'type': '', 
        'tail_mime_type': '',
        'read_head': False, 'write_head': False, 'exec_head': False, 
        'read_tail': False, 'write_tail': False, 'exec_tail': False, 
    }

    if os.path.exists(given_path):
        given_results['exists'] = True
        given_results['path'] = os.path.abspath(given_path)
        given_results['head'] = os.path.dirname(given_results['path'])
        given_results['tail'] = os.path.basename(given_results['path'])
        if os.access(given_results['path'], os.R_OK):
            given_results['read_tail'] = True
        if os.access(given_results['path'], os.W_OK):
            given_results['write_tail'] = True
        if os.access(given_results['path'], os.X_OK):
            given_results['exec_tail'] = True
        if given_results['read_tail']:
            if os.path.isfile(given_results['path']):
                given_results['type'] = 'file'
                cmd = "file --brief --mime-type " + given_path
                msg.debug('Process Command', cmd)
                process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
                stdout, stderr = process.communicate()
                given_results['tail_mime_type'] = stdout.strip()
            elif os.path.isdir(given_results['path']):
                given_results['type'] = 'dir'
                given_results['tail_mime_type'] = 'inode/directory'
        if os.access(given_results['head'], os.R_OK):
            given_results['read_head'] = True
        if os.access(given_results['head'], os.W_OK):
            given_results['write_head'] = True
        if os.access(given_results['head'], os.X_OK):
            given_results['exec_head'] = True

    msg.debug("Evaluate Path", given_results)
    return given_results

def check_extraction_path_given(msg, config_file, extract_path_cmd, extract_path_config):
    path_data = {}

    if extract_path_cmd:
        path_data = evaluate_given_path(msg, extract_path_cmd)
        msg.verbose("Extraction Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
        if path_data['exists']:
            if path_data['write_tail']:
                path_data['extract_here'] = path_data['path']
                path_data['extract_here_for_reports'] = True
                msg.normal("Extraction Directory Change", "From command line: {0}".format(path_data['path']))
            else:
                msg.min("Extraction Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
                msg.min(" Error", "Write permisson denied, cannot extract file to {0}".format(path_data['path']))
                msg.min(" * Suggestion", "Fix the -x, --extract_here directory\n")
                sys.exit(2)
        else:
            msg.min("Extraction Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
            msg.min(" Error", "Directory not found - {0}".format(path_data['given_path']))
            msg.min(" * Suggestion", "Try mkdir -p {0} or fix the -x, --extract_here directory\n".format(path_data['given_path']))
            sys.exit(2)
    elif extract_path_config:
        path_data = evaluate_given_path(msg, extract_path_config)
        msg.verbose("Extraction Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
        if path_data['exists']:
            if path_data['write_tail']:
                path_data['extract_here'] = path_data['path']
                path_data['extract_here_for_reports'] = True
                msg.normal("Extraction Directory Change", "From config file: {0}".format(path_data['path']))
            else:
                msg.min("Extraction Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
                msg.min(" Error", "Write permisson denied, cannot extract file to {0}".format(path_data['path']))
                msg.min(" * Suggestion", "Fix the extract_path in {0}\n".format(config_file))
                sys.exit(2)
        else:
            msg.min("Extraction Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
            msg.min(" Error", "Directory not found - {0}".format(path_data['given_path']))
            msg.min(" * Suggestion", "Try mkdir -p {0} or fix the extract_path in {1}\n".format(path_data['given_path'], config_file))
            sys.exit(2)

    return path_data

def check_report_path_given(msg, config_file, report_path_cmd, report_path_config):
    path_data = {}

    if report_path_cmd:
        path_data = evaluate_given_path(msg, report_path_cmd)
        msg.verbose("SCA Report Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
        if path_data['exists']:
            if path_data['write_tail']:
                msg.normal("SCA Report Directory Change", "From command line: {0}".format(path_data['path']))
            else:
                msg.min("SCA Report Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
                msg.min(" Error", "Write permisson denied, cannot create report file in {0}".format(path_data['path']))
                msg.min(" * Suggestion", "Fix the -o, --output directory\n")
                sys.exit(3)
        else:
            msg.min("SCA Report Directory", "Evaluating path given on command line: {0}".format(path_data['given_path']))
            msg.min(" Error", "Directory not found - {0}".format(path_data['given_path']))
            msg.min(" * Suggestion", "Try mkdir -p {0} or fix the -o, --output directory\n".format(path_data['given_path']))
            sys.exit(3)
    elif report_path_config:
        path_data = evaluate_given_path(msg, report_path_config)
        msg.verbose("SCA Report Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
        if path_data['exists']:
            if path_data['write_tail']:
                msg.normal("SCA Report Directory Change", "From config file: {0}".format(path_data['path']))
            else:
                msg.min("SCA Report Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
                msg.min(" Error", "Write permisson denied, cannot create report file in {0}".format(path_data['path']))
                msg.min(" * Suggestion", "Fix the report_output_path in {0}\n".format(config_file))
                sys.exit(3)
        else:
            msg.min("SCA Report Directory", "Evaluating path given in the config file: {0}".format(path_data['given_path']))
            msg.min(" Error", "Directory not found - {0}".format(path_data['given_path']))
            msg.min(" * Suggestion", "Try mkdir -p {0} or fix the report_output_path in {1}\n".format(path_data['given_path'], config_file))
            sys.exit(3)

    return path_data

def separate_entry(msg, count):
    if count > 1:
        msg.min()
        msg.separator(msg.LOG_MIN, '=')
        msg.min()

def analyze_supportconfig_directory(msg, config, report, data, email):
    dict1 = {}
    dict2 = {}

    sca_data = SupportconfigAnalysis(msg, config, data['supportconfig_directory'])
    if sca_data.is_valid():
        dict1['file_data'] = data
        dict2 = sca_data.get_results()
        sca_results = {**dict1, **dict2}
        report.set_data(sca_results)
        if len(email) > 0:
            report.email_report(email)
        else:
            report.generate_report()
        if( msg.get_level() < msg.LOG_DEBUG ):
            report.clean_up()



##############################################################################
# Main
##############################################################################

def main(argv):
    '''main entry point'''
    global SVER, progress_bar_active, config_file
    global width, description_width
    VALID_MIME_TYPES = [ 'application/x-xz', 'application/x-bzip', 'application/x-gzip', 'application/x-tar' ]

    file_data = {} # Final dictionary will be: {'valid': False, 'type': 'Unknown', 'analyzer': 'scaool', 'remove_tarball': False, 'remove_directory': False}
    this_file_data = {}
    supportconfig_received = ''
    remove_archive = False
    analyze_server = False
    pattern_library = False
    given_type = ''
    given_report_output_path = {}
    given_extract_path = {}

    if( os.path.exists(config_file) ):
        config.read(config_file)
        width = int(config_entry(config.get("Common", "display_width")))
        description_width = int(config_entry(config.get("Common", "description_width")))
        sca_library_path = config_entry(config.get("Common", "sca_library_path"), '/')
        sca_patterns_path = config_entry(config.get("Common", "sca_pattern_path"), '/')
        extract_path = config_entry(config.get("Common", "extract_path"), '/')
        report_output_path = config_entry(config.get("Common", "report_output_path"), '/')
        email_addr_str = config_entry(config.get("Common", "report_email_list"))
        msg.set_width(description_width)
        config_logging = msg.validate_level(config_entry(config.get("Common", "log_level")))
        if( config_logging >= msg.LOG_QUIET ):
            msg.set_level(config_logging)
        else:
            msg.verbose("Warning: Invalid log level in config file, using instance default")
    else:
        title()
        print("Error: File not found - " + config_file + "\n")
        sys.exit(1)

    os.environ['PYTHONPATH'] = os.path.abspath(sca_library_path + 'python')
    os.environ['PERL5LIB'] = os.path.abspath(sca_library_path + 'perl')
    os.environ['BASHLIB'] = os.path.abspath(sca_library_path + 'bash')

    try:
        (optlist, args) = getopt.gnu_getopt(argv[1:], "hbde:jno:pl:qrst:vx:", ["help", "batch", "debug", "email=", "json", "normal", "output=", "summary", "log_level=", "quiet", "remove", "server", "type=", "verbose", "extract_here="])
    except getopt.GetoptError as exc:
        title()
        print("Error:", exc, file=sys.stderr)
        print()
        usage()
        sys.exit(2)
    for opt, arg in optlist:
        if opt in {"-h", "--help"}:
            title()
            usage()
            sys.exit(0)
        elif opt in {"-b", "--batch"}:
            progress_bar_active = False
        elif opt in {"-d", "--debug"}:
            msg.set_level(msg.LOG_DEBUG)
        elif opt in {"-e", "--email"}:
            email_addr_str = arg
        elif opt in {"-j", "--json"}:
            given_type = "json"
        elif opt in {"-n", "--normal"}:
            msg.set_level(msg.LOG_NORMAL)
        elif opt in {"-o", "--output"}:
            given_report_output_path = arg
        elif opt in {"-p", "--summary"}:
            pattern_library = True
        elif opt in {"-r", "--remove"}:
            remove_archive = True
        elif opt in {"-s", "--server"}:
            analyze_server = True
        elif opt in {"-t", "--type"}:
            given_type = arg
        elif opt in {"-q", "--quiet"}:
            msg.set_level(msg.LOG_QUIET)
        elif opt in {"-v", "--verbose"}:
            msg.set_level(msg.LOG_VERBOSE)
        elif opt in {"-x", "--extract_here"}:
            given_extract_path = arg
        elif opt in {"-l", "--log_level"}:
            user_logging = msg.validate_level(arg)
            if( user_logging >= msg.LOG_QUIET ):
                msg.set_level(user_logging)
            else:
                print("Warning: Invalid log level, using instance default")

    if( msg.get_level() > msg.LOG_QUIET ):
        title()

    if pattern_library:
        show_pattern_library(msg, sca_patterns_path)
        sys.exit(0)

    preconfigured_extraction_path = check_extraction_path_given(msg, config_file, given_extract_path, extract_path)
    preconfigured_report_path = check_report_path_given(msg, config_file, given_report_output_path, report_output_path)

    total_args_given = len(args)
    supportconfig_dir = ''
    count = 0

    if total_args_given > 0:
        for given_source in args:
            msg.normal("Checking", given_source)
            this_file_data = evaluate_given_path(msg, given_source)
            this_file_data['analyzer'] = "scatool v" + str(SVER)
            this_file_data['remove_tarball'] = False
            this_file_data['remove_directory'] = False
            this_file_data['extract_here_for_reports'] = False
            if this_file_data['exists']:
                count += 1
                if total_args_given > 1:
                    msg.min("Processing [{}/{}]".format(count, total_args_given), this_file_data['path'])
                else:
                    msg.min("Processing", this_file_data['path'])

                if this_file_data['type'] == 'dir':
                    if remove_archive:
                        this_file_data['remove_directory'] = True
                    if valid_supportconfig_dir(msg, this_file_data['path']):
                        sca_report = SCAReport(msg, config)
                        if given_type:
                            sca_report.set_type(given_type)
                        if preconfigured_report_path:
                            sca_report.set_path(preconfigured_report_path['path'])
                        else:
                            if this_file_data['write_head']:
                                sca_report.set_path(this_file_data['head'])
                            else:
                                msg.min(" Error", "Write permisson denied, cannot create report file in {0}".format(this_file_data['head']))
                                msg.min(" * Suggestion", "Use -o, --output to specify an alternate report file directory")
                                separate_entry(msg, total_args_given)
                                continue
                        this_file_data['supportconfig_directory'] = this_file_data['path']
                        analyze_supportconfig_directory(msg, config, sca_report, this_file_data, email_addr_str)
                    separate_entry(msg, total_args_given)

                elif this_file_data['type'] == 'file':
                    if not this_file_data['tail_mime_type'] in VALID_MIME_TYPES:
                        msg.min(" Skipping file", "Not a supportconfig compressed file - {0}".format(this_file_data['path']))
                        separate_entry(msg, total_args_given)
                        continue
                    if remove_archive:
                        this_file_data['remove_tarball'] = True
                    this_file_data['remove_directory'] = True

                    if preconfigured_extraction_path:
                        this_file_data['extract_here'] = preconfigured_extraction_path['extract_here']
                        this_file_data['extract_here_for_reports'] = preconfigured_extraction_path['extract_here_for_reports']
                    else:
                        if this_file_data['write_head']:
                            this_file_data['extract_here'] = this_file_data['head']
                        else:
                            msg.min(" Error", "Write permisson denied, cannot extract file to {0}".format(this_file_data['head']))
                            msg.min(" * Suggestion", "Use -x, --extract_here to specify an alternate extraction directory")
                            separate_entry(msg, total_args_given)
                            continue
                    supportconfig_dir = extract_supportconfig(msg, this_file_data)
                    if valid_supportconfig_dir(msg, supportconfig_dir):
                        this_file_data['embedded_dir'] = supportconfig_dir
                        this_file_data['supportconfig_directory'] = this_file_data['embedded_dir']

                        sca_report = SCAReport(msg, config)
                        if given_type:
                            sca_report.set_type(given_type)
                        if preconfigured_report_path:
                            sca_report.set_path(preconfigured_report_path['path'])
                        else:
                            if this_file_data['extract_here_for_reports']:
                                sca_report.set_path(this_file_data['extract_here'])
                            else:
                                if this_file_data['write_head']:
                                    sca_report.set_path(this_file_data['head'])
                                else:
                                    msg.min(" Error", "Write permisson denied, cannot create report file in {0}".format(this_file_data['head']))
                                    msg.min(" * Suggestion", "Use -o, --output to specify an alternate report file directory")
                                    separate_entry(msg, total_args_given)
                                    continue
                        analyze_supportconfig_directory(msg, config, sca_report, this_file_data, email_addr_str)
                    separate_entry(msg, total_args_given)

                elif not this_file_data['read_tail']:
                    msg.min(" Error", "Read permisson denied - {0}".format(this_file_data['path']))
                    msg.min(" * Suggestion", "Use chmod or sudo to elevate read permissions")
                    separate_entry(msg, total_args_given)
                    continue
                else:
                    msg.min(" Error", "Unknown file type - {0}".format(this_file_data['path']))
                    separate_entry(msg, total_args_given)
                    continue
        msg.normal()
    elif analyze_server:
        if i_am_root():
            given_source = get_local_supportconfig(msg, config)
            this_file_data = evaluate_given_path(msg, given_source)
            this_file_data['analyzer'] = "scatool v" + str(SVER)    
            this_file_data['remove_tarball'] = False
            this_file_data['remove_directory'] = True
            this_file_data['extract_here_for_reports'] = False
            this_file_data['supportconfig_directory'] = this_file_data['path']
            if valid_supportconfig_dir(msg, this_file_data['supportconfig_directory']):
                sca_report = SCAReport(msg, config)
                if given_type:
                    sca_report.set_type(given_type)
                if preconfigured_report_path:
                    sca_report.set_path(preconfigured_report_path['path'])
                else:
                    sca_report.set_path(this_file_data['head'])
                analyze_supportconfig_directory(msg, config, sca_report, this_file_data, email_addr_str)
        else:
            msg.min("Analyze Local Server", "Root access required to run supportconfig")
    else:
        usage()
        sys.exit(0)

    msg.min()

# Entry point
if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)
    config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
    msg = DisplayMessages()
    main(sys.argv)

