<jupyter_start><jupyter_text>Guided Webshell Investigation - MDE Microsoft Sentinel EnrichmentsNotebook Version: 1.0Python Version: Python 3.6Data Sources Required: MDE SecurityAlert, W3CIIS Log (or similar web logging)This notebook investigates Microsoft Defender for Endpoint (MDE) webshell alerts. The notebook will guide you through steps to collect MDE alerts for webshell activity and link them to server access logs to identify potential attackers.Configuration Required!This Notebook presumes you have Microsoft Sentinel Workspace settings configured in a config file. If you do not have this in place please read the docs and use this notebook to test.How to use:This notebook provides a step-by-step investigation to understand MDE webshell alerts on your server. While our example uses IIS logging this notebook can be converted to support any web log type.After congiuration you can investigate two scenarios, a webshell file alert or a webshell command execution alert. For each of these we will need to retrieve different data, the notebook contains branching execution at Step 3 to enable this.Below you'll find a more detailed description of the two types of investigation    Shell File Alert    This alert type will fire when a file that is suspected to be a webshell appears on disk. For this investigation we will start with a known filename that is a suspected shell (e.g. Setconfigure.aspx) and we will try to understand how this webshell was placed on the server.    Shell Command Execution Alert    This alert type will fire when a command is executed on your web server that is suspicious. For this investigation we start with the command line that was executed and the time window that execution took place.For both of the above alert types this notebook will allow you to find the following information:    The attacker IP    The attacker User Agent    The website name the attacker interacted with    The location of the shell on your serverOnce we have that information this notebook will allow you to investigate the attacker IP, User Agent or both to discover:    The files the attacker accessed prior to the installation of the shell    The first time the attacker accessed your server Notebook InitializationThis cell:Checks for the correct Python versionChecks versions and optionally installs required packagesImports the required packages into the notebookSets a number of configuration options.This should complete without errors. If you encounter errors or warnings look at the following two notebooks:TroubleShootingNotebooksConfiguringNotebookEnvironmentYou may also need to do some additional configuration to successfully use functions such as Threat Intelligence service lookup and Geo IP lookup. See the Configuration section at the end of the notebook and the ConfiguringNotebookEnvironment.<jupyter_code>from pathlib import Path
from IPython.display import display, HTML

REQ_PYTHON_VER = "3.6"
REQ_MSTICPY_VER = "1.0.0"

display(HTML("<h3>Starting Notebook setup...</h3>"))

# If not using Azure Notebooks, install msticpy with
# %pip install msticpy
from msticpy.nbtools import nbinit
nbinit.init_notebook(
    namespace=globals()
);
import ipywidgets as widgets
from ipywidgets import HBox

try: 
    pick_time_range = widgets.Dropdown(
        options=['30d', '60d', '90d'],
        decription="Time range",
        disabled=False,
     )
    
    workspaces_available = WorkspaceConfig().list_workspaces()
    if not workspaces_available:
        def_config = WorkspaceConfig()
        workspaces_available = {
            def_config["workspace_id"]: def_config["workspace_name"]
        }
    
    target_workspace = widgets.Dropdown(
        options=workspaces_available.keys(), 
        decription="Workspace")

    display(HBox([pick_time_range, target_workspace]))
except RuntimeError:
    md("""You do not have any Workspaces configured in your config files.
       Please run the https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb
       to setup these files before proceeding""" ,'bold')
# This time period is used to determine how far back the analytic looks, e.g. 1h, 3d, 7d, 1w
# If you expereince timeout errors or the notebook is returning too much data, try lowering this
time_range = pick_time_range.value
workspace_name = target_workspace.value
# Collect Microsoft Sentinel Workspace Details from our config file and use them to connect
import warnings
try:
    # Update to WorkspaceConfig(workspace="WORKSPACE_NAME") to get alerts from a Workspace other than your default one.
    # Run WorkspaceConfig().list_workspaces() to see a list of configured workspaces
    ws_config = WorkspaceConfig(workspace=workspace_name)
    ws_id = ws_config['workspace_id']
    ten_id = ws_config['tenant_id']
    md("Workspace details collected from config file")
    with warnings.catch_warnings():
        warnings.simplefilter(action="ignore")
        qry_prov = QueryProvider(data_environment='LogAnalytics')
    qry_prov.connect(connection_str=ws_config.code_connect_str)
