# Copyright (c) 2020 Broadcom. All rights reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
#
# This software and all information contained therein is confidential and proprietary and shall not be duplicated, used, disclosed or
# disseminated in any way except as authorized by the applicable license agreement, without the express written permission of Broadcom.
# All authorized reproductions must be marked with this language.
#
# EXCEPT AS SET FORTH IN THE APPLICABLE LICENSE AGREEMENT, TO THE EXTENT PERMITTED BY APPLICABLE LAW OR AS AGREED BY BROADCOM IN ITS
# APPLICABLE LICENSE AGREEMENT, BROADCOM PROVIDES THIS DOCUMENTATION "AS IS" WITHOUT WARRANTY OF ANY KIND, INCLUDING WITHOUT LIMITATION,
# ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT. IN NO EVENT WILL BROADCOM BE LIABLE TO
# THE END USER OR ANY THIRD PARTY FOR ANY LOSS OR DAMAGE, DIRECT OR INDIRECT, FROM THE USE OF THIS DOCUMENTATION, INCLUDING WITHOUT LIMITATION,
# LOST PROFITS, LOST INVESTMENT, BUSINESS INTERRUPTION, GOODWILL, OR LOST DATA, EVEN IF BROADCOM IS EXPRESSLY ADVISED IN ADVANCE OF THE
# POSSIBILITY OF SUCH LOSS OR DAMAGE.


"""A command line utility for get Policy content differences from Splunk."""
from __future__ import absolute_import

from xmldiff import main, formatting
from pprint import pprint
import socket
import sys, os, re
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

try:
    import utils
except ImportError:
    raise Exception("Add the SDK repository to your PYTHONPATH to run the examples "
                    "(e.g., export PYTHONPATH=~/splunk-sdk-python.")

                    
# Splunk imports
from splunklib.client import connect
import splunklib.results as results

''' Splunk connection properties '''
HOST = "localhost"
PORT = 8089
USERNAME = "splunk_username"
PASSWORD = "splunk_password"

service = None

''' Splunk search query and sort query '''            
searchQuery =  """ search {} EventContent
                             | extract pairdelim="," kvdelim=":=" 
                             | rex field=_raw "EventContent:(?<EventContent>.*)" 
                             | rex field=_raw "EventSegment:(?<EventSegment>.*?),"
                             | rex field=_raw "EventSegment:(?<EventLastSegment>.*?),"
                             | rex field=_raw "EventSegmentCount:(?<EventSegmentCount>.*?),"
                             | sort _time, EventSegmentCount
                             | table _time, Admin, Domain_Name, Name, Id, EventSegment, EventLastSegment, EventContent
               """
pattern  = re.compile(r'^.*?(<.*?diff:.*)?$', re.MULTILINE)
        


def splunkConnect():    
    ''' Connect to splunk '''
    global service
    service = connect( host=HOST,
                    port=PORT,
                    username=USERNAME,
                    password=PASSWORD)

    socket.setdefaulttimeout(None)


    
DEBUG_LOG_LEVEL_ALL = False
def log(msg):
    ''' Logging routine '''
    if DEBUG_LOG_LEVEL_ALL:
        print(msg)


''' Map holding policy xml from past audit events '''
oldPolicyCache = {}          
''' Map holding policy xml that requires xml concatination '''
partialPolicyMaps = {}       
''' Policy ids that cannot be diff'd since only one Audit event is available with selection criteria '''
unDiffedPolicyMaps = {}      



def getEffectivePolicy(policyDict):
    ''' Fetch the whole policy for diff or partial policy for conflating '''
    policy = Policy(policyDict)
    effectivePolicy = policy
    log("getEffectivePolicy: " + policy.id)

    try:
        if policy.id in partialPolicyMaps:
            # Chunked xml 
            effectivePolicy = partialPolicyMaps[policy.id]
            effectivePolicy.update(policy)
            log("getEffectivePolicy(): Fetched  effectivePolicy from partialPolicyMaps : " + effectivePolicy.id + " " + effectivePolicy.name)
        
        
            if effectivePolicy.isXmlComplete:
                p = partialPolicyMaps.pop(policy.id, None)
                log("getEffectivePolicy(): XmlComplete: Removed item from partialPolicyMaps: " + p.id)

        elif not policy.isXmlComplete:
            partialPolicyMaps[policy.id] = policy;
            log("getEffectivePolicy(): Updated partialPolicyMaps with: " + policy.id + " " + policy.name)
        elif policy.isXmlComplete:
            log("getEffectivePolicy(): " + policy.toString())
            
        log("getEffectivePolicy(): effectivePolicyName: " + effectivePolicy.name);
    
    except Exception as e:
        print("ERROR: getEffectivePolicy(): {}\n{}".format(effectivePolicy.toString(), str(e)))
    
    return effectivePolicy
    

   