except RuntimeError:
    md("""You do not have any Workspaces configured in your config files.
       Please run the https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb
       to setup these files before proceeding""" ,'bold')
# This cell will collect an alert summary to help you decide which investigation to launch
alert_summary_query = f'''
let timeRange = {time_range};  
SecurityAlert  
| where ProviderName =~ "MDATP"  
| where DisplayName has_any("Possible IIS web shell", "Possible IIS compromise", "Suspicious processes indicative of a web shell", "A suspicious web script was created", "Possible web shell installation")
| extend AlertType = iff(DisplayName has_any("Possible IIS web shell", "Possible IIS compromise", "Suspicious processes indicative of a web shell", "A suspicious web script was created"), "Webshell Command Alerts", "Webshell File Alerts")
| summarize count(AlertType) by AlertType
| project AlertType, NumberOfAlerts=count_AlertType 
'''
display(HTML('<h2>Alert Summary</h2><p>The following alert types have been found on your server:'))
alertout = qry_prov.exec_query(alert_summary_query)
display(alertout)

if (not isinstance(alertout, pd.DataFrame)) or alertout.empty:
    print("No MDE Webshell alerts found.")
    print("If you think that this is not correct, please check the time range you are using.")<jupyter_output><empty_output><jupyter_text>Before you continue! Now it's time to select which type of investigation you would like to try. Above we have provided a summary of the high-level alert types present on your server, if the above table is blank no alerts were found.    If the table is empty, this notebook has no alerts to work with and will produce errors in subsequent cells.If you have alerts you have a couple of different options. You can click the links to jump to the start of the investigation. Shell file alert Investigation: If you would like to conduct an investigation into an ASPX file that has been detected by Microsoft Defender ATP please run the code block beneath "Begin File Investigation"Shell command alert Investigation: If you would like to conduct an investigation into suspicious command execution on your web server please run the code block below "Begin Command Investigation" Step 3: Begin File InvestigationWe can now begin our investigation into a webshell file that has been placed on a system in your network. We'll start by collecting relevant events from MDE.<jupyter_code># First the notebook collects alerts from MDE with the following query
display(HTML('<h3>Collecting relevant alerts from MDE</h3>'))

mde_events_query = f'''
let timeRange = {time_range};
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
SecurityAlert
| where TimeGenerated > ago(timeRange)
| where ProviderName == "MDATP"
| where DisplayName =~ "Possible web shell installation" 
| extend alertData = parse_json(Entities)
| mvexpand alertData
| where alertData.Type == "file"
| where alertData.Name has_any(scriptExtensions)
| extend filename = alertData.Name, directory = alertData.Directory 
| project TimeGenerated, filename, directory
'''

aspx_data = qry_prov.exec_query(mde_events_query)
if (not isinstance(aspx_data, pd.DataFrame)) or aspx_data.empty:
    print("No MDE Webshell alerts found. Please try a different time range.")
    raise ValueError("No MDE Webshell alerts found. Cannot continue")

shells = aspx_data['filename']

# Everything below is presentational
pick_shell = widgets.Dropdown(
    options=shells,
    decription="Webshells",
    disabled=False,
 )

if isinstance(aspx_data, pd.DataFrame) and not aspx_data.empty:
    display(HTML('<p>Below you can see the filename, the directory it was found in, and the time it was found.</p><p>Please select a webshell to investigate before you continue:</p>'))
    display(aspx_data)
    display(pick_shell)
    display(HTML('<hr>'))
    display(HTML('<h1>Collect Enrichment Events</h1>'))
    display(HTML('<p>Now we will enrich this webshell event with additional information before continuing to find the attacker.</p>'))
else:
    md_warn('No relevant alerts were found in your MDE logs, try expanding your timeframe in the config.')
# Now collect enrichments from the W3CIIS log table
dfindex = pick_shell.index
filename = aspx_data.loc[[dfindex]]['filename'].values[0]
directory = aspx_data.loc[[dfindex]]['directory'].values[0]
timegenerated = aspx_data.loc[[dfindex]]['TimeGenerated'].values[0]

# Check the directory matches
directory_split = directory.split("\\")
first_directory = directory_split[-1]

# This query will collect  file accessed on the server within the same time window
iis_query = f'''
let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
W3CIISLog
| where TimeGenerated >= datetime("{timegenerated}") - 10s
| where TimeGenerated <= datetime("{timegenerated}") + 10s
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1] | extend firstDir = splitUriStem[-2]
| where FileName == "{filename}" and firstDir == "{first_directory}"
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by AttackerIP=cIP, AttackerUserAgent=csUserAgent, SiteName=sSiteName, ShellLocation=csUriStem
| order by StartTime asc
'''

if isinstance(iis_data, pd.DataFrame) and not iis_data.empty:
    iis_data = qry_prov.exec_query(iis_query)
    display(HTML('<div style="border-left: 6px solid #ccc; border-left-width: 6px; border-left-style: solid; padding: 0.01em 16px; border-color:#00cc69; background-color:#e6fff3;"><h3>Enrichment complete!<br> Please <a href="#Step-1:-Find-the-Attacker">click here</a> to continue your investigation.</h3><br></div><hr>'))
else:
    if iis_data.empty:
        md_warn('No events were found in W3CIISLog')
    else:
        md_warn('The query failed, it may have timed out')<jupyter_output><empty_output><jupyter_text>Step 3: Begin Command InvestigationTo begin the investigation into a command that has been executed by a webshell on your network, we will begin by collecting MDE data.<jupyter_code>command_investigation_query = f'''
let timeRange = {time_range}; 
let alerts = SecurityAlert  
| where TimeGenerated > ago(timeRange) 
| extend alertData = parse_json(Entities), recordGuid = new_guid(); 
let shellAlerts = alerts 
| where ProviderName =~ "MDATP"  
| mvexpand alertData 
| where alertData.Type == "file" and alertData.Name == "w3wp.exe" 
| distinct SystemAlertId 
| join kind=inner (alerts) on SystemAlertId; 
let alldata = shellAlerts  
| mvexpand alertData 
| extend Type = alertData.Type; 
let filedata = alldata  
| extend id = tostring(alertData.$id)  
| extend ImageName = alertData.Name  
| where Type == "file" and ImageName != "w3wp.exe" 
| extend imagefileref = id;  
let commanddata = alldata  
| extend CommandLine = tostring(alertData.CommandLine)  
| extend creationtime = tostring(alertData.CreationTimeUtc)  
| where Type =~ "process"  
| where isnotempty(CommandLine)  
| extend imagefileref = tostring(alertData.ImageFile.$ref); 
let hostdata = alldata 
| where Type =~ "host" 
| project HostName = tostring(alertData.HostName), DnsDomain = tostring(alertData.DnsDomain), SystemAlertId 
| distinct HostName, DnsDomain, SystemAlertId; 
filedata  
| join kind=inner (  
commanddata  
) on imagefileref 
| join kind=inner (hostdata) on SystemAlertId 
| project DisplayName, recordGuid, TimeGenerated, ImageName, CommandLine, HostName, DnsDomain
'''

cmd_data = qry_prov.exec_query(command_investigation_query)