class Policy:
        ''' Data type representing SEPM Policy object '''
        def __init__(self, dictPolicy):
            #log(dictPolicy)
            if not 'EventContent' in dictPolicy:
                return
            
            self.domain = dictPolicy['Domain_Name']
            self.user= dictPolicy['Admin']
            self.time = dictPolicy['_time']
            self.xml = dictPolicy['EventContent']
            
            if 'EventSegment' in dictPolicy:
                self.id = dictPolicy['EventSegment']
                self.isXmlComplete = False
            elif 'EventLastSegment' in dictPolicy:
                self.id = dictPolicy['EventLastSegment']
                self.isXmlComplete = True 
            elif 'EventContent' in dictPolicy and 'Id' in dictPolicy:
                self.id = dictPolicy['Id']
                self.isXmlComplete = True 
            else:
                log("Error: Unknown Policy object: " + self.toString());
                
            #The following fields maybe optional in chunked policy logs
            self.name = dictPolicy.get('Name', "<chunked>")
            log("Policy object: " + self.toString());
            
            
        def update(self, policyChunk):
            prevChunk = self.xml
            curChunk = policyChunk.xml
            
            self.xml = prevChunk + curChunk
            log("Debug: Chunk xml after concat\n " + self.xml)
            self.isXmlComplete = policyChunk.isXmlComplete
                
            log("Partial policy update done: " + self.toString())
        
            
        def toString(self):
            if DEBUG_LOG_LEVEL_ALL:
                return 'Policy Name: {}, Id: {}, Domain: {}, User: {}, UpdateTime: {}, isXmlSplit: {}'.format(
                            self.name, self.id, self.domain, self.user, self.time, not self.isXmlComplete)
            else:
                return 'Policy Name: {}, Id: {}, Domain: {}, User: {}, UpdateTime: {}'.format(
                            self.name, self.id, self.domain, self.user, self.time)
            
                
        def getXmlForDiff(self):
            return self.xml
        
        
        def toStringWithXml(self):
            return self.toString() + '\nXml:\n{}'.format(self.getXmlForDiff())
       

def diff(newPolicy):
    ''' Perfors xml diff against current and last xml content for the policy with same ID '''
    policyId = newPolicy.id
    
    if policyId in oldPolicyCache:
        oldPolicy = oldPolicyCache[policyId]
    
        print(newPolicy.toString())
        try:
            d = main.diff_texts(oldPolicy.getXmlForDiff(), newPolicy.getXmlForDiff(),  formatter=formatting.DiffFormatter())
            print("Diff info:\n{}".format(d))

            print("XML diff excerpt:")
            d = main.diff_texts(oldPolicy.getXmlForDiff(), newPolicy.getXmlForDiff(),  formatter=formatting.XMLFormatter(normalize=formatting.WS_BOTH, pretty_print=True))
            listLines = pattern.split(d)
            
            for line in listLines:
                if (isinstance(line, str) and len(line) > 4):
                    print(line)
            
            unDiffedPolicyMaps.pop(policyId, None)
        
        except Exception as e:
            print ("Error: {}\nOldPolicy:\n{}\nNewPolicy:\n{}".format(e, oldPolicy.toStringWithXml(), newPolicy.toStringWithXml()))
        
    else:
         unDiffedPolicyMaps[policyId] = 1
    
    oldPolicyCache[policyId]  = newPolicy
    print("\n")



def getUndiffedPolicies():
    ''' Churns out all non diffable policies '''
    for policyId in unDiffedPolicyMaps:
        policy = oldPolicyCache[policyId]
        print('\nWARNING: Earlier policy unavailable for diff, adjust time window.\n{}'.format(policy.toStringWithXml()))



def processPolicyAuditLogsFromSplunk():
    ''' Fetches audit logs related to policies from splunk and does xml diff '''
    cmdlineArgs = ""
    if (len(sys.argv) > 1):
        cmdlineArgs = " ".join(sys.argv[1:])
        
    search = searchQuery.format(cmdlineArgs)
    response = service.jobs.oneshot(search)
    
    reader = results.ResultsReader(response)
    for dictPolicy in reader:
        if not isinstance(dictPolicy, dict):
            continue

        try:
            if 'EventContent' in dictPolicy:
                #log(dictPolicy)
                policy = getEffectivePolicy(dictPolicy)
                
                if policy.isXmlComplete:
                        diff(policy)
                    
        except Exception as e:
            pprint("Exception: {}".format(str(e)))

                        

def run_main():
    usage = """usage: SepmPolicyDiffs.py <source> [search-filters]\n
               SepmPolicyDiff.py source="tcp:514" 
            """
    splunkConnect()
    processPolicyAuditLogsFromSplunk()
    getUndiffedPolicies()



if __name__ == "__main__":
    run_main()