if isinstance(cmd_data, pd.DataFrame) and not cmd_data.empty:
    display(HTML('''<h2>Step 3.1: Select a command to investigate</h2>
    <p>Below you will find the suspicious commands that were executed. Matching GUIDs indicate that the events were linked and likely executed within seconds of each other, 
    for the purpose of the investigation you can select either as the default time windows are wide enough to encapsulate both events. There's a full breakdown of the fields below.</p>
    <ul>
    <li>DisplayName: The MDE alert display name</li>
    <li>recordGuid: A GUID used to track previoulsy linked events</li>
    <li>TimeGenerated: The time the log entry was made</li>
    <li>ImageName: The executing process image name</li>
    <li>CommandLine: The command line that was executed</li>
    <li>HostName: The host name of the impacted machine</li>
    <li>DnsDomain: The domain of the impacted machine</li>
    </ul>
    <p>Note: The GUID generated here will change with each execution and is used only by the notebook.</p>'''))


    command = cmd_data['recordGuid']

    pick_cmd = widgets.Dropdown(
        options=command,
        decription="Commands",
        disabled=False,
     )

    display(HTML('<h3>Select the GUID associated with the command you wish to investigate.</h3>'))
    display(pick_cmd)
    display(HTML('<hr><h2>Step 3.2: Execute to Collect Events</h2><p>Please select an access threshold, by default the script will look for files on the server that have been accessed by fewer than 3 IP addresses</p>'))
    
    access_threshold = widgets.IntSlider(
        value=3,
        min=0,
        max=15,
        step=1,
        decription="Access Threshold",
        disabled=False,
        orientation='horizontal',
        readout=True,
        readout_format='d'
     )
    display(access_threshold)
else:
    if iis_data.empty:
        md_warn('No events were found in SecurityAlert. Continuing will result in errors.')
    else:
        md_warn('The query failed, it may have timed out. Continuing will result in errors.')
dfindex = pick_cmd.index
imagename = cmd_data.loc[[dfindex]]['ImageName'].values[0]
commandline = cmd_data.loc[[dfindex]]['CommandLine'].values[0]
creationtime = cmd_data.loc[[dfindex]]['TimeGenerated'].values[0]

# Retrieves access to script files on the web server using logs stored in W3CIIS.
# Checks for how many unique client IP addresses access the file, uses access_threshold
script_data_query = f'''
let scriptExtensions = dynamic([".php", ".jsp", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
let alldata = W3CIISLog
| where TimeGenerated >= datetime("{creationtime}") - 30s
| where TimeGenerated <= datetime("{creationtime}") + 30s
| where csUriStem has_any(scriptExtensions)
| extend splitUriStem = split(csUriStem, "/")
| extend FileName = splitUriStem[-1] | extend firstDir = splitUriStem[-2]
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by AttackerIP=cIP, AttackerUserAgent=csUserAgent, csUriStem, filename=tostring(FileName), tostring(firstDir)
| order by StartTime asc;
let fileprev = W3CIISLog
| summarize accessCount=dcount(cIP) by csUriStem;
alldata
| join (
    fileprev
) on csUriStem 
| extend ShellLocation = csUriStem
| project-away csUriStem, csUriStem1
| where accessCount <= {access_threshold}
'''
aspx_data = qry_prov.exec_query(script_data_query)

if isinstance(aspx_data, pd.DataFrame) and not aspx_data.empty:
    display(HTML('<h2>Step 3.3: File to investigate</h2><p>The files in the drop down below were accessed on the web server (and are therefore in W3CIIS Log) within 30 seconds of the command executing.</p><p>By default the notebook will only show files that have been accessed by a single client IP or UA.</p>'))


    shells = aspx_data['ShellLocation']

    pick_shell = widgets.Dropdown(
        options=shells,
        decription="Webshells",
        disabled=False,
     )

    aspx_data_display = aspx_data
    aspx_data_display = aspx_data_display.drop(['AttackerIP', 'AttackerUserAgent', 'firstDir', 'EndTime'], axis=1)
    aspx_data_display.rename(columns={'filename':'ShellName', 'StartTime':'AccessTime'}, inplace=True)


    display(HTML('Please select which file you would like to investigate:'))
    #display(aspx_data)
    display(pick_shell)
    display(HTML('<hr><h2>Step 3.4: Enrich</h2>'))
else:
    if aspx_data.empty:
        md_warn('No events were found in W3CIISLog. Continuing will result in errors.')
    else:
        md_warn('The query failed, it may have timed out. Continuing will result in errors.')
if isinstance(aspx_data, pd.DataFrame) and not aspx_data.empty:
    dfindex = pick_shell.index
    filename = aspx_data.loc[[dfindex]]['filename'].values[0]
    timegenerated = aspx_data.loc[[dfindex]]['StartTime'].values[0]
    
    #Check the directory matches
    first_directory = aspx_data.loc[[dfindex]]['firstDir'].values[0]

    iis_query = f'''
    let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
    W3CIISLog
    | where TimeGenerated >= datetime("{timegenerated}") - 30s
    | where TimeGenerated <= datetime("{timegenerated}") + 30s
    | where csUriStem has_any(scriptExtensions)
    | extend splitUriStem = split(csUriStem, "/")
    | extend FileName = splitUriStem[-1] | extend firstDir = splitUriStem[-2]
    | where FileName == "{filename}" and firstDir == "{first_directory}"
    | summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated) by AttackerIP=cIP, AttackerUserAgent=csUserAgent, SiteName=sSiteName, ShellLocation=csUriStem
    | order by StartTime asc
    '''

    iis_data = qry_prov.exec_query(iis_query)
else:
    md_warn('There is no data in an object that should have data. A previous step has likely failed, we cannot continue.')

if isinstance(iis_data, pd.DataFrame) and not iis_data.empty:
    display(HTML('<div style="border-left: 6px solid #ccc; border-left-width: 6px; border-left-style: solid; padding: 0.01em 16px; border-color:#00cc69; background-color:#e6fff3;"><h3>Enrichment complete!<br> Please <a href="#Step-4:-Find-the-Attacker">click here</a> to continue your investigation.</h3><br></div><hr>'))
else:
    if iis_data.empty:
        md_warn('No events were found in W3CIISLog. Continuing will result in errors.')
    else:
        md_warn('The query failed, it may have timed out. Continuing will result in errors.')<jupyter_output><empty_output><jupyter_text>Step 4: Find the Attacker<jupyter_code>attackerip = iis_data['AttackerIP']
attackerua = iis_data['AttackerUserAgent']

pick_ip = widgets.Dropdown(
    options=attackerip,
    decription="IP Addresses",
    disabled=False,
 )
pick_ua = widgets.Dropdown(
    options=attackerua,
    decription="User Agents",
    disabled=False,
 )
pick_window = widgets.Dropdown(
    options=['30m','1h','5h','7h', '1d', '3d','7d'],
    decription="Window",
    disabled=False,
 )
pick_investigation = widgets.Dropdown(
    options=['Investigate Both', 'Investigate IP','Investigate Useragent'],
    decription="What should we investigatew?",
    disabled=False,
 )

display(HTML('<h2>Candidate Attacker IP Addresses</h2>'))
md('The following attacker IP addresses accessed the webshell during the alert window, continue to Step 5 to choose which to investigate.')
display(iis_data)

display(HTML('<hr><br><h2>Step 5: Select Investigation Parameters</h2>'))
display(HTML('<h3>Attacker To Investigate</h3><p>Now it is time to hone in on our attacker. If you have multiple attacker indicators you can repeat from this step.</p><p>Select parameters to investigate, the default selection is the earliest access within the alert window:</p>'))
display(HBox([pick_ip, pick_ua, pick_investigation]))
widgets.jslink((pick_ip, 'index'), (pick_ua, 'index'))

display(HTML('<h3>Previous file access window</h3><p>To determine what files were accessed immediately before the shell, please pick the window we\'ll use to look back:</p>'))
display(pick_window)

display(HTML('<hr><h2>Step 6: Collect Attacker Enrichments</h2><p>Finally execute the below cell to collect additional details about the attacker.</p>'))
queryWindow = pick_window.value # Lookback window
investigation_param = pick_investigation.index # 0 = both, 1 = ip, 2 = ua
dfindex = pick_ip.index # contains dataframe index (int)

attackerip = str(pick_ip.value)
attackerua = iis_data.loc[[dfindex]]['AttackerUserAgent'].values[0]
attackertime = iis_data.loc[[dfindex]]['StartTime'].values[0]
sitename = iis_data.loc[[dfindex]]['SiteName'].values[0]
shell_location = iis_data.loc[[dfindex]]['ShellLocation'].values[0]

access_data = ['','']
first_server_access_data = ['','']

def iis_access_ip():
    iis_access_ip = f'''
    let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
    W3CIISLog
    | where TimeGenerated >= datetime("{attackertime}") - {queryWindow}
    | where TimeGenerated <= datetime("{attackertime}")
    | where sSiteName == "{sitename}"
    | where cIP == "{attackerip}"
    | order by TimeGenerated desc
    | project TimeAccessed=TimeGenerated, SiteName=sSiteName, ServerIP=sIP, FilesTouched=csUriStem, AttackerP=cIP
    | where FilesTouched has_any(scriptExtensions)
    | order by TimeAccessed asc
    '''
    
    #Find the first time the attacker accessed the webserver
    first_server_access_ip = f'''
    W3CIISLog
    | where TimeGenerated > ago(30d)
    | where sSiteName == "{sitename}"
    | where cIP == "{attackerip}"
    | order by TimeGenerated asc
    | take 1
    | project TimeAccessed=TimeGenerated, Site=sSiteName, FileAccessed=csUriStem
    | order by TimeAccessed asc
    '''
    
    access_data = qry_prov.exec_query(iis_access_ip)
    
    first_server_access_data = qry_prov.exec_query(first_server_access_ip)
    
    return access_data, first_server_access_data

def iis_access_ua():
    iis_access_ua = f'''
    let scriptExtensions = dynamic([".php", ".jsp", ".js", ".aspx", ".asmx", ".asax", ".cfm", ".shtml"]);
    W3CIISLog
    | where TimeGenerated >= datetime("{attackertime}") - {queryWindow}
    | where TimeGenerated <= datetime("{attackertime}")
    | where sSiteName == "{sitename}"
    | where csUserAgent == "{attackerua}"
    | order by TimeGenerated desc
    | project TimeAccessed=TimeGenerated, SiteName=sSiteName, ServerIP=sIP, FilesTouched=csUriStem, AttackerP=cIP, AttackerUserAgent=csUserAgent
    | where FilesTouched has_any(scriptExtensions)
    | order by TimeAccessed asc
    '''
    
    #Find the first time the attacker accessed the webserver
    first_server_access_ua = f'''
    W3CIISLog
    | where TimeGenerated > ago(30d)
    | where sSiteName == "{sitename}"
    | where csUserAgent == "{attackerua}"
    | order by TimeGenerated asc
    | take 1
    | project TimeAccessed=TimeGenerated, Site=sSiteName, FileAccessed=csUriStem
    | order by TimeAccessed asc
    '''
    
    access_data =  qry_prov.exec_query(iis_access_ua)
    first_server_access_data =  qry_prov.exec_query(first_server_access_ua)
    
    return access_data, first_server_access_data
    
first_shell_index = None
if investigation_param == 1:
    display(HTML('<p>Querying for attacker IP</p>'))
    result = iis_access_ip()
    access_data[0] = result[0]
    first_server_access_data[0] = result[1]
    first_shell_index = access_data[0][access_data[0].FilesTouched==shell_location].first_valid_index()
    
elif investigation_param == 2:
    display(HTML('<p>Querying for attacker UA</p>'))
    result = iis_access_ua()
    access_data[1] = result[0]
    first_server_access_data[1] = result[1]
    first_shell_index = access_data[1][access_data[1].FilesTouched==shell_location].first_valid_index()
    
elif investigation_param == 0:  
    display(HTML('<p>Querying for attacker IP and UA</p>'))
    result_ip = iis_access_ip()
    result_ua = iis_access_ua()
    
    access_data[0] = result_ip[0]
    access_data[1] = result_ua[0]
    
    first_server_access_data[0] = result_ip[1]
    first_server_access_data[1] = result_ua[1]
    
    first_shell_index = access_data[0][access_data[0].FilesTouched==shell_location].first_valid_index()
    first_shell_index_ua = access_data[1][access_data[1].FilesTouched==shell_location].first_valid_index()
    
display(HTML('<div style="border-left: 6px solid #ccc; border-left-width: 6px; border-left-style: solid; padding: 0.01em 16px; border-color:#00cc69; background-color:#e6fff3;"><h3>Enrichment complete!</h3><p>Continue to generate your report</p><br></div><hr><h2>Step 7: Generate Report</h2>'))
attackerua = attackerua.replace("+", " ")
display(HTML(f'''
<h2>  Attack Summary</h2>
<div style="border-left: 6px solid #ccc; border-left-width: 6px; border-left-style: solid; padding: 0.01em 16px; border-color:#1aa3ff; background-color: #f2f2f2;">
<p></p>
<p><b>Attacker IP: </b>{attackerip}</p>
<p><b>Attacker user agent: </b>{attackerua}</p>
<p><b>Webshell installed: </b>{shell_location}</p>
<p><b>Victim site: </b>{sitename}</p>
<br>
</div>
'''))

look_back = 0
# No results
if first_shell_index is None:
    first_shell_index = 0
# Our default look back is 5 files, if there are not 5 files we take what we can get
elif first_shell_index < 5:
    look_back = first_shell_index
else:
    look_back = 5

if investigation_param == 1:
    display(HTML('<h2>File history</h2>'))
    if first_shell_index > 0:
        print('The files the attacker IP \"'+attackerip+'\" accessed prior to the webshell installation were:')
        display(access_data[0][first_shell_index-look_back:first_shell_index+1])
    else:
        print(f'No files were access by the attacker prior to webshell install, try expaning the query window (currently:{queryWindow})')

    display(HTML('<h2>Earliest access</h2><p>In the last 30 days the earliest known access to the server from the attacker IP was:</p>'))
    display(first_server_access_data[0])
    
elif investigation_param == 2:
    display(HTML('<h2>File history</h2>'))
    if first_shell_index > 0:
        print('The files the attacker UA \"'+attackerua+'\" accessed prior to the webshell installation were:')
        display(access_data[1][first_shell_index-look_back:first_shell_index+1])
    else:
        print(f'No files were access by the attacker prior to webshell install, try expaning the query window (currently:{queryWindow})')

    display(HTML('<h2>Earliest access</h2><p>In the last 30 days the earliest known access to the server from the attacker UA was:</p>'))
    display(first_server_access_data[1])
    
elif investigation_param == 0:
    
    look_back_ua = 0
    if first_shell_index_ua is None:
        first_shell_index_ua = 0
    elif first_shell_index_ua < 5:
        look_back_ua = first_shell_index_ua
    else:
        look_back_ua = 5
    
    display(HTML('<h2>File history</h2>'))
    if first_shell_index > 0 or first_shell_index_ua > 0:
        print('The files the attacker IP \"'+attackerip+'\" accessed prior to the webshell installation were:')
        display(access_data[0][first_shell_index-look_back:first_shell_index+1])
        print('The files the attacker UA \"'+attackerua+'\" accessed prior to the webshell installation were:')
        display(access_data[1][first_shell_index_ua-look_back_ua:first_shell_index_ua+1])
    else:
        print(f'No files were access by the attacker prior to webshell install, try expanding the query window (currently:{queryWindow})')

    display(HTML('<h2>Earliest access</h2><p>In the last 30 days the earliest known access to the server from the attacker IP was:</p>'))
    display(first_server_access_data[0])
    display(HTML('<p>In the last 30 days the earliest known access to the server from the attacker UA was:</p>'))
    display(first_server_access_data[1])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10382724719101123, 'fraction_numerical': 0.00976123595505618, 'mean_word_length': 4.730583501006036, 'pattern_counts': {'":': 0, '<': 177, '<?xml version=': 0, '>': 182, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '150083', 'license': 'MIT', 'n_tokens_mistral': 8741, 'n_tokens_neox': 8401, 'n_words': 2723, 'path': 'Guided Investigation - MDE Webshell Alerts.ipynb', 'repo_name': 'argvssarma/AzureSentinel', 'size': '28480'}