diff --git a/.gitignore b/.gitignore index 9c3a086e..f1844f00 100644 --- a/.gitignore +++ b/.gitignore @@ -330,3 +330,5 @@ ASALocalRun/ .mfractor/ **/.ipynb_checkpoints/** **/Kqlmagic_temp_files/** +**/.mypy_cache/** +**/kqlmagic/** diff --git a/A Getting Started Guide For Azure Sentinel ML Notebooks.ipynb b/A Getting Started Guide For Azure Sentinel ML Notebooks.ipynb new file mode 100644 index 00000000..441129f1 --- /dev/null +++ b/A Getting Started Guide For Azure Sentinel ML Notebooks.ipynb @@ -0,0 +1,888 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started with Azure ML Notebooks and Azure Sentinel\n", + "**Notebook Version:** 1.0
\n", + " **Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", + " **Required Packages**:
\n", + " **Platforms Supported**:\n", + " - Azure Notebooks Free Compute\n", + " - Azure Notebooks DSVM\n", + " - OS Independent\n", + "\n", + "**Data Sources Required**:\n", + " - Log Analytics - SiginLogs (Optional)\n", + " - VirusTotal\n", + " - MaxMind\n", + " \n", + " \n", + "This notebook takes you through the basics needed to get started with Azure Notebooks and Azure Sentinel, and how to perform the basic actions of data acquisition, data enrichment, data analysis, and data visualization. These actions are the building blocks of threat hunting with notebooks and are useful to understand before running more complex notebooks. This notebook only lightly covers each topic but includes 'learn more' sections to provide you with the resource to deep dive into each of these topics. \n", + "\n", + "This notebook assumes that you are running this in an Azure Notebooks environment, however it will work in other Jupyter environments.\n", + "\n", + "**Note:**\n", + "This notebooks uses SigninLogs from your Azure Sentinel Workspace. If you are not yet collecting SigninLogs configure this connector in the Azure Sentinel portal before running this notebook.\n", + "This notebook also uses the VirusTotal API for data enrichment, for this you will require an API key which can be obtained by signing up for a free [VirusTotal community account](https://www.virustotal.com/gui/join-us)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## What is a Jupyter notebook?\n", + "You are currently reading a Jupyter notebook. [Jupyter](http://jupyter.org/) is an interactive development and data manipulation environment presented in a browser. Using Jupyter you can create documents, called Notebooks. These documents are made up of cells that contain interactive code, alongside that code's output, and other items such as text and images (what you are looking at now is a cell of Markdown text).\n", + "\n", + "The name, Jupyter, comes from the core supported programming languages that it supports: Julia, Python, and R. Whilst you can use any of these languages we are going to use Python in this notebook, in addition the notebooks that come with Azure Sentinel are all written in Python. Whilst there are pros, and cons to each language Python is a well-established language that has a large number of materials and libraries well suited for data analysis and security investigation, making it ideal for our needs.\n", + "\n", + "### Learn more:\n", + " - The [Infosec Jupyter Book](https://infosecjupyterbook.com/introduction.html) has more details on the technical working of Jupyter.\n", + " - [The Jupyter Project documentation](https://jupyter.org/documentation)\n", + "\n", + "---\n", + "## How to use a Jupyter notebook?\n", + "To use a Jupyter notebook you need a Jupyter server that will render the notebook and execute the code within it. This can take the form of a local [Jupyter installation](https://pypi.org/project/jupyter/), or a remotely hosted version such as [Azure Notebooks](https://notebooks.azure.com/). If you are reading this it is highly likely that you already have a Jupyter server that this notebook is using.\n", + "You can learn more about installing and running your own Jupyter server [here](https://realpython.com/jupyter-notebook-introduction/).\n", + "\n", + "### Using Azure Notebooks\n", + "If you accessed this notebook from Azure Sentinel, you are probably using Azure Notebooks to run this notebook. Azure Notebooks runs in the same way that a local Jupyter server with, except with the additional feature of integrated project management and file storage. When you open a notebook in Azure Notebooks the user interface is nearly identical to a standard Jupyter notebook experience.\n", + "\n", + "Before you can start running code in a notebook you need to make sure that it is connected to a Jupyter server and you have the correct type of kernel configured. For this notebook we are going to be using Python 3.6, hopefully Azure Notebooks has already loaded this kernel for you - you can check this by looking at the top left corner of the screen where you should see the currently connected kernel. \n", + "\n", + "![KernelIssue](https://github.com/Azure/Azure-Sentinel-Notebooks/raw/master/images/nb_img1.png)\n", + "\n", + "If this does not read Python 3.6 you can select the correct kernel by selecting Kernel > Change kernel from the top menu and clicking Python 3.6.\n", + "\n", + "> **Note**: the notebook works with Python 3.6, 3.7 or later. If you are using this notebook in Azure ML or another Jupyter environment you can choose any kernel that supports Python 3.6 or later\n", + "\n", + "![KernelPicker](https://github.com/Azure/Azure-Sentinel-Notebooks/raw/master/images/nb_img2.png)\n", + "\n", + "Once you have done this you should be ready to move onto a code cell.\n", + "> **Tip**: You can identify which cells are code by selecting them and looking at the drop down box at the center of the top menu. It will either read 'Code' (for interactive code cells), 'Markdown' (for Markdown text cells like this one), or RawNBConvert (these are just raw data and not interpreted by Jupyter - they can be used by tools that process notebook files, such as *nbconvert* to render the data into HTML or LaTeX). \n", + "\n", + "If you click on the cell below you should see this box change to 'Code'.\n", + "\n", + "### Learn More:\n", + "More details on Azure Notebooks can be found in the [Azure Notebooks documentation](https://docs.microsoft.com/en-us/azure/notebooks/) and the [Azure Sentinel documentation](https://docs.microsoft.com/en-us/azure/sentinel/notebooks).\n", + "\n", + "---\n", + "## Running code\n", + "Once you have selected a code cell you can run it by clicking the run button at the menu bar at the top, or by pressing Ctrl+Enter.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is our first code cell, it contains basic Python code.\n", + "# You can run a code cell by selecting it and clicking the Run button in the top menu, or by pressing Shift + Enter.\n", + "# Once you run a code cell any output from that code will be displayed directly below it.\n", + "print(\"Congratulations you just ran this code cell\")\n", + "y = 2+2\n", + "print(\"2 + 2 =\", y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Variables set within a code cell persist between cells meaning you can chain cells together" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "y + 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn More : \n", + " - The [Infosec Jupyter Book](https://infosecjupyterbook.com/) provides an infosec specific intro to Python.\n", + " - [Real Python](https://realpython.com/) is a comprehensive set of Python learnings and tutorials.\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that you understand the basics we can move onto more complex code.\n", + "\n", + "---\n", + "## Setting up the environment\n", + "Code cells behave in the same way your code would in other environments, so you need to remember about common coding practices such as variable initialization and library imports. \n", + "Before we execute more complex code we need to make sure the required packages are installed and libraries imported. At the top of many of the Azure Sentinel notebooks you will see large cells that will check kernel versions and then install and import all the libraries we are going to be using in the notebook, make sure you run this before running other cells in the notebook.\n", + "If you are running notebooks locally or via dedicated compute in Azure Notebooks library installs will persist but this is not the case with Azure Notebooks free tier, so you will need to install each time you run. Even if running in a static environment imports are required for each run so make sure you run this cell regardless." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import warnings\n", + "from IPython.display import display, HTML, Markdown\n", + "\n", + "REQ_PYTHON_VER=(3, 6)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", + "\n", + "display(HTML(\"

Starting Notebook setup...

\"))\n", + "# If you did not clone the entire Azure-Sentinel-Notebooks repo you may not have this file\n", + "if Path(\"./utils/nb_check.py\").is_file():\n", + " from utils.nb_check import check_python_ver, check_mp_ver\n", + "\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", + " try:\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", + " except ImportError:\n", + " !pip install --user --upgrade msticpy\n", + " if \"msticpy\" in sys.modules:\n", + " importlib.reload(sys.modules[\"msticpy\"])\n", + " else:\n", + " import msticpy\n", + " check_mp_ver(MSTICPY_REQ_VERSION)\n", + " \n", + "from msticpy.nbtools import nbinit\n", + "nbinit.init_notebook(\n", + " namespace=globals(),\n", + " extra_imports=[\"ipwhois, IPWhois, pyyaml\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Configuration\n", + "Once we have set up our Jupyter environment with the libraries that we'll use in the notebook, we need to make sure we have some configuration in place. Some of the notebook components need addtional configuration to connect to external services (e.g. API keys to retrieve Threat Intelligence data). This includes configuration for connection to our Azure Sentinel workspace, as well as some threat intelligence providers we will use later.\n", + "The easiest way to handle the configuration for these services is to store them in a msticpyconfig file (`msticpyconfig.yaml`). More details on msticpyconfig can be found here: https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html\n", + "\n", + "### Learn more: \n", + "- In this notebook we will setup the basic config we need to get started. If you need a more complete walk-through we have a separate notebook to help you: https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Azure-Sentinel-Notebooks GitHub repo contains an template msticpyconfig file ready to be populated. If you have run this notebook before you may have a msticpyconfig file already populated, the cell below allows you to checks if this file. If your config file does not contain details under Azure Sentinel > Workspaces, or TIProviders the following cells will populate these for you.
\n", + "If you want to see an example of what a populated msticpyconfig file should look like a samples is included in the repo as msticpyconfig-sample.yaml." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "def print_config():\n", + " with open('msticpyconfig.yaml') as f:\n", + " data = yaml.load(f, Loader=yaml.FullLoader)\n", + " print(yaml.dump(data))\n", + "try:\n", + " print_config()\n", + "except FileNotFoundError:\n", + " print(\"No msticpyconfig.yaml was found in your current directory.\")\n", + " print(\"We are downloading a template file for you.\")\n", + " import urllib\n", + " urllib.request.urlretrieve(\"https://raw.githubusercontent.com/Azure/Azure-Sentinel-Notebooks/master/msticpyconfig.yaml\", \"msticpyconfig.yaml\")\n", + " print_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you do not have and msticpyconfig file we can populate one for you. Before you do this you will need a few things.\n", + "\n", + "The first is the Workspace ID and Tenant ID of the Azure Sentinel Workspace you wish to connect to.\n", + "\n", + " - You can get the workspace ID by opening Azure Sentinel in the [Azure Portal](https://portal.azure.com) and selecting Settings > Workspace Settings. Your Workspace ID is displayed near the top of this page.\n", + "\n", + "- You can get your tenant ID (also referred to organization or directory ID) via [Azure Active Directory](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id)\n", + "\n", + "We are going to use [VirusTotal](https://www.virustotal.com) to enrich our Azure Sentinel data. For this you will need a VirusTotal API key, one of these can be obtained for free (as a personnal key) via the [VirusTotal](https://developers.virustotal.com/v3.0/reference#getting-started) website.\n", + "We are using VirusTotal for this notebook but we also support a range of other threat intelligence providers: https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html\n", + "

\n", + "In addition we are going to plot IP address locations on a map, in order to do this we are going to use [MaxMind](https://www.maxmind.com) to geolocate IP addresses which requires an API key. You can sign up for a free account and API key at https://www.maxmind.com/en/geolite2/signup. \n", + "

\n", + "Once you have these required items run the cell below and you will prompted to enter these elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "ws_id = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", + " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", + "ten_id = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", + " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", + "vt_key = nbwidgets.GetEnvironmentKey(env_var='VT_KEY',\n", + " prompt='Please enter your VirusTotal API Key:', auto_display=True)\n", + "mm_key = nbwidgets.GetEnvironmentKey(env_var='MM_KEY',\n", + " prompt='Please enter your MaxMind API Key:', auto_display=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " The cell below will now populate a msticpyconfig file with these values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "with open(\"msticpyconfig.yaml\") as config:\n", + " data = yaml.load(config, Loader=yaml.Loader)\n", + "data['AzureSentinel']\n", + "\n", + "workspace = {\"Default\":{\"WorkspaceId\": ws_id.value, \"TenantId\": ten_id.value}}\n", + "ti = {\"VirusTotal\":{\"Args\": {\"AuthKey\" : vt_key.value}, \"Primary\" : True, \"Provider\": \"VirusTotal\"}}\n", + "other_prov = {\"GeoIPLite\" : {\"Args\" : {\"AuthKey\" : mm_key.value, \"DBFolder\" : \"~/msticpy\"}, \"Provider\" : \"GeoLiteLookup\"}}\n", + "data['AzureSentinel']['Workspaces'] = workspace\n", + "data['TIProviders'] = ti\n", + "data['OtherProviders'] = other_prov\n", + "\n", + "with open(\"msticpyconfig.yaml\", 'w') as config:\n", + " yaml.dump(data, config)\n", + " \n", + "print(\"msticpyconfig.yaml updated\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now validate our configuration is correct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from msticpy.common.pkg_config import refresh_config, validate_config\n", + "refresh_config()\n", + "validate_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note** you may see warnings for missing providers when running this cell.\n", + "> This is not an issue as we will not be using all providers in this notebook\n", + "> so long as you get thie message \"No errors found.\" you are OK to proceed.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Getting Data\n", + "Now that we have configured the details necessary to connect to Azure Sentinel we can go ahead and get some data. We will do this with `QueryProvider()` from MSTICpy. \n", + "You can use the `QueryProvider` class to connect to different data sources such as MDATP, the Security Graph API, and the one we will use here, Azure Sentinel. \n", + "\n", + "### Learn more:\n", + " - More details on configuring and using QueryProviders can be found in the [MSTICpy Documentation](https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#instantiating-a-query-provider).\n", + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For now, we are going to set up a QueryProvider for Azure Sentinel, pass it the details for our workspace that we just stored in the msticpyconfig file, and connect. The connection process will ask us to authenticate to our Azure Sentinel workspace via [device authorization](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) with our Azure credentials. You can do this by clicking the device login code button that appears as the output of the next cell, or by navigating to https://microsoft.com/devicelogin and manually entering the code. Note that this authentication persists with the kernel you are using with the notebook, so if you restart the kernel you will need to re-authenticate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initalize a QueryProvider for Azure Sentinel\n", + "qry_prov = QueryProvider(\"LogAnalytics\")\n", + "\n", + "# Get the Azure Sentinel workspace details from msticpyconfig\n", + "try:\n", + " ws_config = WorkspaceConfig()\n", + " md(\"Workspace details collected from config file\")\n", + "except:\n", + " raise(\"No workspace settings are configured, please run the cells above to configure these.\")\n", + " \n", + "# Connect to Azure Sentinel with our QueryProvider and config details\n", + "# ws_config.code_connect_str is a feature of MSTICpy that creates the required connection string from details in our msticpyconfig\n", + "qry_prov.connect(connection_str=ws_config.code_connect_str)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have connected we can query Azure Sentinel for data, but before we do that we need to understand what data is avalaible to query. The QueryProvider object provides a way to get a list of tables as well as tables and table columns:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get list of tables in our Workspace\n", + "display(qry_prov.schema_tables [:5]) # We are outputting only the first 5 tables for brevity\n", + "# Get list of tables and thier columns\n", + "qry_prov.schema['SigninLogs'] # We are only displaying the columns for SigninLogs for brevity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MSTICpy includes a number of built in queries that you can run.
\n", + "You can list available queries with .list_queries() and get specific details about a query by calling it with \"?\" as a parameter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get a list of avaliable queries\n", + "qry_prov.list_queries()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get details about a query\n", + "qry_prov.Azure.list_all_signins_geo(\"?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can then run the query by calling it with the required parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "# set our query end time as now\n", + "end = datetime.now()\n", + "# set our query start time as 1 hour ago\n", + "start = end - timedelta(hours=1)\n", + "# run query with specified start and end times\n", + "logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", + "# display first 5 rows of any results\n", + "logons_df.head() # If you have no data you will just see the column headings displayed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another way to run queries is to pass a string format of a KQL query to the query provider, this will run the query against the workspace connected to above, and will return the data in a [Pandas DataFrame](https://pandas.pydata.org/). We will look at working with Pandas in a bit more detail later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define our query\n", + "test_query = \"\"\"\n", + "SigninLogs\n", + "| where TimeGenerated > ago(7d)\n", + "| take 10\n", + "\"\"\"\n", + "\n", + "# Pass that query to our QueryProvider\n", + "test_df = qry_prov.exec_query(test_query)\n", + "\n", + "# Check that we have some data\n", + "if isinstance(test_df, pd.DataFrame) and not test_df.empty:\n", + " # .head() returns the first 5 rows of our results DataFrame\n", + " display(test_df.head())\n", + "# If where is no data load some sample data to use instead\n", + "else:\n", + " md(\"You don't appear to have any SigninLogs - we will load sample data for you to use.\")\n", + " qry_prov = QueryProvider(\"LocalData\", data_paths=[\"nbdemo/data/\"], query_paths=[\"nbdemo/data/\"])\n", + " logons_df = qry_prov.Azure.list_all_signins_geo()\n", + " display(logons_df.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + " - You can learn more about the MSTICpy pre-defined queries in the [MSTICpy Documentation](https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#running-an-pre-defined-query)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Pandas\n", + "Our query results are returned in the form of a Pandas DataFrame. DataFrames are a core component of the Azure Sentinel notebooks and of MSTICpy and is used for both input and output formats.\n", + "Pandas DataFrames are incredibly versitile data structures with a lot of useful features, we will cover a small number of them here and we recommend that you check out the Learn more section to learn more about Pandas features.\n", + "
\n", + "
\n", + "### Displaying a DataFrame:\n", + "The first thing we want to do is display our DataFrame. You can either just run it or explicity display it by calling `display(df)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# For this section we are going to create a DataFrame from data we have saved in a csv file\n", + "df = pd.read_csv(\"https://raw.githubusercontent.com/microsoft/msticpy/master/tests/testdata/host_logons.csv\", index_col=[0] )\n", + "# Display our DataFrame\n", + "df # or display(df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note** if the dataframe variable (`df` in the example above) is the last statement in a \n", + "> code cell, Jupyter will automatically display it without using the `display()` function. \n", + "> However, if you want to display a DataFrame in the middle of \n", + "> other code in a cell you must use the `display()` function.\n", + "\n", + "You may not want to display the whole DataFrame and instead display only a selection of items. There are numerous ways to do this and the cell below shows some of the most widely used functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display the first 2 rows using head(): \", \"bold\")\n", + "display(df.head(2)) # we don't need to call display here but just for illustration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display the 3rd row using iloc[]: \", \"bold\")\n", + "df.iloc[3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Show the column names in the DataFrame \", \"bold\")\n", + "df.columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display just the TimeGenerated and TenantId columnns: \", \"bold\")\n", + "df[[\"TimeGenerated\", \"TenantId\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also choose to select a subsection of our DataFrame based on the contents of the DataFrame:\n", + "\n", + "> **Tip**: the syntax in these examples is using a technique called *boolean indexing*. \n", + ">
`df[]`\n", + "> returns all rows in the dataframe where the boolean expression is True\n", + ">
In the first example we telling pandas to return all rows where the column value of\n", + "> 'TargetUserName' matches 'MSTICAdmin'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display only rows where TargetUserName value is 'MSTICAdmin': \", \"bold\")\n", + "df[df['TargetUserName']==\"MSTICAdmin\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display rows where TargetUserName is either MSTICAdmin or adm1nistratror:\", \"bold\")\n", + "display(df[df['TargetUserName'].isin(['adm1nistrator', 'MSTICAdmin'])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our DataFrame call also be extended to add new columns with additional data if reqired:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df[\"NewCol\"] = \"Look at my new data!\"\n", + "display(df[[\"TenantId\",\"Account\", \"TimeGenerated\", \"NewCol\"]].head(2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "There is a lot more you can do with Pandas, the links below provide some useful resources:\n", + " - [Getting starting with Pandas](https://pandas.pydata.org/pandas-docs/stable/getting_started/index.html)\n", + " - [Infosec Jupyerbook intro to Pandas](https://infosecjupyterbook.com/notebooks/tutorials/03_intro_to_pandas.html)\n", + " - [A great list of Pandas hints and tricks](https://www.dataschool.io/python-pandas-tips-and-tricks/)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Enriching data\n", + "\n", + "Now that we have seen how to query for data, and do some basic manipulation we can look at enriching this data with additional data sources. For this we are going to use an external threat intelligence provider to give us some more details about an IP address we have in our dataset using the [MSTICpy TIProvider](\"https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html\") feature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "# Check if we have logon data already and if not get some\n", + "if not isinstance(logons_df, pd.DataFrame) or logons_df.empty:\n", + " # set our query end time as now\n", + " end = datetime.now()\n", + " # set our query start time as 1 hour ago\n", + " start = end - timedelta(days=1)\n", + " # run query with specified start and end times\n", + " logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", + " \n", + "# Create our TI provider\n", + "ti = TILookup()\n", + "# Get the first logon IP address from our dataset\n", + "ip = logons_df.iloc[1]['IPAddress']\n", + "# Look up the IP in VirusTotal\n", + "ti_resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", + "\n", + "# Format our results as a DataFrame\n", + "ti_resp = ti.result_to_df(ti_resp)\n", + "display(ti_resp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the [Pandas apply()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html) feature we can get results for all the IP addresses in our data set and add the lookup severity score as a new column in our DataFrame for easier reference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Take the IP address in each row, look it up against TI and return the seveirty score\n", + "def lookup_res(row):\n", + " ip = row['IPAddress']\n", + " resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", + " resp = ti.result_to_df(resp)\n", + " return resp[\"Severity\"].iloc[0]\n", + "\n", + "# Take the first 3 rows of data and copy they into a new DataFrame\n", + "enrich_logons_df = logons_df.iloc[:3].copy()\n", + "# Create a new column called TIRisk and populate that with the TI severity score of the IP Address in that row\n", + "enrich_logons_df['TIRisk'] = enrich_logons_df.apply(lookup_res, axis=1)\n", + "# Display a subset of columns from our DataFrame\n", + "display(enrich_logons_df[[\"TimeGenerated\", \"ResultType\", \"UserPrincipalName\", \"IPAddress\", \"TIRisk\"]])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "MSTICpy includes further threat intelligence capabilities as well as other data enrichment options. More details on these can be found in the [documentation](https://msticpy.readthedocs.io/en/latest/DataEnrichment.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Analyzing data\n", + "With the data we have collected we may wish to perform some analysis on it in order to better understand it. MSTICpy includes a number of features to help with this, and there are a vast array of other data analysis capabilities available via Python ranging from simple processes to complex ML models. We will start here by keeping it simple and look at how we can decode some Base64 encoded command line strings we have in order to allow us to understand their content." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from msticpy.sectools import base64unpack as b64\n", + "# Take our encoded Powershell Command\n", + "b64_cmd = \"powershell.exe -encodedCommand SW52b2tlLVdlYlJlcXVlc3QgaHR0cHM6Ly9jb250b3NvLmNvbS9tYWx3YXJlIC1PdXRGaWxlIEM6XG1hbHdhcmUuZXhl\"\n", + "# Unpack the Base64 encoded elements\n", + "unpack_txt = b64.unpack(input_string=b64_cmd)\n", + "# Display our results and transform for easier reading\n", + "unpack_txt[1].T" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also use MSTICpy to extract Indicators of Compromise (IoCs) from a dataset, this makes it easy to extract and match on a set of IoCs within our data. In the example below we take a US Cybersecurity & Infrastructure Security Agency (CISA) report and extract all domains listed in the report:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "# Set up our IoCExtract oject\n", + "ioc_extractor = iocextract.IoCExtract()\n", + "# Download our threat report\n", + "data = requests.get(\"https://www.us-cert.gov/sites/default/files/publications/AA20-099A_WHITE.stix.xml\")\n", + "# Extract domains listed in our report\n", + "iocs = ioc_extractor.extract(data.text, ioc_types=\"dns\")['dns']\n", + "# Display the first 5 iocs found in our report\n", + "list(iocs)[:5]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "There are a wide range of options when it comes to data analysis in notebooks using Python. Here are some useful resources to get you started:\n", + " - [MSITCpy DataAnalysis documentation](https://msticpy.readthedocs.io/en/latest/DataAnalysis.html)\n", + " - Scikit-Learn is a popular Python ML data analysis library, which has a useful [tutorial](https://scikit-learn.org/stable/tutorial/basic/tutorial.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Visualizing data\n", + "Visualizing data can provide an excellent way to analyse data, identify patterns and anomalies. Python has a wide range of data visualization capabilities each of which have thier own benefits and drawbacks. We will look at some basic capabilities as well as the in-build visualizations in MSTICpy.\n", + "


\n", + "**Basic Graphs**
\n", + "Pandas and Matplotlib provide the easiest and simplest way to produce simple plots of data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vis_q = \"\"\"\n", + "SigninLogs\n", + "| where TimeGenerated > ago(7d)\n", + "| sample 5\"\"\"\n", + "\n", + "# Try and query for data but if using sample data load that instead\n", + "try:\n", + " vis_data = qry_prov.exec_query(vis_q)\n", + "except FileNotFoundError:\n", + " vis_data = logons_df\n", + "\n", + "# Check we have some data in our results and if not use previously used dataset\n", + "if not isinstance(vis_data, pd.DataFrame) or vis_data.empty:\n", + " vis_data = logons_df\n", + "\n", + "# Plot up to the first 5 IP addresses\n", + "vis_data.head()[\"IPAddress\"].value_counts().plot.bar(\n", + " title=\"IP prevelence\", legend=False\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pie_df = vis_data.copy()\n", + " # If we have lots of data just plot the first 5 rows\n", + "pie_df.head()['IPAddress'].value_counts().plot.pie(legend=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + " - The [Infosec Jupyterbook](https://infosecjupyterbook.com/) includes a section on data visualization.\n", + " - [Bokeh Library Documentation](https://bokeh.org/)\n", + " - [Matplotlib tutorial](https://matplotlib.org/3.2.0/tutorials/index.html)\n", + " - [Seaborn visualization library tutorial](https://seaborn.pydata.org/tutorial.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Conclusion\n", + "This notebook has showed you the basics of using notebooks and Azure Sentinel for security investigaitons. There are many more things possible using notebooks and it is stronly encouraged to read the material we have referenced in the learn more sections in this notebook. You can also explore the other Azure Sentinel notebooks in order to take advantage of the pre-built hunting logic, and understand other analysis techniques that are possible.
\n", + "### Appendix:\n", + " - [Jupyter Notebooks: An Introduction](https://realpython.com/jupyter-notebook-introduction/)\n", + " - [Threat Hunting in the cloud with Azure Notebooks](https://medium.com/@maarten.goet/threat-hunting-in-the-cloud-with-azure-notebooks-supercharge-your-hunting-skills-using-jupyter-8d69218e7ca0)\n", + " - [MSTICpy documentation](https://msticpy.readthedocs.io/)\n", + " - [Azure Sentinel Notebooks documentation](https://docs.microsoft.com/en-us/azure/sentinel/notebooks)\n", + " - [The Infosec Jupyterbook](https://infosecjupyterbook.com/introduction.html)\n", + " - [Linux Host Explorer Notebook walkthrough](https://techcommunity.microsoft.com/t5/azure-sentinel/explorer-notebook-series-the-linux-host-explorer/ba-p/1138273)\n", + " - [Why use Jupyter for Security Investigations](https://techcommunity.microsoft.com/t5/azure-sentinel/why-use-jupyter-for-security-investigations/ba-p/475729)\n", + " - [Security Investigtions with Azure Sentinel & Notebooks](https://techcommunity.microsoft.com/t5/azure-sentinel/security-investigation-with-azure-sentinel-and-jupyter-notebooks/ba-p/432921)\n", + " - [Pandas Documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/index.html)\n", + " - [Bokeh Documentation](https://docs.bokeh.org/en/latest/)" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3.6", + "language": "python", + "name": "python36" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/A Getting Started Guide For Azure Sentinel Notebooks.ipynb b/A Getting Started Guide For Azure Sentinel Notebooks.ipynb new file mode 100644 index 00000000..6c52ce2b --- /dev/null +++ b/A Getting Started Guide For Azure Sentinel Notebooks.ipynb @@ -0,0 +1,947 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started with Azure Notebooks and Azure Sentinel\n", + "**Notebook Version:** 1.0
\n", + " **Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", + " **Required Packages**:
\n", + " **Platforms Supported**:\n", + " - Azure Notebooks Free Compute\n", + " - Azure Notebooks DSVM\n", + " - OS Independent\n", + "\n", + "**Data Sources Required**:\n", + " - Log Analytics - SiginLogs (Optional)\n", + " - VirusTotal\n", + " - MaxMind\n", + " \n", + " \n", + "This notebook takes you through the basics needed to get started with Azure Notebooks and Azure Sentinel, and how to perform the basic actions of data acquisition, data enrichment, data analysis, and data visualization. These actions are the building blocks of threat hunting with notebooks and are useful to understand before running more complex notebooks. This notebook only lightly covers each topic but includes 'learn more' sections to provide you with the resource to deep dive into each of these topics. \n", + "\n", + "This notebook assumes that you are running this in an Azure Notebooks environment, however it will work in other Jupyter environments.\n", + "\n", + "**Note:**\n", + "This notebooks uses SigninLogs from your Azure Sentinel Workspace. If you are not yet collecting SigninLogs configure this connector in the Azure Sentinel portal before running this notebook.\n", + "This notebook also uses the VirusTotal API for data enrichment, for this you will require an API key which can be obtained by signing up for a free [VirusTotal community account](https://www.virustotal.com/gui/join-us)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## What is a Jupyter notebook?\n", + "You are currently reading a Jupyter notebook. [Jupyter](http://jupyter.org/) is an interactive development and data manipulation environment presented in a browser. Using Jupyter you can create documents, called Notebooks. These documents are made up of cells that contain interactive code, alongside that code's output, and other items such as text and images (what you are looking at now is a cell of Markdown text).\n", + "\n", + "The name, Jupyter, comes from the core supported programming languages that it supports: Julia, Python, and R. Whilst you can use any of these languages we are going to use Python in this notebook, in addition the notebooks that come with Azure Sentinel are all written in Python. Whilst there are pros, and cons to each language Python is a well-established language that has a large number of materials and libraries well suited for data analysis and security investigation, making it ideal for our needs.\n", + "\n", + "### Learn more:\n", + " - The [Infosec Jupyter Book](https://infosecjupyterbook.com/introduction.html) has more details on the technical working of Jupyter.\n", + " - [The Jupyter Project documentation](https://jupyter.org/documentation)\n", + "\n", + "---\n", + "## How to use a Jupyter notebook?\n", + "To use a Jupyter notebook you need a Jupyter server that will render the notebook and execute the code within it. This can take the form of a local [Jupyter installation](https://pypi.org/project/jupyter/), or a remotely hosted version such as [Azure Notebooks](https://notebooks.azure.com/). If you are reading this it is highly likely that you already have a Jupyter server that this notebook is using.\n", + "You can learn more about installing and running your own Jupyter server [here](https://realpython.com/jupyter-notebook-introduction/).\n", + "\n", + "### Using Azure Notebooks\n", + "If you accessed this notebook from Azure Sentinel, you are probably using Azure Notebooks to run this notebook. Azure Notebooks runs in the same way that a local Jupyter server with, except with the additional feature of integrated project management and file storage. When you open a notebook in Azure Notebooks the user interface is nearly identical to a standard Jupyter notebook experience.\n", + "\n", + "Before you can start running code in a notebook you need to make sure that it is connected to a Jupyter server and you have the correct type of kernel configured. For this notebook we are going to be using Python 3.6, hopefully Azure Notebooks has already loaded this kernel for you - you can check this by looking at the top left corner of the screen where you should see the currently connected kernel. \n", + "\n", + "![KernelIssue](https://github.com/Azure/Azure-Sentinel-Notebooks/raw/master/images/nb_img1.png)\n", + "\n", + "If this does not read Python 3.6 you can select the correct kernel by selecting Kernel > Change kernel from the top menu and clicking Python 3.6.\n", + "\n", + "> **Note**: the notebook works with Python 3.6, 3.7 or later. If you are using this notebook in Azure ML or another Jupyter environment you can choose any kernel that supports Python 3.6 or later\n", + "\n", + "![KernelPicker](https://github.com/Azure/Azure-Sentinel-Notebooks/raw/master/images/nb_img2.png)\n", + "\n", + "Once you have done this you should be ready to move onto a code cell.\n", + "> **Tip**: You can identify which cells are code by selecting them and looking at the drop down box at the center of the top menu. It will either read 'Code' (for interactive code cells), 'Markdown' (for Markdown text cells like this one), or RawNBConvert (these are just raw data and not interpreted by Jupyter - they can be used by tools that process notebook files, such as *nbconvert* to render the data into HTML or LaTeX). \n", + "\n", + "If you click on the cell below you should see this box change to 'Code'.\n", + "\n", + "### Learn More:\n", + "More details on Azure Notebooks can be found in the [Azure Notebooks documentation](https://docs.microsoft.com/en-us/azure/notebooks/) and the [Azure Sentinel documentation](https://docs.microsoft.com/en-us/azure/sentinel/notebooks).\n", + "\n", + "---\n", + "## Running code\n", + "Once you have selected a code cell you can run it by clicking the run button at the menu bar at the top, or by pressing Ctrl+Enter.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is our first code cell, it contains basic Python code.\n", + "# You can run a code cell by selecting it and clicking the Run button in the top menu, or by pressing Shift + Enter.\n", + "# Once you run a code cell any output from that code will be displayed directly below it.\n", + "print(\"Congratulations you just ran this code cell\")\n", + "y = 2+2\n", + "print(\"2 + 2 =\", y)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Variables set within a code cell persist between cells meaning you can chain cells together" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "y + 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn More : \n", + " - The [Infosec Jupyter Book](https://infosecjupyterbook.com/) provides an infosec specific intro to Python.\n", + " - [Real Python](https://realpython.com/) is a comprehensive set of Python learnings and tutorials.\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that you understand the basics we can move onto more complex code.\n", + "\n", + "---\n", + "## Setting up the environment\n", + "Code cells behave in the same way your code would in other environments, so you need to remember about common coding practices such as variable initialization and library imports. \n", + "Before we execute more complex code we need to make sure the required packages are installed and libraries imported. At the top of many of the Azure Sentinel notebooks you will see large cells that will check kernel versions and then install and import all the libraries we are going to be using in the notebook, make sure you run this before running other cells in the notebook.\n", + "If you are running notebooks locally or via dedicated compute in Azure Notebooks library installs will persist but this is not the case with Azure Notebooks free tier, so you will need to install each time you run. Even if running in a static environment imports are required for each run so make sure you run this cell regardless." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import warnings\n", + "from IPython.display import display, HTML, Markdown\n", + "\n", + "REQ_PYTHON_VER=(3, 6)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", + "\n", + "display(HTML(\"

Starting Notebook setup...

\"))\n", + "# If you did not clone the entire Azure-Sentinel-Notebooks repo you may not have this file\n", + "if Path(\"./utils/nb_check.py\").is_file():\n", + " from utils.nb_check import check_python_ver, check_mp_ver\n", + "\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", + " try:\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", + " except ImportError:\n", + " !pip install --user --upgrade msticpy\n", + " if \"msticpy\" in sys.modules:\n", + " importlib.reload(sys.modules[\"msticpy\"])\n", + " else:\n", + " import msticpy\n", + " check_mp_ver(MSTICPY_REQ_VERSION)\n", + " \n", + "from msticpy.nbtools import nbinit\n", + "nbinit.init_notebook(\n", + " namespace=globals(),\n", + " extra_imports=[\"ipwhois, IPWhois, pyyaml\"]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Configuration\n", + "Once we have set up our Jupyter environment with the libraries that we'll use in the notebook, we need to make sure we have some configuration in place. Some of the notebook components need addtional configuration to connect to external services (e.g. API keys to retrieve Threat Intelligence data). This includes configuration for connection to our Azure Sentinel workspace, as well as some threat intelligence providers we will use later.\n", + "The easiest way to handle the configuration for these services is to store them in a msticpyconfig file (`msticpyconfig.yaml`). More details on msticpyconfig can be found here: https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html\n", + "\n", + "### Learn more: \n", + "- In this notebook we will setup the basic config we need to get started. If you need a more complete walk-through we have a separate notebook to help you: https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Azure-Sentinel-Notebooks GitHub repo contains an template msticpyconfig file ready to be populated. If you have run this notebook before you may have a msticpyconfig file already populated, the cell below allows you to checks if this file. If your config file does not contain details under Azure Sentinel > Workspaces, or TIProviders the following cells will populate these for you.
\n", + "If you want to see an example of what a populated msticpyconfig file should look like a samples is included in the repo as msticpyconfig-sample.yaml." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "def print_config():\n", + " with open('msticpyconfig.yaml') as f:\n", + " data = yaml.load(f, Loader=yaml.FullLoader)\n", + " print(yaml.dump(data))\n", + "try:\n", + " print_config()\n", + "except FileNotFoundError:\n", + " print(\"No msticpyconfig.yaml was found in your current directory.\")\n", + " print(\"We are downloading a template file for you.\")\n", + " import urllib\n", + " urllib.request.urlretrieve(\"https://raw.githubusercontent.com/Azure/Azure-Sentinel-Notebooks/master/msticpyconfig.yaml\", \"msticpyconfig.yaml\")\n", + " print_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you do not have and msticpyconfig file we can populate one for you. Before you do this you will need a few things.\n", + "\n", + "The first is the Workspace ID and Tenant ID of the Azure Sentinel Workspace you wish to connect to.\n", + "\n", + " - You can get the workspace ID by opening Azure Sentinel in the [Azure Portal](https://portal.azure.com) and selecting Settings > Workspace Settings. Your Workspace ID is displayed near the top of this page.\n", + "\n", + "- You can get your tenant ID (also referred to organization or directory ID) via [Azure Active Directory](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id)\n", + "\n", + "We are going to use [VirusTotal](https://www.virustotal.com) to enrich our Azure Sentinel data. For this you will need a VirusTotal API key, one of these can be obtained for free (as a personnal key) via the [VirusTotal](https://developers.virustotal.com/v3.0/reference#getting-started) website.\n", + "We are using VirusTotal for this notebook but we also support a range of other threat intelligence providers: https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html\n", + "

\n", + "In addition we are going to plot IP address locations on a map, in order to do this we are going to use [MaxMind](https://www.maxmind.com) to geolocate IP addresses which requires an API key. You can sign up for a free account and API key at https://www.maxmind.com/en/geolite2/signup. \n", + "

\n", + "Once you have these required items run the cell below and you will prompted to enter these elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "ws_id = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", + " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", + "ten_id = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", + " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", + "vt_key = nbwidgets.GetEnvironmentKey(env_var='VT_KEY',\n", + " prompt='Please enter your VirusTotal API Key:', auto_display=True)\n", + "mm_key = nbwidgets.GetEnvironmentKey(env_var='MM_KEY',\n", + " prompt='Please enter your MaxMind API Key:', auto_display=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " The cell below will now populate a msticpyconfig file with these values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "with open(\"msticpyconfig.yaml\") as config:\n", + " data = yaml.load(config, Loader=yaml.Loader)\n", + "data['AzureSentinel']\n", + "\n", + "workspace = {\"Default\":{\"WorkspaceId\": ws_id.value, \"TenantId\": ten_id.value}}\n", + "ti = {\"VirusTotal\":{\"Args\": {\"AuthKey\" : vt_key.value}, \"Primary\" : True, \"Provider\": \"VirusTotal\"}}\n", + "other_prov = {\"GeoIPLite\" : {\"Args\" : {\"AuthKey\" : mm_key.value, \"DBFolder\" : \"~/msticpy\"}, \"Provider\" : \"GeoLiteLookup\"}}\n", + "data['AzureSentinel']['Workspaces'] = workspace\n", + "data['TIProviders'] = ti\n", + "data['OtherProviders'] = other_prov\n", + "\n", + "with open(\"msticpyconfig.yaml\", 'w') as config:\n", + " yaml.dump(data, config)\n", + " \n", + "print(\"msticpyconfig.yaml updated\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now validate our configuration is correct." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from msticpy.common.pkg_config import refresh_config, validate_config\n", + "refresh_config()\n", + "validate_config()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note** you may see warnings for missing providers when running this cell.\n", + "> This is not an issue as we will not be using all providers in this notebook\n", + "> so long as you get thie message \"No errors found.\" you are OK to proceed.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Getting Data\n", + "Now that we have configured the details necessary to connect to Azure Sentinel we can go ahead and get some data. We will do this with `QueryProvider()` from MSTICpy. \n", + "You can use the `QueryProvider` class to connect to different data sources such as MDATP, the Security Graph API, and the one we will use here, Azure Sentinel. \n", + "\n", + "### Learn more:\n", + " - More details on configuring and using QueryProviders can be found in the [MSTICpy Documentation](https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#instantiating-a-query-provider).\n", + "

" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For now, we are going to set up a QueryProvider for Azure Sentinel, pass it the details for our workspace that we just stored in the msticpyconfig file, and connect. The connection process will ask us to authenticate to our Azure Sentinel workspace via [device authorization](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) with our Azure credentials. You can do this by clicking the device login code button that appears as the output of the next cell, or by navigating to https://microsoft.com/devicelogin and manually entering the code. Note that this authentication persists with the kernel you are using with the notebook, so if you restart the kernel you will need to re-authenticate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initalize a QueryProvider for Azure Sentinel\n", + "qry_prov = QueryProvider(\"LogAnalytics\")\n", + "\n", + "# Get the Azure Sentinel workspace details from msticpyconfig\n", + "try:\n", + " ws_config = WorkspaceConfig()\n", + " md(\"Workspace details collected from config file\")\n", + "except:\n", + " raise(\"No workspace settings are configured, please run the cells above to configure these.\")\n", + " \n", + "# Connect to Azure Sentinel with our QueryProvider and config details\n", + "# ws_config.code_connect_str is a feature of MSTICpy that creates the required connection string from details in our msticpyconfig\n", + "qry_prov.connect(connection_str=ws_config.code_connect_str)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have connected we can query Azure Sentinel for data, but before we do that we need to understand what data is avalaible to query. The QueryProvider object provides a way to get a list of tables as well as tables and table columns:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get list of tables in our Workspace\n", + "display(qry_prov.schema_tables [:5]) # We are outputting only the first 5 tables for brevity\n", + "# Get list of tables and thier columns\n", + "qry_prov.schema['SigninLogs'] # We are only displaying the columns for SigninLogs for brevity" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MSTICpy includes a number of built in queries that you can run.
\n", + "You can list available queries with .list_queries() and get specific details about a query by calling it with \"?\" as a parameter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get a list of avaliable queries\n", + "qry_prov.list_queries()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get details about a query\n", + "qry_prov.Azure.list_all_signins_geo(\"?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can then run the query by calling it with the required parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "# set our query end time as now\n", + "end = datetime.now()\n", + "# set our query start time as 1 hour ago\n", + "start = end - timedelta(hours=1)\n", + "# run query with specified start and end times\n", + "logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", + "# display first 5 rows of any results\n", + "logons_df.head() # If you have no data you will just see the column headings displayed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another way to run queries is to pass a string format of a KQL query to the query provider, this will run the query against the workspace connected to above, and will return the data in a [Pandas DataFrame](https://pandas.pydata.org/). We will look at working with Pandas in a bit more detail later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-06-26T19:27:44.779558Z", + "start_time": "2020-06-26T19:27:44.569079Z" + } + }, + "outputs": [], + "source": [ + "# Define our query\n", + "test_query = \"\"\"\n", + "SigninLogs\n", + "| where TimeGenerated > ago(7d)\n", + "| take 10\n", + "\"\"\"\n", + "\n", + "# Pass that query to our QueryProvider\n", + "test_df = qry_prov.exec_query(test_query)\n", + "\n", + "# Check that we have some data\n", + "if isinstance(test_df, pd.DataFrame) and not test_df.empty:\n", + " # .head() returns the first 5 rows of our results DataFrame\n", + " display(test_df.head())\n", + "# If where is no data load some sample data to use instead\n", + "else:\n", + " md(\"You don't appear to have any SigninLogs - we will load sample data for you to use.\")\n", + " qry_prov = QueryProvider(\"LocalData\", data_paths=[\"nbdemo/data/\"], query_paths=[\"nbdemo/data/\"])\n", + " logons_df = qry_prov.Azure.list_all_signins_geo()\n", + " display(logons_df.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + " - You can learn more about the MSTICpy pre-defined queries in the [MSTICpy Documentation](https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#running-an-pre-defined-query)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Pandas\n", + "Our query results are returned in the form of a Pandas DataFrame. DataFrames are a core component of the Azure Sentinel notebooks and of MSTICpy and is used for both input and output formats.\n", + "Pandas DataFrames are incredibly versitile data structures with a lot of useful features, we will cover a small number of them here and we recommend that you check out the Learn more section to learn more about Pandas features.\n", + "
\n", + "
\n", + "### Displaying a DataFrame:\n", + "The first thing we want to do is display our DataFrame. You can either just run it or explicity display it by calling `display(df)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# For this section we are going to create a DataFrame from data we have saved in a csv file\n", + "df = pd.read_csv(\"https://raw.githubusercontent.com/microsoft/msticpy/master/tests/testdata/host_logons.csv\", index_col=[0] )\n", + "# Display our DataFrame\n", + "df # or display(df)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note** if the dataframe variable (`df` in the example above) is the last statement in a \n", + "> code cell, Jupyter will automatically display it without using the `display()` function. \n", + "> However, if you want to display a DataFrame in the middle of \n", + "> other code in a cell you must use the `display()` function.\n", + "\n", + "You may not want to display the whole DataFrame and instead display only a selection of items. There are numerous ways to do this and the cell below shows some of the most widely used functions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display the first 2 rows using head(): \", \"bold\")\n", + "display(df.head(2)) # we don't need to call display here but just for illustration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display the 3rd row using iloc[]: \", \"bold\")\n", + "df.iloc[3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Show the column names in the DataFrame \", \"bold\")\n", + "df.columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display just the TimeGenerated and TenantId columnns: \", \"bold\")\n", + "df[[\"TimeGenerated\", \"TenantId\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also choose to select a subsection of our DataFrame based on the contents of the DataFrame:\n", + "\n", + "> **Tip**: the syntax in these examples is using a technique called *boolean indexing*. \n", + ">
`df[]`\n", + "> returns all rows in the dataframe where the boolean expression is True\n", + ">
In the first example we telling pandas to return all rows where the column value of\n", + "> 'TargetUserName' matches 'MSTICAdmin'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display only rows where TargetUserName value is 'MSTICAdmin': \", \"bold\")\n", + "df[df['TargetUserName']==\"MSTICAdmin\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "md(\"Display rows where TargetUserName is either MSTICAdmin or adm1nistratror:\", \"bold\")\n", + "display(df[df['TargetUserName'].isin(['adm1nistrator', 'MSTICAdmin'])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our DataFrame call also be extended to add new columns with additional data if reqired:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df[\"NewCol\"] = \"Look at my new data!\"\n", + "display(df[[\"TenantId\",\"Account\", \"TimeGenerated\", \"NewCol\"]].head(2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "There is a lot more you can do with Pandas, the links below provide some useful resources:\n", + " - [Getting starting with Pandas](https://pandas.pydata.org/pandas-docs/stable/getting_started/index.html)\n", + " - [Infosec Jupyerbook intro to Pandas](https://infosecjupyterbook.com/notebooks/tutorials/03_intro_to_pandas.html)\n", + " - [A great list of Pandas hints and tricks](https://www.dataschool.io/python-pandas-tips-and-tricks/)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Enriching data\n", + "\n", + "Now that we have seen how to query for data, and do some basic manipulation we can look at enriching this data with additional data sources. For this we are going to use an external threat intelligence provider to give us some more details about an IP address we have in our dataset using the [MSTICpy TIProvider](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html) feature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "# Check if we have logon data already and if not get some\n", + "if not isinstance(logons_df, pd.DataFrame) or logons_df.empty:\n", + " # set our query end time as now\n", + " end = datetime.now()\n", + " # set our query start time as 1 hour ago\n", + " start = end - timedelta(days=1)\n", + " # run query with specified start and end times\n", + " logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", + " \n", + "# Create our TI provider\n", + "ti = TILookup()\n", + "# Get the first logon IP address from our dataset\n", + "ip = logons_df.iloc[1]['IPAddress']\n", + "# Look up the IP in VirusTotal\n", + "ti_resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", + "\n", + "# Format our results as a DataFrame\n", + "ti_resp = ti.result_to_df(ti_resp)\n", + "display(ti_resp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the [Pandas apply()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html) feature we can get results for all the IP addresses in our data set and add the lookup severity score as a new column in our DataFrame for easier reference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Take the IP address in each row, look it up against TI and return the seveirty score\n", + "def lookup_res(row):\n", + " ip = row['IPAddress']\n", + " resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", + " resp = ti.result_to_df(resp)\n", + " return resp[\"Severity\"].iloc[0]\n", + "\n", + "# Take the first 3 rows of data and copy they into a new DataFrame\n", + "enrich_logons_df = logons_df.iloc[:3].copy()\n", + "# Create a new column called TIRisk and populate that with the TI severity score of the IP Address in that row\n", + "enrich_logons_df['TIRisk'] = enrich_logons_df.apply(lookup_res, axis=1)\n", + "# Display a subset of columns from our DataFrame\n", + "enrich_logons_df[[\"TimeGenerated\", \"ResultType\", \"UserPrincipalName\", \"IPAddress\", \"TIRisk\"]]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "MSTICpy includes further threat intelligence capabilities as well as other data enrichment options. More details on these can be found in the [documentation](https://msticpy.readthedocs.io/en/latest/DataEnrichment.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Analyzing data\n", + "With the data we have collected we may wish to perform some analysis on it in order to better understand it. MSTICpy includes a number of features to help with this, and there are a vast array of other data analysis capabilities available via Python ranging from simple processes to complex ML models. We will start here by keeping it simple and look at how we can decode some Base64 encoded command line strings we have in order to allow us to understand their content." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from msticpy.sectools import base64unpack as b64\n", + "# Take our encoded Powershell Command\n", + "b64_cmd = \"powershell.exe -encodedCommand SW52b2tlLVdlYlJlcXVlc3QgaHR0cHM6Ly9jb250b3NvLmNvbS9tYWx3YXJlIC1PdXRGaWxlIEM6XG1hbHdhcmUuZXhl\"\n", + "# Unpack the Base64 encoded elements\n", + "unpack_txt = b64.unpack(input_string=b64_cmd)\n", + "# Display our results and transform for easier reading\n", + "unpack_txt[1].T" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also use MSTICpy to extract Indicators of Compromise (IoCs) from a dataset, this makes it easy to extract and match on a set of IoCs within our data. In the example below we take a US Cybersecurity & Infrastructure Security Agency (CISA) report and extract all domains listed in the report:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "# Set up our IoCExtract oject\n", + "ioc_extractor = iocextract.IoCExtract()\n", + "# Download our threat report\n", + "data = requests.get(\"https://www.us-cert.gov/sites/default/files/publications/AA20-099A_WHITE.stix.xml\")\n", + "# Extract domains listed in our report\n", + "iocs = ioc_extractor.extract(data.text, ioc_types=\"dns\")['dns']\n", + "# Display the first 5 iocs found in our report\n", + "list(iocs)[:5]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + "There are a wide range of options when it comes to data analysis in notebooks using Python. Here are some useful resources to get you started:\n", + " - [MSITCpy DataAnalysis documentation](https://msticpy.readthedocs.io/en/latest/DataAnalysis.html)\n", + " - Scikit-Learn is a popular Python ML data analysis library, which has a useful [tutorial](https://scikit-learn.org/stable/tutorial/basic/tutorial.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Visualizing data\n", + "Visualizing data can provide an excellent way to analyse data, identify patterns and anomalies. Python has a wide range of data visualization capabilities each of which have thier own benefits and drawbacks. We will look at some basic capabilities as well as the in-build visualizations in MSTICpy.\n", + "


\n", + "**Basic Graphs**
\n", + "Pandas and Matplotlib provide the easiest and simplest way to produce simple plots of data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vis_q = \"\"\"\n", + "SigninLogs\n", + "| where TimeGenerated > ago(7d)\n", + "| sample 5\"\"\"\n", + "\n", + "# Try and query for data but if using sample data load that instead\n", + "try:\n", + " vis_data = qry_prov.exec_query(vis_q)\n", + "except FileNotFoundError:\n", + " vis_data = logons_df\n", + "\n", + "# Check we have some data in our results and if not use previously used dataset\n", + "if not isinstance(vis_data, pd.DataFrame) or vis_data.empty:\n", + " vis_data = logons_df\n", + "\n", + "# Plot up to the first 5 IP addresses\n", + "vis_data.head()['IPAddress'].value_counts().plot.bar(title=\"IP prevelence\", legend=False)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pie_df = vis_data.copy()\n", + " # If we have lots of data just plot the first 5 rows\n", + "pie_df.head()['IPAddress'].value_counts().plot.pie(legend=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Bokeh](https://bokeh.org/) is a powerful visualization library that allows you to create complex, interactive visualizations. MSTICpy includes a number of pre-built visualizations using Bokeh including a timeline feature that can be used to represent events over time. You can interact with the timeline by zooming and panning, using the range selector, as well as hovering over data points to see more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime, timedelta\n", + "# Check if we have logon data already and if not get some\n", + "if not isinstance(logons_df, pd.DataFrame) or logons_df.empty:\n", + " # set our query end time as now\n", + " end = datetime.now()\n", + " # set our query start time as 1 hour ago\n", + " start = end - timedelta(days=1)\n", + " # run query with specified start and end times\n", + " logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", + " \n", + "display(timeline.display_timeline(logons_df.head(10), source_columns=[\"TimeGenerated\", \"ResultType\", \"UserPrincipalName\", \"IPAddress\"], group_by=\"AppDisplayName\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "MSTICpy also includes a feature to allow you to map locations, this can be particularily useful when looking at the distribution of remote network connections or other events. Below we plot the locations of remote logons observed in our Azure AD data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from msticpy.sectools.ip_utils import convert_to_ip_entities\n", + "from msticpy.nbtools.foliummap import FoliumMap, get_map_center\n", + "\n", + "# Convert our IP addresses in string format into an ip address entity\n", + "ip_entity = entityschema.IpAddress()\n", + "ip_list = [convert_to_ip_entities(i)[0] for i in logons_df['IPAddress'].head(10)]\n", + " \n", + "# Get center location of all IP locaitons to center the map on\n", + "location = get_map_center(ip_list)\n", + "logon_map = FoliumMap(location=location, zoom_start=4)\n", + "\n", + "# Add location markers to our map and dsiplay it\n", + "if len(ip_list) > 0:\n", + " logon_map.add_ip_cluster(ip_entities=ip_list)\n", + "display(logon_map.folium_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Learn more:\n", + " - The [Infosec Jupyterbook](https://infosecjupyterbook.com/) includes a section on data visualization.\n", + " - [Bokeh Library Documentation](https://bokeh.org/)\n", + " - [Matplotlib tutorial](https://matplotlib.org/3.2.0/tutorials/index.html)\n", + " - [Seaborn visualization library tutorial](https://seaborn.pydata.org/tutorial.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Conclusion\n", + "This notebook has showed you the basics of using notebooks and Azure Sentinel for security investigaitons. There are many more things possible using notebooks and it is stronly encouraged to read the material we have referenced in the learn more sections in this notebook. You can also explore the other Azure Sentinel notebooks in order to take advantage of the pre-built hunting logic, and understand other analysis techniques that are possible.
\n", + "### Appendix:\n", + " - [Jupyter Notebooks: An Introduction](https://realpython.com/jupyter-notebook-introduction/)\n", + " - [Threat Hunting in the cloud with Azure Notebooks](https://medium.com/@maarten.goet/threat-hunting-in-the-cloud-with-azure-notebooks-supercharge-your-hunting-skills-using-jupyter-8d69218e7ca0)\n", + " - [MSTICpy documentation](https://msticpy.readthedocs.io/)\n", + " - [Azure Sentinel Notebooks documentation](https://docs.microsoft.com/en-us/azure/sentinel/notebooks)\n", + " - [The Infosec Jupyterbook](https://infosecjupyterbook.com/introduction.html)\n", + " - [Linux Host Explorer Notebook walkthrough](https://techcommunity.microsoft.com/t5/azure-sentinel/explorer-notebook-series-the-linux-host-explorer/ba-p/1138273)\n", + " - [Why use Jupyter for Security Investigations](https://techcommunity.microsoft.com/t5/azure-sentinel/why-use-jupyter-for-security-investigations/ba-p/475729)\n", + " - [Security Investigtions with Azure Sentinel & Notebooks](https://techcommunity.microsoft.com/t5/azure-sentinel/security-investigation-with-azure-sentinel-and-jupyter-notebooks/ba-p/432921)\n", + " - [Pandas Documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/index.html)\n", + " - [Bokeh Documentation](https://docs.bokeh.org/en/latest/)" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3.6", + "language": "python", + "name": "python36" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ConfiguringNotebookEnvironment.ipynb b/ConfiguringNotebookEnvironment.ipynb index e004a45f..85963bf7 100644 --- a/ConfiguringNotebookEnvironment.ipynb +++ b/ConfiguringNotebookEnvironment.ipynb @@ -30,7 +30,9 @@ "### Creating a virtual environment\n", "If you are running these notebooks locally, it is a good idea to create a clean virtual python environment, before installing any of the packages . This will prevent installed packages conflicting with versions that you may need for other applications.\n", "\n", - "For standard python use the `virtualenv` command. For Conda use the `conda env` command. In both cases be sure to activate the environment before running jupyter using `activate {my_env_name}`.\n", + "For standard python use the [`venv`](https://docs.python.org/3/library/venv.html?highlight=venv) command. \n", + "For Conda use the [`conda env`](https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) command. \n", + "In both cases be sure to activate the environment before running jupyter using `activate {my_env_name}` or `conda activate {my_env_name}`.\n", "\n", "\n", "### Using Requirements.txt\n", @@ -49,12 +51,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T00:47:41.219073Z", - "start_time": "2019-10-31T00:47:41.213073Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Run this cell to view requirements.txt\n", @@ -85,7 +82,10 @@ "pip install pkg_name --user --upgrade\n", "```\n", "\n", - "This will avoid permission errors by installing into your user folder." + "This will avoid permission errors by installing into your user folder.\n", + "\n", + "> **Note**: the use of the `--user` option is usually not required in a Conda environment \n", + "> since the Python site packages are normally already installed in a per-user folder." ] }, { @@ -93,57 +93,69 @@ "metadata": {}, "source": [ "### Install Packages from this Notebook\n", - "The first time this cell runs for a new Azure Notebooks project or other Python environment it will take several minutes to download and install the packages. In subsequent runs it should run quickly and confirm that package dependencies are already installed. Unless you want to upgrade the packages you can feel free to skip execution of the next cell.\n", - "\n", - "If you see any import failures (```ImportError```) in the notebooks, please re-run this notebook and answer 'y' when prompted, then re-run the cell where the import failure occurred.\n", - "\n", - "Note you may see some warnings about incompatibility with certain packages. This should not affect the functionality of this notebook but you may need to upgrade the packages producing the warnings to a more recent version." + "The first time this cell runs for a new Azure ML or Azure Notebooks notebook or other Python environment it will do the following things:\n", + "1. Check the kernel version to ensure that a Python 3.6 or later kernel is running\n", + "2. Check the msticpy version - if this is not installed or the version installed is less than the required version (in `REQ_MSTICPY_VER`)\n", + " it will attempt to install a new version (you will be prompted whether you want to do this)\n", + " The install can take several minutes depending on the versions of packages that you already have installed.\n", + " \n", + " > **Note:** These two steps are run from a local python module - this is available in the Azure-Sentinel-Notebooks repo.\n", + " > If you do not have this locally, download it from [here](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/utils/nb_check.py) and\n", + " > put a copy in a `utils` subfolder of your current directory.\n", + " \n", + "3. Once *msticpy* is installed and imported, the `init_notebook` function is run. This:\n", + " - imports common modules used in the notebook\n", + " - installs additional packages\n", + " - sets some global options\n", + " \n", + "> **Note:** In subsequent runs, this cell shoud run quickly since you will already have the required packages installed.\n", + "\n", + "\n", + "> **Warning:** you may see some warnings about incompatibility with certain packages. This should not affect the functionality of this notebook but you may need to upgrade the packages producing the warnings to a more recent version." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ + "from pathlib import Path\n", + "import os\n", "import sys\n", "import warnings\n", - "\n", - "warnings.filterwarnings(\"ignore\",category=DeprecationWarning)\n", - "\n", - "MIN_REQ_PYTHON = (3,6)\n", - "if sys.version_info < MIN_REQ_PYTHON:\n", - " print('Check the Kernel->Change Kernel menu and ensure that Python 3.6')\n", - " print('or later is selected as the active kernel.')\n", - " sys.exit(\"Python %s.%s or later is required.\\n\" % MIN_REQ_PYTHON)\n", - "\n", - "# Package Installs - try to avoid if they are already installed\n", - "try:\n", - " import msticpy.sectools as sectools\n", - " import Kqlmagic\n", - " from dns import reversename, resolver\n", - " from ipwhois import IPWhois\n", - " import folium\n", - " \n", - " print('If you answer \"n\" this cell will exit with an error in order to avoid the pip install calls,')\n", - " print('This error can safely be ignored.')\n", - " resp = input('msticpy and Kqlmagic packages are already loaded. Do you want to re-install? (y/n)')\n", - " if resp.strip().lower() != 'y':\n", - " sys.exit('pip install aborted - you may skip this error and continue.')\n", - " else:\n", - " print('After installation has completed, restart the current kernel and run '\n", - " 'the notebook again skipping this cell.')\n", - "except ImportError:\n", - " pass\n", - "\n", - "print('\\nPlease wait. Installing required packages. This may take a few minutes...')\n", - "!pip install --user -r requirements.txt \n", - "\n", - "print('To ensure that the latest versions of the installed libraries '\n", - " 'are used, please restart the current kernel and run '\n", - " 'the notebook again skipping this cell.')" + "from IPython.display import display, HTML, Markdown\n", + "\n", + "REQ_PYTHON_VER=(3, 6)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", + "\n", + "display(HTML(\"

Starting Notebook setup...

\"))\n", + "if Path(\"./utils/nb_check.py\").is_file():\n", + " from utils.nb_check import check_python_ver, check_mp_ver\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", + " try:\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", + " except ImportError:\n", + " !pip install --upgrade msticpy\n", + " if \"msticpy\" in sys.modules:\n", + " importlib.reload(sys.modules[\"msticpy\"])\n", + " else:\n", + " import msticpy\n", + " check_mp_ver(REQ_MSTICPY_VER)\n", + " \n", + "extra_imports = [\n", + " \"msticpy.nbtools, observationlist\",\n", + " \"msticpy.sectools, domain_utils\",\n", + " \"pyvis.network, Network\",\n", + "]\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", + "from msticpy.nbtools import nbinit\n", + "nbinit.init_notebook(\n", + " namespace=globals(),\n", + " extra_imports=[\"ipwhois, IPWhois\"],\n", + " additional_packages=[\"pyvis\", \"python-whois\"],\n", + ");" ] }, { @@ -159,6 +171,17 @@ "metadata": {}, "source": [ "## Creating your `config.json`\n", + "When you start a notebook from Azure Sentinel for the first time it will create a `config.json` file in\n", + "your notebooks folder. This should be populated with your workspace and tenant IDs needed to \n", + "authenticate to Azure Sentinel.\n", + "\n", + "If you are using notebooks in a different environment you may need to create a `config.json` or `msticpyconfig.yaml` (see below)\n", + "to supply this information to your notebook.\n", + "\n", + "Form more information see this [msticpy Package Configuration](https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html)\n", + "\n", + "---\n", + "\n", "If you need to create or modify your config.json you can run the following cell.\n", "\n", "You will need the subscription and workspace IDs for your Azure Sentinel Workspace. These can be found here in the Azure Sentinel portal as shown below.\n", @@ -172,13 +195,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T00:51:46.650354Z", - "start_time": "2019-10-31T00:51:46.611399Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "import requests\n", @@ -260,27 +277,39 @@ "metadata": {}, "source": [ "## `msticpyconfig.yaml` Configuration File\n", - "Before you can use the msticpy TILookup class you need to configure your TI provider settings.\n", "\n", - "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This file is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", - "For more details on msticpy configuration see the [msticpy documentation](https://msticpy.readthedocs.io/en/latest/msticpyconfig.html).\n", + "`config.json` provides some basic configuration for connecting to your Azure Sentinel workspace. \n", + "However, there are many features that require additional configuration information. Some examples are:\n", + "- Threat Intelligence Provider connection information\n", + "- GeoIP connection information\n", + "- Keyvault configuration for storing secrets remotely\n", + "- MDATP and Azure API connection information.\n", + "- Connection information for multiple Azure Sentinel workspaces.\n", + "\n", + "Settings for these are stored in the `msticpyconfig.yaml` file. This file is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", + "Form more information about *msticpy* configuration see [msticpy Package Configuration](https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html).\n", + "\n", + "The most commonly-used sections are described below.\n", + "\n", "\n", "### Threat Intelligence Provider Setup\n", "For more information on the msticpy Threat Intel lookup class see the [documentation here](https://msticpy.readthedocs.io/en/latest/TIProviders.html).\n", "\n", "Primary providers are used by default. Secondary providers are not run by default but can be invoked by using the `providers` parameter to `lookup_ioc()` or `lookup_iocs()`. Set the `Primary` config setting to `True` or `False` for each provider ID according to how you want to use them. The `providers` parameter should be a list of strings identifying the provider(s) to use. \n", "\n", - "The provider ID is given by the `Provider:` setting for each of the TI providers - do not alter this value.\n", - "\n", - "Delete or comment out the section for any TI Providers that you do not wish to use.\n", + "- The provider ID is given by the `Provider:` setting for each of the TI providers - do not alter this value.\n", + "- Delete or comment out the section for any TI Providers that you do not wish to use.\n", + "- For most providers you will usually need to supply an authorization (API) key and in some cases a user ID for each provider.\n", + "- For the Azure Sentinel TI provider, you will need the workspace ID and tenant ID and will need to authenticate in order \n", + " to access the data (although if you have an existing authenticated connection with the same workspace/tenant, this connection will be re-used).\n", "\n", - "For most providers you will usually need to supply an authorization (API) key and in some cases a user ID for each provider.\n", + "If you need to create a new msticpyconfig.yaml file, run the \"Create a new mstipyconfig.yaml\" cell below.\n", "\n", - "For the Azure Sentinel TI provider, you will need the workspace ID and tenant ID and will need to authenticate in order to access the data (although if you have an existing authenticated connection with the same workspace/tenant, this connection will be re-used).\n", + "**Warning** - this will overwrite a file of the same name in the current directory\n", "\n", - "If you need to create a config file, run the \"Create a new mstipyconfig.yaml\" cell below.\n", - "\n", - "**Warning** - this will overwrite a file of the same name in the current directory\n", + "### GeoIP Providers\n", + "Like the TI providers these services normally need an API key to access. You can read more about configuration\n", + "the supported providers here. [msticpy GeoIP Providers](https://msticpy.readthedocs.io/en/latest/data_acquisition/GeoIPLookups.html)\n", "\n", "### Browshot Setup\n", "The functionality to screenshot a URL in msticpy.sectools.domain_utils relies on a service called BrowShot (https://browshot.com/). An API key is required to use this service and it needs to be defined in the `msticpyconfig` file as well. As this is not a threat intelligence provider it doesn't not fall under the `TIProviders` section of `msticpyconfig` but instead sits alone. See the cell below for example configuration." @@ -296,12 +325,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T02:03:13.536170Z", - "start_time": "2020-02-27T02:03:13.530188Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "%pfile msticpyconfig.yaml\n" @@ -312,18 +336,30 @@ "metadata": {}, "source": [ "### Create a new `msticpyconfig.yaml`\n", - "If you need to create a msticpyconfig from scratch, edit the cell below uncommenting the sections you need and adding the correct values.\n", + "If you need to create a msticpyconfig from scratch, edit the cell below:\n", + "1. Uncommenting the first line to enable the %%writefile magic instruction\n", + "2. Edit sections you need and adding the correct values.\n", + "3. Delete the other sections or leave commented out\n", "\n", + "\n", + "Guidelines:\n", "- Usually you will only need the `default` workspace in the `AzureSentinel/Workspaces` section\n", "- You can add TI Provider auth/API keys to the relevant sections (either as text or\n", - " stored in an environment variable)\n", + " stored in an environment variable - the OTX entry shows the former and the XForce entry\n", + " shows an example of the latter syntax)\n", "- Delete the providers/sections that you do not need.\n", "- Usually one or more TI providers and one GeoIP provider will be needed for most notebooks\n", - "\n", - ">

** WARNING **

\n", - "> Executing the following cell will overwrite the contents of the cell to any existing\n", - "> `msticpyconfig.yaml`.
\n", - ">

Do not run this cell if you have existing configuration settings

" + "- For a single string (e.g. API key) it does not matter whether the string is quoted or not\n", + "- For TI Providers, setting `Primary: True` means that the provider will be used in the\n", + " default set of providers every time you do a TI Lookup.\n", + "\n", + ">

** WARNING **

\n", + "> Executing the following cell will write the contents of the cell to any existing\n", + "> `msticpyconfig.yaml` in the current folder, overwriting any settings that you have\n", + "> in there.\n", + "> If you have a current msticpyconfig.yaml, go to the next cell to read in this file\n", + "> and edit this with your settings.
\n", + ">

Do not run this cell if you have existing configuration settings!

" ] }, { @@ -332,7 +368,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%writefile msticpyconfig.yaml\n", + "#%%writefile msticpyconfig.yaml\n", "\n", "AzureSentinel:\n", " #Workspaces:\n", @@ -424,12 +460,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T02:06:09.736798Z", - "start_time": "2020-02-27T02:06:09.732799Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "%load path/to/your/msticpyconfig.yaml" @@ -438,8 +469,8 @@ ], "metadata": { "hide_input": false, + "history": [], "kernelspec": { - "display_name": "Python 3.6", "language": "python", "name": "python36" }, @@ -453,7 +484,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.10" }, "toc": { "base_numbering": 1, @@ -468,6 +499,7 @@ "toc_section_display": true, "toc_window_display": true }, + "uuid": "75a9aa0a-6ec9-4b1a-a5f0-fc14ed6f8fab", "varInspector": { "cols": { "lenName": 16, @@ -496,13 +528,6 @@ "_Feature" ], "window_display": false - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } } }, "nbformat": 4, diff --git a/Entity Explorer - Account.ipynb b/Entity Explorer - Account.ipynb index 1c4bf399..7658ed17 100644 --- a/Entity Explorer - Account.ipynb +++ b/Entity Explorer - Account.ipynb @@ -33,7 +33,7 @@ }, "source": [ "

Contents

\n", - "" + "" ] }, { @@ -73,12 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-28T21:13:24.369073Z", - "start_time": "2020-02-28T21:13:24.260137Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", @@ -93,7 +88,6 @@ "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", " from utils.nb_check import check_python_ver, check_mp_ver\n", - "\n", " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", " try:\n", " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", @@ -105,6 +99,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "nbinit.init_notebook(\n", " namespace=globals(),\n", @@ -143,12 +140,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:20:45.697714Z", - "start_time": "2019-10-31T21:20:27.173805Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Authentication\n", @@ -200,12 +192,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:20:50.717317Z", - "start_time": "2019-10-31T21:20:50.710320Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "WIDGET_DEFAULTS = {\n", @@ -219,12 +206,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:20:55.324557Z", - "start_time": "2019-10-31T21:20:55.285577Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "query_times = nbwidgets.QueryTime(units='day', max_before=200, before=5, max_after=7)\n", @@ -234,12 +216,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:21:05.153861Z", - "start_time": "2019-10-31T21:21:05.149864Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Set up function to allow easy reference to common parameters for queries\n", @@ -263,12 +240,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:21:15.909785Z", - "start_time": "2019-10-31T21:21:07.674690Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# KQL query for full text search of IP address and display all datatypes \n", @@ -277,8 +249,7 @@ "| where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", "| summarize RowCount=count() by Table=$table\n", "'''.format(**acct_query_params())\n", - "%kql -query datasource_status\n", - "datasource_status_df = _kql_raw_result_.to_dataframe()\n", + "datasource_status_df = qry_prov.exec_query(datasource_status)\n", "\n", "#Display result as transposed matrix of datatypes availabel to query for the query period \n", "if len(datasource_status_df) > 0:\n", @@ -309,12 +280,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:21:27.840285Z", - "start_time": "2019-10-31T21:21:20.047711Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# AAD\n", @@ -406,12 +372,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:21:39.187410Z", - "start_time": "2019-10-31T21:21:39.119455Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from collections import namedtuple\n", @@ -474,22 +435,35 @@ "\n", "def display_activity(selected_item):\n", " acct, source = selected_account(selected_item)\n", - " utils.md(f\"{acct} (source: {source})\", \"bold\")\n", + " outputs = []\n", + " title = HTML(f\"{acct} (source: {source})\")\n", + " outputs.append(title)\n", " if source == \"LinuxHostLogon\":\n", - " display(linux_logon_df[linux_logon_df[\"AccountName\"] == acct]\n", - " .sort_values(\"TimeGenerated\", ascending=True))\n", + " outputs.append(\n", + " linux_logon_df[linux_logon_df[\"AccountName\"] == acct]\n", + " .sort_values(\"TimeGenerated\", ascending=True)\n", + " )\n", " if source == \"WindowsHostLogon\":\n", - " display(win_logon_df[win_logon_df[\"TargetUserName\"] == acct]\n", - " .sort_values(\"TimeGenerated\", ascending=True))\n", + " outputs.append(\n", + " win_logon_df[win_logon_df[\"TargetUserName\"] == acct]\n", + " .sort_values(\"TimeGenerated\", ascending=True)\n", + " )\n", " if source == \"AADLogon\":\n", - " display(aad_signin_df[aad_signin_df[\"UserPrincipalName\"] == acct]\n", - " .sort_values(\"TimeGenerated\", ascending=True))\n", + " outputs.append(\n", + " aad_signin_df[aad_signin_df[\"UserPrincipalName\"] == acct]\n", + " .sort_values(\"TimeGenerated\", ascending=True)\n", + " )\n", " if source == \"AzureActivity\":\n", - " display(azure_activity_df[azure_activity_df[\"UserPrincipalName\"] == acct]\n", - " .sort_values(\"TimeGenerated\", ascending=True))\n", + " outputs.append(\n", + " azure_activity_df[azure_activity_df[\"UserPrincipalName\"] == acct]\n", + " .sort_values(\"TimeGenerated\", ascending=True)\n", + " )\n", " if source == \"O365Activity\":\n", - " display(o365_activity_df[o365_activity_df[\"UserId\"] == acct]\n", - " .sort_values(\"TimeGenerated\", ascending=True))\n", + " outputs.append(\n", + " o365_activity_df[o365_activity_df[\"UserId\"] == acct]\n", + " .sort_values(\"TimeGenerated\", ascending=True)\n", + " )\n", + " return outputs\n", "\n", "def selected_account(selected_acct):\n", " if not selected_acct:\n", @@ -508,12 +482,7 @@ }, { "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-09-22T22:43:08.721257Z", - "start_time": "2019-09-22T22:43:08.686275Z" - } - }, + "metadata": {}, "source": [ "## Related Alerts and Hunting Bookmarks\n", "### Alerts\n", @@ -523,12 +492,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:21:59.287988Z", - "start_time": "2019-10-31T21:21:57.846245Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "account_name, account_source = selected_account(select_acct.value)\n", @@ -564,7 +528,7 @@ "def disp_full_alert(alert):\n", " global related_alert\n", " related_alert = SecurityAlert(alert)\n", - " nbdisplay.display_alert(related_alert, show_entities=True)\n", + " return nbdisplay.format_alert(related_alert, show_entities=True)\n", "\n", "if related_alerts is not None and not related_alerts.empty:\n", " related_alerts[\"CompromisedEntity\"] = related_alerts[\"src_accountname\"]\n", @@ -587,13 +551,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:22:05.399690Z", - "start_time": "2019-10-31T21:22:04.264621Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "acct_name = acct_query_params()[\"account_name\"]\n", @@ -625,7 +583,7 @@ " display(Markdown(\"No related bookmarks found.\"))\n", "\n", "def disp_bookmark(bookmark_id):\n", - " display(related_bkmark_df[related_bkmark_df[\"BookmarkId\"] == bookmark_id].T)\n", + " return related_bkmark_df[related_bkmark_df[\"BookmarkId\"] == bookmark_id].T\n", "\n", "if related_bkmark_df is not None and not related_bkmark_df.empty:\n", " display(Markdown(\"### Click on bookmark to view details.\"))\n", @@ -650,12 +608,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:39:01.948639Z", - "start_time": "2019-10-31T21:39:01.037012Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Function definitions used below\n", @@ -762,12 +715,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:38:34.770088Z", - "start_time": "2019-10-31T21:38:31.168203Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "md(\"Fetching logon data...\")\n", @@ -788,12 +736,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:29:02.043177Z", - "start_time": "2019-10-31T21:29:01.895284Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "logon_summary = (all_win_logons\n", @@ -836,13 +779,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:32:07.264120Z", - "start_time": "2019-10-31T21:30:25.991169Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "ti_results, all_win_logons_ti, src_ip_addrs_win = check_ip_ti(df=all_win_logons, ip_col=\"IpAddress\")\n", @@ -869,12 +806,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:32:54.522511Z", - "start_time": "2019-10-31T21:32:54.163885Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "all_win_logons_geo = check_geo_whois(src_ip_addrs_win, all_win_logons, \"IpAddress\")\n", @@ -902,12 +834,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-30T18:59:39.221208Z", - "start_time": "2019-10-30T18:59:37.439830Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "related_host_alerts = []\n", @@ -945,12 +872,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-30T19:00:08.147249Z", - "start_time": "2019-10-30T19:00:07.109628Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ip_list = \",\".join(list(all_win_logons[\"IpAddress\"].unique()))\n", @@ -984,12 +906,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-30T19:00:12.305509Z", - "start_time": "2019-10-30T19:00:11.427623Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "related_host_bkmks = []\n", @@ -1032,12 +949,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-17T01:12:09.414523Z", - "start_time": "2019-10-17T01:12:06.50592Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "md(\"Fetching logon data...\")\n", @@ -1056,12 +968,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T00:37:26.703212Z", - "start_time": "2019-10-18T00:37:26.610352Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "logon_summary = (all_lx_logons\n", @@ -1104,13 +1011,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T00:34:57.729768Z", - "start_time": "2019-10-18T00:34:56.96394Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "ti_results_lx, all_lx_logons_ti, src_ip_addrs_lx = check_ip_ti(df=all_lx_logons, ip_col=\"SourceIP\")\n", @@ -1137,12 +1038,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T00:36:38.966326Z", - "start_time": "2019-10-18T00:36:38.921842Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "all_lx_logons_geo = check_geo_whois(src_ip_addrs_lx, all_lx_logons, \"SourceIP\")\n", @@ -1171,12 +1067,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T02:35:11.6751Z", - "start_time": "2019-10-18T02:35:06.398572Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "related_host_alerts = []\n", @@ -1214,12 +1105,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T02:38:57.430531Z", - "start_time": "2019-10-18T02:38:51.274039Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ip_list = \",\".join(list(all_lx_logons[\"SourceIP\"].unique()))\n", @@ -1253,12 +1139,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-17T01:37:22.034764Z", - "start_time": "2019-10-17T01:37:19.091489Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "related_host_bkmks = []\n", @@ -1302,13 +1183,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:39:11.590768Z", - "start_time": "2019-10-31T21:39:07.163786Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "md(\"Fetching Azure/Office data...\")\n", @@ -1351,12 +1226,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:39:15.869712Z", - "start_time": "2019-10-31T21:39:15.268413Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "az_all_data = pd.concat([aad_signin_df, azure_activity_df, o365_activity_df], sort=False)\n", @@ -1378,12 +1248,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:39:25.409627Z", - "start_time": "2019-10-31T21:39:25.384637Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "(az_all_data\n", @@ -1419,13 +1284,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T21:56:21.766666Z", - "start_time": "2019-10-31T21:39:35.923317Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "ti_results_az, all_az_ti, src_ip_addrs_az = check_ip_ti(df=az_all_data, ip_col=\"IPAddress\")\n", @@ -1451,12 +1310,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T00:00:36.920040Z", - "start_time": "2019-10-31T00:00:36.771150Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "all_az_geo = check_geo_whois(src_ip_addrs_az.iloc[0:50], az_all_data, \"IPAddress\")\n", @@ -1485,12 +1339,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T00:01:17.332134Z", - "start_time": "2019-10-31T00:01:10.488413Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ip_list = \",\".join(list(src_ip_addrs_az[\"IPAddress\"].unique()))\n", @@ -1527,13 +1376,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-18T03:26:11.596865Z", - "start_time": "2019-10-18T03:26:11.58687Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "print('List of current DataFrames in Notebook')\n", @@ -1655,13 +1498,6 @@ "_Feature" ], "window_display": false - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } } }, "nbformat": 4, diff --git a/Entity Explorer - Domain & URL.ipynb b/Entity Explorer - Domain & URL.ipynb index 57c27622..246f6b74 100644 --- a/Entity Explorer - Domain & URL.ipynb +++ b/Entity Explorer - Domain & URL.ipynb @@ -10,7 +10,7 @@ }, "source": [ "# Entity Explorer - Domain and URL\n", - " <details>\n", + "
\n", "  Details...\n", "\n", " **Notebook Version:** 1.0
\n", @@ -25,7 +25,7 @@ " - Log Analytics - Syslog, SecurityEvent, DnsEvents, CommonSecurityLog, AzureNetworkAnalytics_CL
\n", "**TI Proviers Used**\n", " - VirusTotal, Open Page Rank, BrowShot(all required for certain elements), AlienVault OTX, IBM XForce (optional) - all providers require accounts and API keys\n", - " </details>\n", + "
\n", "\n", "This Notebooks brings together a series of tools and techniques to enable threat hunting within the context of a domain name or URL that has been identified as of interest. It provides a series of techniques to assist in determining whether a domain or URL is malicious. Once this has been established it provides an overview of the scope of the domain or URL across an environment, along with indicators of areas for further investigation such as hosts of interest. " ] @@ -37,14 +37,14 @@ }, "source": [ "

Table of Contents

\n", - "" + "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Hunting Hypothesis: \n", + "## Hunting Hypothesis: \n", "Our broad initial hunting hypothesis is that a particular Linux host in our environment\n", "has been compromised, we will need to hunt from a range of different positions to\n", "validate or disprove this hypothesis." @@ -91,10 +91,10 @@ "import os\n", "import sys\n", "import warnings\n", - "from IPython.display import display, HTML, Markdown\n", + "from IPython.display import display, HTML, Markdown, Image\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -111,6 +111,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "extra_imports = [\n", " \"msticpy.nbtools, observationlist\",\n", @@ -139,7 +142,7 @@ }, "source": [ "### Get WorkspaceId and Authenticate to Log Analytics\n", - "<details>\n", + "
\n", "  Details...\n", "If you are using user/device authentication, run the following cell. \n", "- Click the 'Copy code to clipboard and authenticate' button.\n", @@ -159,7 +162,7 @@ "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", "On successful authentication you should see a ```popup schema``` button.\n", "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - "</details>" + "
" ] }, { @@ -209,7 +212,9 @@ "qry_prov = QueryProvider('LogAnalytics')\n", "la_connection_string = f'loganalytics://code().tenant(\"{ten_id}\").workspace(\"{ws_id}\")'\n", "qry_prov.connect(connection_str=f'{la_connection_string}')\n", - "tilookup = TILookup()" + "tilookup = TILookup()\n", + "tilookup.reload_providers()\n", + "tilookup.provider_status" ] }, { @@ -219,7 +224,7 @@ "#### Authentication and Configuration Problems\n", "\n", "
\n", - "<details>\n", + "
\n", " Click for details about configuring your authentication parameters\n", " \n", "The notebook is expecting your Azure Sentinel Tenant ID and Workspace ID to be configured in one of the following places:\n", @@ -231,7 +236,7 @@ "```%pfile config.json```\n", "\n", "For help with setting up your `msticpyconfig.yaml` see the [Setup](#Setup) section at the end of this notebook and the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)\n", - "</details>" + "
" ] }, { @@ -239,7 +244,7 @@ "metadata": {}, "source": [ "## Select the domain or URL you wish to investigate\n", - "Enter the domain or URL you wish to investigate." + "Enter the domain or URL you wish to investigate. e.g. www.microsoft.com/index.html" ] }, { @@ -309,8 +314,12 @@ "### Threat Intelligence\n", "As a first step we want to establish if this domain or URL is known to to be malicious by our Threat Intelligence providers.\n", "\n", - "#### msticpyconfig.yaml configuration file\n", - "You can configure primary and secondary TI providers and any required parameters in the msticpyconfig.yaml file. This is read from the current directory or you can set an environment variable (MSTICPYCONFIG) pointing to its location. To configure this file see the ConfigureNotebookEnvironment notebook." + "#### `msticpyconfig.yaml` configuration File\n", + "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb) and [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file). \n", + "\n", + "For Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)" ] }, { @@ -363,7 +372,11 @@ "### Domain analysis\n", "To build up a fuller picture of the domain we can use whois, and other data sources to gather pertinent data. Indicators such as registration data, domain entropy, and registration details can provide indicators that a domain is not legitimate in nature.\n", "\n", - "This cell uses the Open Page Rank API (https://www.domcop.com/openpagerank/) - in order to use this you need to add your API key to your `msticpyconfig.yaml` configuration file (as you did for other TI providers). Please see the `ConfigureNotebookEnvironment` notebook for more details on this." + "This cell uses the Open Page Rank API (https://www.domcop.com/openpagerank/) - in order to use this you need to add your API key to your `msticpyconfig.yaml` configuration file (as you did for other TI providers). \n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb) and [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file). \n", + "\n", + "For Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)" ] }, { @@ -467,7 +480,7 @@ }, "source": [ "### TLS Cert Details\n", - "Does the domain have an associated tls certificate and if so is that certificate blacklisted by abuse.ch?\n", + "Does the domain have an associated tls certificate and if so is that certificate in the malicious certs list held by abuse.ch?\n", "Details such as the certificate's subject and issuer can also provide indicators as to the domains nature." ] }, @@ -487,23 +500,23 @@ "else:\n", " scope = domain\n", "\n", - "# See if TLS cert is in abuse.ch blacklist and get cert details\n", - "result, x509 = dom_val.ssl_blacklisted(scope)\n", + "# See if TLS cert is in abuse.ch malicious certs list and get cert details\n", + "result, x509 = dom_val.in_abuse_list(scope)\n", "\n", "if x509 is not None:\n", " cert_df = pd.DataFrame({\"SN\" :[x509.serial_number],\n", " \"Subject\":[[(i.value) for i in x509.subject]],\n", " \"Issuer\": [[(i.value) for i in x509.issuer]],\n", " \"Expired\": [x509.not_valid_after],\n", - " \"In SSLBL?\": result})\n", + " \"InAbuseList\": result})\n", "\n", " display(cert_df.T)\n", " summary.add_observation(caption=\"TLS Summary\", description=f\"Summary of TLS certificate for {domain}\", data=cert_df)\n", - " md(\"If 'In SSLBL?' is True this shows that the SSL certificate figerprint appeared in the abuse.ch blacklist\")\n", + " md(\"If 'InAbuseList' is True this shows that the SSL certificate fingerprint appeared in the abuse.ch list\")\n", " graph_items.append((domain,result))\n", "\n", "else:\n", - " md(\"No Blacklisted TLS certificate was found.\")" + " md(\"No TLS certificate was found in abuse.ch lists.\")" ] }, { @@ -514,7 +527,11 @@ "What IP address is assocatiated with this domain, what do we know about that IP?\n", "What other domains have been associated with this IP, and is it a known ToR exit node?\n", "\n", - "In order to use this ToR lookup functionality of MSTICpy you need to configure it as a provider in your `msticpyconfig.yaml` configuration file. No API key is required to use this functionality. Please see the `ConfigureNotebookEnvironment` notebook for more details on this." + "In order to use this ToR lookup functionality of MSTICpy you need to configure it as a provider in your `msticpyconfig.yaml` configuration file. No API key is required to use this functionality. \n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb) and [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file). \n", + "\n", + "For Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)" ] }, { @@ -593,7 +610,11 @@ "### Site Screenshot\n", "Using https://browshot.com/ return a screenshot of the domain or url being investigated. This can help us identify if the site is a phishing portal.\n", "\n", - "As with other external providers you need an API key to use the BrowShot service, and have the provider configured in your `msticpyconfig.yaml` file. Please see the `ConfigureNotebookEnvironment` notebook for more details on this." + "As with other external providers you need an API key to use the BrowShot service, and have the provider configured in your `msticpyconfig.yaml` file. \n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb) and [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file). \n", + "\n", + "For Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)" ] }, { @@ -844,7 +865,7 @@ "# Show selected alert when selected\n", "if isinstance(related_alerts, pd.DataFrame) and not related_alerts.empty:\n", " display(Markdown('### Click on alert to view details.'))\n", - " rel_alert_select = nbwidgets.AlertSelector(alerts=related_alerts,\n", + " rel_alert_select = nbwidgets.SelectAlert(alerts=related_alerts,\n", " action=show_full_alert)\n", " rel_alert_select.display()\n", "else:\n", @@ -1317,18 +1338,6 @@ "md(f\"URL: {url}\", \"bold\")\n", "summary.display_observations()" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration\n", - "\n", - "### `msticpyconfig.yaml` configuration File\n", - "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", - "\n", - "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" - ] } ], "metadata": { @@ -1382,10 +1391,10 @@ "height": "calc(100% - 180px)", "left": "10px", "top": "150px", - "width": "512px" + "width": "352.33px" }, "toc_section_display": true, - "toc_window_display": false + "toc_window_display": true }, "varInspector": { "cols": { diff --git a/Entity Explorer - IP Address.ipynb b/Entity Explorer - IP Address.ipynb index ab67e17a..e129706d 100644 --- a/Entity Explorer - IP Address.ipynb +++ b/Entity Explorer - IP Address.ipynb @@ -1,1972 +1,1975 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Title: IP Explorer\n", - "<details>\n", - "  Details...\n", - " \n", - "**Notebook Version:** 1.0
\n", - "**Python Version:** Python 3.7 (including Python 3.6 - AzureML)
\n", - "**Required Packages**: kqlmagic, msticpy, pandas, numpy, matplotlib, networkx, ipywidgets, ipython, scikit_learn, dnspython, ipwhois, folium, holoviews
\n", - "**Platforms Supported**:\n", - "- Azure Notebooks Free Compute\n", - "- Azure Notebooks DSVM\n", - "- OS Independent\n", - "\n", - "**Data Sources Required**:\n", - "- Log Analytics \n", - " - Heartbeat\n", - " - SecurityAlert\n", - " - SecurityEvent\n", - " - AzureNetworkAnalytics_CL\n", - " \n", - "- (Optional) \n", - " - VirusTotal (with API key)\n", - " - Alienvault OTX (with API key) \n", - " - IBM Xforce (with API key) \n", - " - CommonSecurityLog\n", - "</details>\n", - "\n", - "\n", - "Brings together a series of queries and visualizations to help you assess the security state of an IP address. It works with both internal addresses and public addresses. \n", - "
For internal addresses it focuses on traffic patterns and behavior of the host using that IP address. For public IPs it lets you perform threat intelligence lookups, passive dns, whois and other checks. \n", - "
It also allows you to examine any network traffic between the external IP address and your resources." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "toc": true - }, - "source": [ - "

Table of Contents

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Hunting Hypothesis\n", - "Our broad initial hunting hypothesis is that a we have received IP address entity which is suspected to be compromized internal host or external public address to whom internal hosts are communicating in malicious manner, we will need to hunt from a range of different positions to validate or disprove this hypothesis.\n", - "\n", - "Before you start hunting please run the cells in Setup at the bottom of this Notebook." - ] - }, - { - "attachments": { - "ipexplorer-mindmapv2.PNG": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAACpQAAAX2CAYAAACAl9mmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAP+lSURBVHhe7P17vB5lgh927h9pT89029N29/Q0fXVf0rSn78x0m+mmpxloUHMRAiEQiJsAQQuEQFyEuEhIIAQSEgKBLqALICEJIZAQl3YSx3HsTOzNxzNee+yJ491s4mzirHfj+JNN1rMbr5OpPb+in9N1SvWe80oc6ZxX7/eP7+forXqq6qmnnqqjU+d3nuf/8O/81XMqAAAAAAAAAAAAAIaXQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUAAAAAAAAAAAAYcgKlAAAAAAAAAAAAAENOoBQAAAAAAAAAAABgyAmUAgAAAAAAAAAAAAw5gVIAAAAAAAAAAACAISdQCgAAAAAAAAAAADDkBEoBAAAAAAAAAAAAhpxAKQAAAAAAAAAAAMCQEygFAAAAAAAAAAAAGHICpQAAAAAAAAAAAABDTqAUAAAAAAAAAAAAYMgJlAIAAAAAAAAAAAAMOYFSAAAAAAAAAAAAgCEnUAoAAAAAAAAAAAAw5ARKAQAAAAAAAAAAAIacQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUAAAAAAAAAAAAYcgKlAAAAAAAAAAAAAENOoBQAAAAAAAAAAABgyAmUAgAAAAAAAAAAAAw5gVIAAAAAAAAAAACAISdQCgAAAAAAAAAAADDkBEoBAAAAAAAAAAAAhpxAKQAAAAAAAAAAAMCQEygFAAAAAAAAAAAAGHICpQAAAAAAAAAAAABDTqAUAAAAAAAAAAAAYMgJlAIAAAAAAAAAAAAMOYFSAAAAAAAAAAAAgCEnUAoAAAAAAAAAAAAw5ARKAQAAAAAAAAAAAIacQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUAAAAAAAAAAAAYcgKlAAAAAAAAAAAAAENOoBQAAAAAAAAAAABgyAmUAgAAAAAAAAAAAAw5gVIAAAAAAAAAAACAISdQCgAAAAAAAAAAADDkBEoBAAAAAAAAAAAAhpxAKQAAAAAAAAAAAMCQEygFAAAAAAAAAAAAGHICpQAAAAAAAAAAAABDTqAUAAAAAAAAAAAAYMgJlAIAAAAAAAAAAAAMOYFSAAAAAAAAAAAAgCEnUAoAAAAAAAAAAAAw5ARKAQAAAAAAAAAAAIacQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUAAAAAAAAAAAAYcgKlAAAAAAAAAAAAAENOoBQAAAAAAAAAAABgyAmUAgAAAAAAAAAAAAw5gVIAAAAAAAAAAACAISdQCgAAAAAAAAAAADDkBEoBAAAAAAAAAAAAhpxAKQAAAAAAAAAAAMCQEygFAAAAAAAAAAAAGHICpQAAAAAAAAAAAABDTqAUAAAAAAAAAAAAYMgJlAIAAAAAAAAAAAAMOYFSAAAAAAAAAAAAgCEnUAoAAAAAAAAAAAAw5ARKAQAAAAAAAAAAAIacQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUAAAAAAAAAAAAYcgKlAAAAAAAAAAAAAENOoBQAAAAAAAAAAABgyAmUAgAAAAAAAAAAAAw5gVIAAAAAAAAAAACAISdQCgAAAAAAAAAAADDkBEoBAAAAAAAAAAAAhpxAKQAAAAAAAAAAAMCQEygFAAAAAAAAAAAAGHICpQAAAAAAAAAAAABDTqAUAAAAAAAAAAAAYMgJlAIAAAAAAAAAAAAMOYFSAAAAAAAAAAAAgCEnUAoAAAAAAAAAAAAw5ARKAQAAAAAAAAAAAIacQCkAAAAAAAAAAADAkBMoBQAAAAAAAAAAABhyAqUAAAAAAAAAAAAAQ06gFAAAAAAAAAAAAGDICZQCAAAAAAAAAAAADDmBUgAAAAAAAAAAAIAhJ1AKAAAAAAAAAAAAMOQESgEAAAAAAAAAAACGnEApAAAAAAAAAAAAwJATKAUATrjL73mgeuPn79Vfu9b38ns3317te/tIdeeTT3Wu5+Q43ut3qvvQmedWd6/bUD3/6qvV6bPndZaB6eQrs66qXn7zjerxF7Z3rgcAAAAAAACGm0ApAPTw4R+cV129bHm18+DB6uDP36sliLNw9RPVx86+sHObyZIAZYKUCVSWZQkAJdTXNFqnx56oPvqjGWP20VRCRCl/zYMrOstEAnKPb3ux3vdkBo4ESk+cD9Iv+iVQ2i33y/JNm+u2/vqcazvLMHlOxWfyyQ52CpQCAAAAAAAA4xEoBYAOCQI9umVbHQ7atOfV6qZHHquue2hltfHlV6qD771br5uMoF4vvcJLe4+8Vd28cnV11f0P11KnZ3fvqes5Xp1KiCiBp2de2d0zfPXNK6+vXjn0Zl1uMgNHAqUnzgfpF126rtXxXr9TyW/85JLquZFnQeTfXWU4cU7VZ/LJDnYKlAIAAAAAAADjESgFgA4ZxTOBoLuferoeFa8sz4iEi598qg4wjTfS5wfVK7yUIFACQc2yJWi1/523q3NvXTxmXVFCRK+P1DsBqDOvv6WzXEbVy3nn/CYzcHS8gUSB0ol9kH7RpetaHe/1O5UIlE6tU/WZfLKDnVN1XAAAAAAAAGAwCJQCQEtG78yodwndfPWyq49af/rsefUonk+8uKMOM5XlX5t9TbV2x87qwLvv1OGm51/dW50x76Yx2yZolNBmGQX0tXferqfM/tT5s8aUO5bwUpx/2131vnoFL0uIKPVLoPS+9RuPKnPajEurLfv21bbtf+2owNEnzp1ZLdu4qd4+dc/XxU88ddRopymXc2q2Q+rVDCT2Cii2z7tXoLSfth4W/faLOfc+WAfyFqx6fEyZixffWweNV27eWrd1rktR9luu17UPPlI9vOn5ep/Z15a9+46rj5f+uGak7vMeWDFads/hw/Xn5n3Vj9JvMjpk+kL6RPpG7tHPXnB59dPbl1TbDxyo65z65BzST8v2CSjm3Jp1fmzrC9WXZ82t16eNs7yp9Mmu9s89kXujea/cO3LPlXulnP+qkTZftGbdaLkdr79e1zVlSoA1y75w0Zwx+87zKcu/dMn79TvVncrP5PZztku/z95jufeax82zf9PuPdXOgwer71w1v2fd2s/jfvpx0XVP5Jya9yEAAAAAAAAw9QRKAaAlIa1db7xRPbVzV/WRs87vLNP29TnX1mGchNbKVMwvvPZateetw9X3r1tQl8m+ErxJsClhtUyPnJBZAjoJ8yTUU/Z3rOGlXsHLogR/7l63oXpy+47OMFoCUBlRb8HK1XWQrRkmSrgt0zgnoLTs6Wfruq94bksd1GpO65yv+dw8x1IuIaLJCJT209bDpN9+keud655gXjOIluuZtvvh/IV1mRuWr6qvTb5+ceaVddiyXK8EPje89HId+ix9tzliZ799vPTHLE+d0udyLXNNs6zZ7/uR80tYdP/Itjmf1C/9PMtynJxf6tFc3uxTt6x6vK7z6m0v1nVOOC71SH3SBp+ecVkdtEuANvLvcs7t9i/3QPp8AnPZX74275Vy/jlmAtzpw7etfrLaffhQfQ/OuO39MF7CgQn7JvRb6vqtuTfU5XJflWWnulP5mdx8znbp99l7rPdeOW7prwl4nr1gUWeZon1O/fbjhHzLHxm074k8T9rBWAAAAAAAAGDqCJQCQEsJbE0U9CkS5EmAJ4Gnb1xx3ejyhM52H/pl8CtTHydkc9fa9WNG0Zt7/8N1sCbhsbLsWMNLZTroBHra66IEf1LPWXctrY+XESvL+tTnkee31OG+1LsdKM3+s00Cec1tMtV0lmefWXbBorvrz0s3PDPmHOeveHRSAqX9tvVEfuXHs6a9rnp36bdflGucvp0+nvUJmSUI1gyZdl2bsqwZYIsE3PYeOVKHUfO53z5e+mMCfwn+lXJlVMdFa9aOLutH+keCl83+mXqu3/XSUcs/89PZ1bb9++uQ3cfPubiWf2f0yk+e98t2T59N21265P76cxkxtBmgjXb7lzYofbbIuZe+Xc4/x2yGFhN0TOAxodf09TOvv+X9kRxH2rmUKfdiM2TaS1e/mm666t12Kj+TJzqnfp+9x3rv5bglTJr1KVe26VW39vO4335cAsHNEWTz9YGNGXX1l88PAAAAAAAAYOoJlAJASwnN9BteyhTMCdW0w4wJ6CWoVwJoZSTI7107dhTNEnBrhvr6DS9l9MiZd943svzNOgyaqZ/LuqZmQKiECEvYJ+u/eeX19TTJqWMJ/5TzT5mMDJj9N6fejvZoiUvWbTiq3tEOKXaFFqN93u0AU79t3VzX9pcf2F+d+U416b67+593Lj9en7lzW2f9246lX5Tp7UvIrbRvMzjXdW16Xa+5Sx8ac7367eO9Amtl6vJyvfvVdb9E9p/9te+LLC99Jf0790LqM16wrd9Aaa82yNT7V9z3YN2He51/QnYJ3eX+y31Y+nXaLm2Y9Y9ve7G+plnf3LbtC8v2dvarD+q7r0xuP//sXS901r+p9NN2e/UyaM/krvVxLM/e47n3MhJvAtwJqzZDqL3qVq5DuT/77cfle058bfY1Y8r24zt99rnvvfGv67Jf3/zH1Zcffa867bZnqo9ecH3nPgEAAAAAAIBuAqUA0NI1Gl4JkyVUV5RQUgnZNNc1lXLZX8I3zfBRkXXN0FCv8FLX/iPBnbNu7B2Gawd/sv+MfJgREPM5gcKMbpdR7tply7mX8yj7bK4rYaVe59gOJPYKKLbPux1g6retm/ts+/XZi6rP3LF52vvoRfM76992LP0i/Sv9rEwdvmDV42P6QXRdm17XK5+b16vX9S/rSh9v97GiLM/1Ln2reT6lH7S1+03Rqz5Z3uwrCeGlbjlGAqgrnttcj7LYDNmV+rT7WPsYvY7Z1Ov8I+fSDAbmGiX0l9Fby/XLSLPNunX59ctu6+xX082fv+jGzvo3DcMzuUuvPtdcN9GzN5rnUo4bufdTz+YortGrbqVdy3043jm0+/GFd9xTf86orRmZ+L71G6u/cnl/4dLfvHlN9fmluyf0pUfeqk5/6m9X39z2T8YETb/29N+t74eufQMAAAAAAABjCZQCQEsZobOE7rIsXzOd+1X3P1zdvHJ1HcRph5cyIlvWt2W7bN8r8FNGc+snvJTj5vhl3xnx8Ntzb6hHxSvlurSDP2Uq7YR6EkZ65pXdtfYIdik7XqgpU4RnuuOTHSidqK2b+zzVHWu/yEiGCU0m6JXrVq57Wd91bXpdr3xuXq9e17/dx9t9rCjLc72b91zRHn2x6Lpfold9srzdnzP99zm33lGHSdM+Cb5l5NJSptd90D5Gr2M29Tr/yLk0g3glTJl7NSPMJlxapjkfFsPwTO7Sq89Fv8/eXvdeprn/2WNrqu0HDlTrduyq+3/ZplfdSruW5/F459Dux5F6pu+Wdjv43rv18+hEPbM/NufOOmT6vTf+tA6Wnnbbxs5yAAAAwPjGe0fB5MuMN49tfWHCGYqmWvtdkX5ytF7v1dvOmHdTPbvPwtW/nElsOsgfzK/asq2egahrPZOjn3fF00ne867avLV+r3wy7/V+nzGeRQCTQ6AUAFoSbkmQbPehQ9V3rjp6lMjyw135YaQEviYaNfBETK/cr/YPpOUcc9zL7l5W7R851jUPruhZ9limvN975MhRU4e3X5z0epHSPu/2S6l+23qYHGu/yEiXGY323vUb67ZsTncfXdem1/XK5+b1Op5pt5vlyvJyvfvVdb9Er7bJ8nL/JkiXl7PNFwsJA9791NN1qPTSJffXy3q9hGgfI/dA2jV9tZSJcpzxzr+E/xKeLC+My/2XF4p5SZP7cLq/TJ5sw/BM7nIsz95jvffSz9I2mZ4/IeVMf1+26VW39vO4V7l2Py7HLW0Z+Xe26/p+Mdl+5cezqt969g/rUOlnl/RubwAAAIjy82/ehTXl5/AHNm6qPnX+rM7tTmW93ou1td+flHcH7bbMH7pu3ru3Om/hnUftY9h9fc619ewuj27ZVr9P7PX+ZTpovyvqt59Mlfz+4+U336y+eeX19ee0afpj3jvmPVy7fDHn3gfr98S5DrkeXWV66fVevS2zZeUZk3fSXeunSoKkCQ1u2r1naEOlpZ80lf6Q3600/1D/eE3n+7xL3u1ueOnlesCDk9kv+n3GTPdnEcCgECgFgA4J2eTFVv4SuBmCifLDXflhJOvX73qpDvO0gzEZ8bBMJ5zp5BPku2vt+jEhp7n3P1wfqxnsOxnhpYwUlyBRRmNshtS6yuZlS+o474H3Q6eRc1g8Us8sLyMm9jrHnFvzxcnZCxbVIdbysilSPgGw5nm3X0r129bD5Fj7RX7ATzgx1y3t2JzuPrpecvV68ZXPzevVbx/v6mPN5c1+0Y+u+yV6tU2Wl/s3Aby0Qzt8mOBt7o9yzr1eQrSPkdEv2/dzXP/wyrrPp43KebZfuCQsmdBk+yVm7r/X33u33u+whqmH4Znc5YM+eye699LPElTOiKF5LmdZGf203ddzrPTDcn/2248zsm62a98T1z20svO+PVFOX/e361Dpx+ct61wPAAAAUd5H5o8ly4wk+bl8zcjP0pltYxiDVb3ei7W135803x2UtozbVj9ZL887i7y7aO9nWCWYliBps4/1+w5pKpR7pbwr6refnAgTvacrf7jdfLeabfLOO+/F2u/Ii2yXd1wpN97+e+l6r961bLroase8S9118GD9PnIY30unTdqzVOW95rMj92mCpSX83bVtl67rP53v8+mk32fMVD6LAE4lAqUA0CE/GGfEtrwkzJTAtz66pv5BMV/zOT8o5i/SS+grQZz8UJm/Is0LsZTNyHEJpGXEwox4WII72WdCUSmTUSLz0qX9IvJkhJdyvAQL88NrRrcbr2x+6MoPyDmflC3nl5d+zR+Yy0uv5jmWdkiblR+Sy7Fzjmnn8gN49tc87/ZLqeinrUvZYXA8/SLTp+e6t6e7jxJMywu2TN/92Qsu7/mSK5+b16vfPt7rBUlZ3rze/ei6X6JX22R5eZnQVecFK1fXoysmbF3+Yr28PEzfy1Th59xyR+cxyj2Qvrxs5BmR/eWeSf8s90o5z7TpppF6pP+XY2b/JdhXpA6pS0J5JUA4bE7VZ3L7FypFmZa/32dvv+dSjtu89zKNWv6ooBkMzTMibbp624ujx0wgOsvK/dlvP+46h0Vr1tZl8n2g2c4nUkYq/e39/2N1xr5/WX3od4fr+wQAAAD963ofWeSPJfOzcUYsbK87leVn+37COe33J13vIYoyEufJfDcw3eUPqvO+ZMZtS0aXjdeGU619r/TbT06Eid7TJTCa94TNd6vZJu/Sck+3/xC6yHa5JnkvO97+e+l6r97rXft00Ksd0z754/2MpNpcPgx6tUn5PUB+l5LfqTTXjafr+k/n+3w66fcZM5XPIoBTiUApAPSQANOM25fU0+8kvJMf8hKISQgvy9t/jXnGvJuqTCucsnkJkRdiVy9bPibgmB8y88N3wmFlf8s3bT5qqqQTFV5q/0Ca47R/4O1V9hPnzqwDcnmBkrrn6+InnjoqkJhyOae6Hd57tw4plZHomj8kp7227N1Xt1XKrt2xs1qw6vEx593rBW4/bT0sjqdfJOiVcFjXi7L00VzntO2rb71V/WD+z3q+5Mrndj/tp4/36mNleft6T6TrfolebZPlzZcJ7Trn3NO/0s+a251148I6eJcyCe+VfbWPkXsi90ave6WcZ0K7zXLZ909Hni1lP0WeNRkVozmS8DA6FZ/JOWaXZv/s99n7Qe69/CIs7ZTgaNpnzHN8ZF8J7V774CP1tuX+PJZ+3D6H1C3B1y/POrn9+TdvfqIepfQ3F6ztXA8AAAC93kf2Wtd+D5SfyzOiafM9RX7Wzs/VzZ/Zu34unmhf5Wfxtdt3jr6/y8/4+cPQbNMeZTHvWnOszMSTz+16ZJv8AW/eKTS3y8/15R1YymT/CX5OFM5pvz/p9R6iyB+edr3TK8o7yfxxd94x5x1FeT/5tdnX1G2Y8+tqq2agaPY9y0b/IDnvRPs555RJO7fPOcfNe+z6nckv3n233yGOKTOyv7yTykis7XdXTeWP2dvHm6gNm9rvwnLOF95xz1HHzbuivPdptl1GX2y+M8u/8x4tdc++ss+cU7PPtu+HY23z8xbe+f7vBxLq/EVbtgOLE907OXaWN3W1Vcq1w8spt23/a/XyXsHmcm/l3HMdcj2yvNf78vY90CxX2qtZ17LPdlsWaY+67/+ijdJeeUfdLJPrmT+0Tz2zz173dde+cg2ybqJ2LAMepD3KsmGRdmhe+6Yyy1naL+9X09/z3G2WKbM3rdy8tef1L/d5RsLOc6z09z2HDx/1/eRY3gH3s7/j0bzX8+/yOc+f+csfnfB4Ez2/i36/F/VTrtybGRAhZZv3aT/fx9tl8jXfh/Peu5Tp93s9wKAQKAUAGDKZRrtruntOjvJCp58XwZGXmXkBMqzT3TM9HWs/ni6+88p/V/3Ws3+vcx0AAAD0CnZFRjdMMKgEhhLsyQh1KZ+ZQjIrR2b7SFjrlkaoKP/OsjITSMJe2SaBuy/OvLLvfZWfxbMsoy3mZ/KbHnlstM7NIFMCivkj0G3791ef+ens+p1SpqxOwCV/UJr9l1BqwkjlnVOZ9SUzkCR4k/2nngnHtEM8bSWwU0I6E707aJdvK2G81PGF116r2ydhujK6aaljgj8lSFlGjy0Bp4QZdx86NNLma+tBDzLLSq7h3KUPjR6n65xfTBhypFzznMtx0x4pk/2lXs2RG3M9s/7lN98cU7cEzZp9oq0E9jKzTnN5v+9f0i7NcyjnmjZJmLWUy3vGzGaTdk8/bPazMqV56Stpp4Shmn02s9CU9mjfK8fS5uWPm7fu21+XKf0s51BmvYmJ7p0cM220bseuuv1+OH9h9ekZl41uH+XdajsMmTZNfdNeqUt7ZqjcN7l/EvS9e+S65DrkWFl3PIHS3OMZLOCG5avqZfmac0gIrd2WUfpl+ljaqPS3ZhuV65llae/m9UxblX2lf6aftveV887IuBO1Y3meTPQMOBWlnzSvfVPzuuXaJqiY+705EECC82n7tGmv61/u8+wr+yjPofTzZn861lmqJtrf8Sr3eukP5XPqtWvkGdk8Xp591z+8cnTbfp7f0e/3on7L5RqVcHyWr3huc/WNK67r63tvnoll8IV830yZ8v1zw0svj17vfr7XAwwSgVIAgCGSH5DzYqhruntOjvJCZ6IXwUWmudp75MjQTnfP9HSs/Xi6+OLyQ9X3D/+b6t/5xS/KAAAAoKkr2JXAz49HliewEgmhZHkCcgloJhBTypZ3bwnxfOGiOdXHz7m4DvpkZMBPnvfLEeTmr3i0DrdcuuT+vvdVfhZPGCehnFKuhHmaQab2iILlvO5au340PJqvSzc8U4edvnftgnrb9bteqsOA37lq/uj+Pz9y7ITPmuGcLu0w3XjvDsqInAnGfmvuDUetjxLGS9gnbVGWJzCUNklAqywrYa6E3rLv0ibN8F3kWDlmwlj5PFqP1jl/7sLL67BjOeeUyzbN6x/ZJtsmpJvPuZ65rs3wZGm/9IP0h7K8qYxi2A4o9vP+pZxDu24lbJh2KbMeZaCBhKCa9Us/yB+y5zhfvezquq+VwFXpK5FREdNHy4i37Xul3zYvAc/cEyUAFyVoVt5b93vvxHihv7xTzfHbgytkm7RZ+lHa6PFtL44532yX883XnGNz/82gaCkf7Xugq1zXsnZbjl7Tkfb4K5dfM1qu9PNS1wsW3V3Xa9aSX743TtulDUuYPMsSNk3Qt3ldvnv1/HoEyXJdYrx2TNi5PCva605l47VJ7qf0xQR0y33UfKaVvt58Nndd/3Kft5/tZQTUXL98zkyDmXGw+RyPhMZzX+cezed+93e8yr1eno/lc/sZVAL2aYNyr/fz/O73e9GxfM/KvZVrVYLzpWw/33tTv5xbZpEr2+br+yMDH6nP5VieVwCDQqAUAGAI5IfrvKTIi6j8gNz8a09OrvJCZ7wXwZGXc7etfrJ+CdX863+YDvrtx9PNJ29cXU97/+uXfbC/xAcAAGB6+NXfv6T6i5dcV33qmsXVp6+/e1ynXbekcx9NJdiVwE9bwiVluumESRLqKoGT5j4yUmgJlZVgWH6GbgZomvrd13g/iydI1Awy5d1fAmQJIOVzwjRdYbBmkPH02fPqEGo7WFfCQs1wTpd2mK5XfTNFcAJRCUBltL20UXN90RW86qVdx151TtAngZ9Sp1LHZlCoSJmyfYKWKVeCo0XeuSYsVsqVwNf9Tz87GiDrRwJpzbYr+nn/UgKb7bpF9pvrm+ucdk5gq6uf5fyuuO/B6rMXXD5meVP7epR7pYQg+23zEmprTw0eZTTH9NN+7p0i+065tFdzea5pQn7tUSOjuU2C1+n7CWJnXTl2CWUmdNncf6++2b4Husp1LWu35XjX9Jxb7qiDpL3um2i3RwkSZwTHBOTb5Yte7RgleFcCxRP5/K0PVX/5tkemtS8s/GWIsJeuNkkbZmTXjESc+ynPziwvz9Pcd/lcrmsJekbX9c++c4wcqyyL8kwu/aJ5fzTLldF0Sz/vd3/Hq32v97r3I0HkErpsLm9qb9/v96Jj+Z7Vvjcj2/TzvbcEgyPT9TfLFcfyvAIYFAKlAABDIH8Nmr9IzYvkvMAY78URJ1avFzptCZNmipS8cG3+ZS9MB/324+nmL8z6WR0o/c2b13SuBwAAYHr7+OU3V7+z8WB1/uE/rub+0Z9W1/xJdUx+6+HNnfstSgAoAcNMWRuPPL/1qCm7S2AlwaAuzeBKwj8JrGR5wi8Z+TFTUJcATL/7Gu9n8Yy+mJEhmyPlNUeFyzZd+y4Sbirn3g4bdYVzurQDO6W+XcfLiG1rt+8cM0JlW1fwqkj7Pf3Sy3XArbnfUsdedS7LSxv2OudImbJ9Kdc8VlMpl5Ht7n7q6fqdXgJ8me490z0nRNvef1NX2Cn6ef8y3jmUdQlH9WqTLgmlLVz9RH3sXKvmuZbr0T5ur/2X5eUcxruuWVYCsPk80b1TZN+pa9qruTwB0YT+EqhsLo/mNuVcSvCv3E9lhN/2/nudQ/s6dpXrWtZuy/bn8STglsBryme/RbO+6X8JzuVa5p5JsDjHb478G73aMbrq3cuHf++i6qr/0//a+Qw+Xhe8+190Lv+g/vxPx5+KPG3SbNemjFpZ/sggEkpMfy0jbea+Sz9qjo7b1Y5p77R7jlWWNZeXfjDe9cm6Eozsd39tpW5Fr2O17/Ve935kn817OiZ6fvfq/+3j9Fsuy1ImZcu92SzXrENTs/yFd9xTh3lzD+X3bHk2NEcPjn6fVwCDQqAUAACAofBr519dB0pPu+2ZzvUAAABMT39x1g3VTw7+/dEQ0Ky/+d9XZ+38G9U3V22vvnL32uqzN93fOSpp07GMUNoMqJTpeJvhlBJESbAkgcESPi3aoz0muHXOrXfUAZMETRJKyWhm2U+/++oVEooEADPNdfaZcqlvCcNFtkmw6eaVq4/af2SEymMJ53RpB3ZKffPH0s1jZWS/TEnc3r6tK3gVGSExx8lofD/52eL6OJnueMvefaN17FXnsry0Ya9zjpQp25dyzaBxU3vEyPSZTO2cQGmCpWn7nHdz/01dYacY75oX451DpjjPAAPHEihNX00/yj4TsMyU2anHDctXjbke7eP22n9ZXs6h13WNhLbb4bPx7p1SJvvuCr8lYJ17IdejuTya25T7p4Swc97NIGB7/73OoX0du8p1LWu35XjXtCkjmSYwm+m9MzV/6heZrrvdHgm1ZdCCRWvW1aNZJvScQOSZN9w6WqZXO0ZXvcfziSturT4x55Zp7ZNzJx5JMm3SfnbmmfztkbbvGrQjo4imnyZcmGdU+lVzdNyudkx7p91zrLKsubz0g17XJ9c2z6YPGijN94Hmcy3TtDfrXrTv9fbnZtmcZ/Oe7uf53av/t4/Tb7ksS5mULfdms1y/38fTFrnPSp/IPZTr3Xz29/O8AhgUAqUAAAAMhT931oV1oPSzd73YuR4AAIDp57Tr766u/MN/XTtj3b7qYxd3Tzk7GXoFVBIwa45SmgBJmT68K6xWJFyS9c0wSUJI9SiWP3+vDuz0u69eIaEidcz6jMaYkdTKdPeR0EszINelTB+c0Q6by7vCOV3agZ2J6juRXgG2Mv14wk9lWbuOvepclpc6pb0TqmtPmRwpU7YvU5Bn5NfxRpv79IzL6pmimkGz7149vz7Gpt17qo+fc/GY8kVzeuXm8n7asN8p70vorEzj3iyXoFTaIv010zVniur2PdC+Hu17pd8273fK+37unbI8+047pb3KshIS7XXN2tvkvtk/Uq/Z9yyrg6UJgZWgWLtsr77Zvge6ynUta7dluRfTHqVMkT6WQHbOadGatZ39plnflEv5sk0pc97CO+ttm/d7VzsWxzrl/alivDbpkvZJP7p3/cb6viyj3hZd1z/7zjFyrGbZsrz0i+b90SzX75T37f0dr/a93v7cLNue8r6f53e/34uO5XtW+96Mfr/3ljbN1+aytG85t2N5XgEMCoFSAAAAhsKHfnB+HSj93D07O9cDAAAwvXzyqturq//431YX//V/Vn1s5rWdZSZTO9hVJCiS0ElzGvmMQJkRyhIYaga1EnJJWCvLEvxJAKgdaiuhuhIq6mdfvUJCRTnWS2+8MSZIExlBMyNlJnTYDDvmXC65a2m9LOGahOgSfsxIhqVMOff2PtvagZ2J6juRruBVZH8JEaVtyrLUN/UudYx2oCjK8lKncs67Dx2qR8kr5T534eXV1n37R7dPeGj9rpfq9i3BqCKj0ZX2Svu2g7sltNSuS1MCnwl+ts+1nzbsdd1ybRNibYal0s/SDzLKXimXvrV0wzN1m2aK+HIPJAjWLHPX2vVjrkf7Xum3zVOv3EcZubbcS5GRUDNSYBnRsd97J7LvtFPaqyxLoHrP4bHB6qb2NqWfpx2y/2YbtcuWkV+bz4nUMXVt3gNdfbhrWbstS39rB91Kvyxh15RPPVKfUiZtmrYt9c2+EjRs76uEEJt9q6sdixyrK8x4qhuvTbqU/p37LO3VDvF3Xf/sO8doXovm8tIv0pcTVs292Lwn5t7/cH28El7td3/Hq32vl8/tZ1Dpr83vm6nTRM/vfr8X9Vsuy3LOzXuz6Od7b3k+t8PBCeyXfR7L8wpgUAiUAgAAMBQESgEAAAbHR34yu7r87/yr2q/9/qzOMpOtHexqSpgkI41lJMN8TkAmgb0S1MwUubetfrIelS5TnSdMk8DLqs1b68BKRlFLmQUrV9cBmBLg63dfvUJCRQmOJazUrn9GT3t0y7bR6XfnPbCiDtJsP3Cgrkumb065BNMSiMyy1DNlXhwpk/o3wzld2oGdieo7ka7gVaT9cx6Z5jv1e2DjprrOKVvqGPl3u85lebNO/Z5zKZdrkmuTa5RrlbBQwpcJ5Z51Y0b3/OW+UibXPfu6b/3G0WO2lVFGmyHOKG2YgGD21Vam2m8fN0GnMt1+wmZlf6Wf5Tpl6vPsI/VLWGrxyPVLEKoE4nJeOb+0R/aVNm9ej/a9cixtPufeB+u6JWyWupa+mHMo4ch+751I22Z/d4+0X2mTLGsG2dpSn7Rt2rgsyzY5x/Yoru2ypY1KO+Ycnv3F/du8B7r6cAkFJmRcptTueu6U/pZ+nv1H2qvZRmVfqVv6ZPpP/p1jNuvbbu/c/xteerlu2/I8i652zPJ8nSgUfarq6icTKf2oPd19dF3/cp8375Eoy0u/6LonMhJq+k7u69LX+93f8Wrf6+Vzzjn3ce7n0l/Tn5rPoH6e3yl3rM/licq1vz8V5ZmYevb63pt95P7OMzGjxKZMRgfOccsz5lieVwCDQqAUAACAoSBQCgAAMDh+e/2B6po/qarP3rysc/2J0BXsKsqoZwnJJGSSZZ84d2a1rBGIydcEZD51/i8DsAlzJoyaUEnKJLiS4OcZ824aLdPPvnqFhJp6TZ0e7XokHJPgy9dmXzOm3E9vX1KfZ6lDAnNrt+8cE87p0g7s9FPf8fQKlCb0mEBcRp/M+pxPQkoJyubfGVku9Ux923Uuy9t16vecc81y7XINE4rKiJpXL1s+ZtTXZplSv7R72r+UaUsYKfVvH6+0YfbTpVm+XbcEuy68454xo+VF+tPyTZvr6599pH7XPvjImHP48qy51bodu+pwVPa3dsfOOvyW0TBL6LV9r5S2bZ9DWd5u84z+t2XvvvoYkdDs968bO/plv/dOrnm2z3lnfQK6uZ7ZtlmuKfVJ26aNy7JyTgkETlQ2dajr/4t6pY3a919XH8455T7PNq++9Vb1g/k/O6oti7RHfV7jtFFX30392yOXjmnvkbLpuwn6NftHux0/fs7F9fJMT57zb7fLMOi69hNJu6f9u/pf1/Xv9awsy5v9ous5nvu5+T3nWPZ3PNr3evmcvpN7oNQtz+g8q5t9rJ/ndynb73O5n3Lt709N/Xwfb5cp3z/zrCxl+n1eAQwKgVIAAACGgkApAADAYPjwjy+u5v7Rn1bnHvh7nevhREqQLqPWtYNLJ9LMO++rw0ozblvSuZ7+Zbr63YcODd307CfK9Q+vrEdsbIdZ6ZaAZNd096eqEig9mc9LAE48gVIAAACGgkApAADAYPjiolX16KSfvuGezvUwWTKa4C2rHh8zil6CYAl3ZgrkZtkTKaPbPbplWz21slAW08UXZ15ZTyG++MmnjhrtlqPlPs7ovl3T3Z+qBEoBTk0CpQAAAAwFgVIAAIDB8Dsb36jm/aP/rfrQ7/5yGm44EW5YvqqevjhTLmcq5gUrV1e73nijDpSedePCzm1OlK/PubaeujlTiyeY1lUGTpZM8/3EizvqabtPm3FpZxnel/Do5fc8UD9H8jyZc++DneVORQKlAKcmgVIAAACGgkApAADAYPjpO/+kmnHkTzrXwWTKqIszbl9Sbdm7rzr43rvVgXffqQN0Z8y7qbP8iXb67HnVY1tfqL50ydzO9XCyJFC9ass2YdI+ZCTXnQcPVvvfPlItfOyJ6sM/GJ4/hhAoBTg1CZQCAAAwFARKAQAABsPlf/d/qn64/T/oXAcAAMCJI1AKAADAUBAoBQAAGAxz//7/p/r+prc61wEAAHDiCJQCAAAwFARKAQAABsM1f1JVZ6zb17kOAACAE0egFAAAgKEgUAoAADAY5v2j/636rkApAADASSdQCgAAwFAQKAUAABgMAqUAAABTQ6AUAACAoSBQCgAAMBgESgEAAKaGQCkAAABDQaAUAABgMAiUAgAATA2BUgAAAIaCQCkAAMBgECgFAACYGgKlAAAADAWBUgAAgMEgUAoAADA1BEoBAAAYCgKlAAAAg0GgFAAAYGoIlAIAADAUBEoBAAAGg0ApAADA1BAoBQAAYCgIlAIAAAwGgVIAAICpIVAKAADAUBAoBQAAGAwCpQAAAFNDoBQAAIChIFAKAAAwGARKAQAApoZAKQAAAENBoBQAAGAwCJQCAABMDYFSAAAAhoJAKQAAwGAQKAUAAJgaAqUAAAAMBYFSAACAwSBQCgAAMDUESgEAABgKAqUAAACDQaAUAABgagiUAgAAMBQESgEAAAaDQCkAAMDUECgFAABgKAiUAgAADAaBUgAAgKkhUAoAAMBQECgFAAAYDAKlAAAAU0OgFAAAgKEgUAoAADAYBEoBAACmhkApAAAAQ0GgFAAAYDAIlAIAAEwNgVIAAACGgkApAADAYBAoBQAAmBoCpQAAAAwFgVIAAIDBIFAKAAAwNQRKAQCG2OX3PFC98fP36q9d66ebM+bdVG3Zt69auPqJzvWTbbLb52NnX1it2ry1WrdjV/UbP7mkswzH5yuzrqpefvON6vEXtneuP1anwrXS344mUAoAADAYBEoBAACmhkApAMA0lWBcwoxNB0ckNLfwsSeqj/5oRud2x2LQAqXfv25Btfvwoerup57uXD/ZTkSgdMNLL1fPv7q3Om3GpZ1lhtWSdRuqfW8fqX7v5tvHLP/MT2dX2/bvrza+/Erdfs111z20snr9vXerixffe0ICpZN1re588qkx93EcHKn39gMHqquXLa8+/IPzOrf7oPS3owmUAgAADAaBUgAAgKkhUAoAME0lGLf3yFvVzStXV1fd/3AtAbpnd++pg6WPbtl2TKHSrnDkZAcmTzXa5+RJKDTh0PTx5vJzb11c7X/n7TpI/K25N4xZt+K5LaPLJztQOpkSKH1t5BzytdzLNz3yWPXCa6/V9/LikeUfOvPczm2nQkYzfW7Pq7VTbWRTgVIAAIDBIFAKAAAwNQRKAQCmqQTjEpBLUK65PCHShEkTskvYrrluPF3hSIHJ8Wmfkyeh0IRDExJtLi9hzAPvvlPNuffB0eUl9Lhp957q4+dcPO0DpV2jr2YE0Wde2V3X+6uXXT1m3VQSKAUAAGCqCZQCAABMDYFSAIBpqlegNM6/7a7REQ8TsssohwtWPT6mTBnxceXmrXWYLcHIouy3BCavffCR6uFNz9f7zL627N1XnTHvpjH7S5A1U+2/cujNepuUXb5pc/Wp82eNlimhvjUjdZ/3wIrRsnsOH64/f9BRGBPIy7nkvPO5BN8yHfr85Y/2PF7qvffIkeqH8xeO7qsEGJc9/ezoskigsQT8Jrt9egX1MpV/piXPNOiR/Z+38M7R9cMgodCEQ9MOnzzv/TZL4DLXNqPypk2aYdP29TuWvneyr1WvQGmvdV+bfU21dsfOOkSbY+R47f72iXNn1nVulsnortlXCUB3nUOm109/bp77Y1tfqL48a269Ps+dLG8q91up64KVq6sdr78+Wu+07Yzbl9RtUdqlXedyfdZu31kt27iprvdUhH8FSgEAAAaDQCkAAMDUECgFAJimxguUNoOVX7pkbh3uSvAuAbxSJkG7PW8drkOUKXPD8lV1OCxfvzjzyjpYVgKTCd1teOnlOnh37/qN9b6bIbSPnHV+tWrz1joolvBZpuwu5RICPG3GpXW5EhrL8tQpwbNM7b39wIHR8Fmp3/Fonnc+l8Bc6rXr4MExx0vw8/qH358+/ewFi6r9I9s1p1MvQdzmeSbImCDcUzt31ec82e1T6tvcNgHFXKdMf576Rf6dwN3MO++rywyL9Nnm1PYlNJqwdNZt27+/+sxPZ9frEphujlrab9+bimvVFRqNBDEfeX5Lvc/vXbugXvb1OddWO0f6cuqd+pdjpEyOnzJllOLmOSRsWwLP4wVKbxlpy2y3etuL9XaL1qyr65bj5bnw6RmXVd+5an4dDo38u2yb88j+c77Z54rnNlffuOK6au7IfnLsco+k7V9+8836Opw+e169bbk+OXauaZ5vOb+sO5kESgEAAAaDQCkAAMDUECgFAJimxguUXvPgijrYlbBZCaU1g3gJxW3Zt29MyLSEI0vYrLks4bSE1MryhPeaI3pmav1MsX/X2vVjRnpMkCzhsoz2mM8lNJZAXIJxpVwZUXXRmrWjy45Hr0DprjfeqINtpVyCcQnIpQ3SFl+4aE4dbkv4LutLm2Vfr7711mjQL6G+BPfKaK+T3T5dAb+0ScKuCb2W7b579fw6xFrqOyzqkO9771aXLrl/9HPaOdcnAdLmtUofaAYx++17U3GtugKluS/Tz3LMx7e9WPevhF2zn3Z/Tqhz96FDoyO0lnNYuuGZMeeQAPV4gdKuUWBj/opH6+1Ku3ede5RA6eKRr+W4f+n3L6qeeHFHtW7HrjFlZ921tB4hOc+ofO51fU42gVIAAIDBIFAKAAAwNQRKAQCmqa5AaUYVzUiI7dH/yvT2JbxVgpclHBfjBUqby2Lu0ofGBODKaKclvFdktMiMGlmCqyU01p7KOvXMFNslCHq8ynm1A6Xt4FssWbdhNPSZ8FtCe2WEyxIwTUCwHs3yFwHSZoAxnye7fbrqm3BwQoUZ1THXt7n9RD5x7cPVF5e/Oe39xSvv6ax/Wwn05trlcwKU5ZqV0UrTxxO8zCiyzWBkv31vKq5Vjp1+1JbwbMKYZVTUr152dX0Ozan9I/VJvUpdsr92sDXK/VH6a/sc0m5Pbt9RH6OEobt0nXvkuM1+P572vdrr+kzkN29eU31+6e4JfWbxluqTN62p+9qv/uSKzn2FQCkAAMBgECgFAACYGgKlAADTVIJXXSG0yAiGZ934y0BYCUiWqdoTkNx75K3qzOtvGS3TFY7sFZjM52ZwrCvcWmRdjp069AqNleUlXNZW6lH0OlY7pNYr+BbZZ0K2Cdvmc4KIJWCaUStzjEwhnqBpabeEDUuAsexjMtunq76fOHdmXYeM/JiRNFOX7L85Imovn7v35ep7B//fk+67r/zzzuXH69N3PN9Z/7aEQxMSTXgybZlrUcKVJVSZtsoItGnTZvCy3743Fdcqx075fM0085kWPue26+DB6q9cfs1oudK/m/dCU6lLr3Mo25f+2nUOCdLmHLO/BG0zbX3ug+ZIp13bRerf7PdFwrVXL1tejwqckGyzzqXde12fiXxnpC8mAHqsfufA/1R9de3frP7CrJ+N2Z9AKQAAwGAQKAUAAJgaAqUAANNUglcJhd68cnUdQosr7nuw+vbcGzpHR0wYMgGxBMYSvHvmld11CK+sT8gsAa8SNuu1rCzvJzCZEFpGWPyggdKMzFjOMTL1drPuRQnMlf30Cr5FzqEZKC2jXyZYet/6jaPtk1FJM534795waz0deDOkONnt06u+KZcpzhetWVcHDRPKS2j4zJE6Nfc3DNL+actcl0xxn6nTy7pc97TlZXcvq0foLCPyRr99byquVY7dDmKWKeGb90Tp36lH834oLlh0dx187nUOZfvSX3udQwKw59x6Rx0mzTMjAdmMXFrK9Nqu6zzSHrmfEpjNFPzfvfrGul6XjJxfrlE5v17XZ7J8+OxLqz9/8c3Vx+ctqz6/dE/1W5v+aDRc+rWn/271K783sy4nUAoAADAYBEoBAACmhkApAMA01Ss01ktG3dz/ztvVves31lODN6e7j65wZNeysrwZHPugU96X5c3w3PEogbmyn17Bt2hOed8su27Hrvpr2cc3r7x+pG5vVnc8sa4OlibIWPYx2e3Trm/CeJ+/aE4t/y7bnbfwznr/j219Ycz+hkFCojn3R55/P1iasHFZlz6ekHXWNa9t9Nv3puJa5djN/hI5RkLNzVFKy7T+Ob/mMdoWrVl71P6i3B+lv7bPIUHSL10yt/532Sbh9LuferoOlSbI3bVdKdt1HtlfArUZubVZ51KX0u69rs+J9OFzZldfuH9PHSD9zsv/bfWR8+cJlAIAAAwIgdLBlHcDNz3yWLXn8OH6ndqyjZs6ywGDbyre9wEAJ4dAKQDANHWsgdLTZlxabdm3rzrw7jt1YK453X10hSO7lpXlzeDYubcursOqd61dPyY0Nvf+h+vjlfBqv6G+49UOqZXgWwJtGTWylPvchZdXW/ftr9sj7VKWJ0yYkRSzj4QTsywjPmbq8ozUmDom1FfKT3b7tIN6CRUmwJhRMRPMK9uVcOMwvoxLSDSh0bRbrkuuT1mXkUPTVmn7tE/aqazrt+9NxbXqCmJGGaU0I3zmc46xftdL9f3bDMtGRhQtfbycQ0YEbZ7D9Q+vrIOhpb+2z6GM0tsOrOZeyH3Ra7tSrus8SvtmVNXmPjOtf3ME1l7X52T42Jy7qt95/X+pvvPS/636lR9fIlAKAAAwAKYyUPqJc2fWQcj80WfeC2V2ki1791Uzbl8y5mffU1GvdwL9mnnnffX7lU0j2+ePhtt/0Eu3vC/Je5O8P+la36W8H5roD5Pb8sfFVy9bXu08eLB+jzRM/Xu6a7/7bpvK92tdplt9AIDJI1AKADBNHc+LxATT8qK7Pd19lJeMCell6vzPXnB534HJhPpWbd5av2DMSIyZfjsjoaZMpokvoc1eL5HK8l4vw/rVfqlWXnLnHLYfOFCPgJCX1QmT5uV1QoLN7TP9fUJu7TDiglWP1/soo1OW5ZPdPl0v5TMiauqaOqfuCeJteOnlel/XPLhi9JjDogQ00+65Ls11eamdkTCzrj0iZr99byquVY7d7C9F+lru1YSZM1Julp29YFEdqM0vrW5b/WRdvxXPbakDnxl1Ny/9M9Loo1u2jTmHUma8QGnXuS9YuboOZDfrkHKZAj/1+Nlja6pzbrmjXt51HqVs9rlmpO3nL3+0HgU4n3OdSrv3uj4nyyeuW1EHSf/dJ/6GQCkAAMAAmKpAad4L5P1AfsbOz9752T/vm1547bX6Z93bH1/bud0H0ev901Toeh9yLPJ+Iu80mn+wzcSO5z1w2jr9Jts1Z/gZz+g7pZHt0qdvfXRNtfjJp+r3qlmWfwuVTp32u++2qXy/1lW3qarPB31OAQATEygFAJimjudFYsJo+98+ctR095EXhhldIYG4V996q/rB/J/1fGGdz+3gWLbPfhM8yzZ5sb580+bqU+fPGi3T6yVSWd7rZVi/2i+uysuj51/dW4cPS90yrVZe+LdfgOblauqRF67N5WXkxnb9Jrt9er3syrTpGQkgv5jIy9uMEJAw7DC+wC2h0Xb7Fglypn3bYdNj6Xsn+1rl2L3Op4xS2hxt9Ix5N1UJN+deLcfIyBEJk5btMlpK6lyXGdk+90DaJscp/bXrHNrnnu1zrByz7DvOunFhPRpryiSEmmW9zqOrLhctvrc+Rhm5tNf1OZn+3Sf+wzpMKlAKAAAw/U1VoDR/MJqfbTMLSHN5fp7OH1DmZ93yB5mTpdf7p6nQ631Iv47nfSbH3m6ZPSfvbfKHynmn2e8fpdez24z078zc03zPlP6doGne7WSU2eY2nDztd99tU/l+ratuU1WfD/qcAgAmJlAKAHAKycvDrunuT1VeHsEvXbrk/jqAOh1+ATUd/dp5V1V/9cj/JlAKAAAwAKYqUDreH4XOXfpQHZ668I576j+GzTu49pTu+SPmzPhR3s19bfY11dodO8f80Wj5w9QS0EqYtGiHCr9/3YL6DzcTAsw+sq8vz5o7ur7UN39k2iyXP/DM7EQ/vX3J6OiT+YPahzc9X/9haNm+retdWznG7HuW/fJcRo6TgG2py0Tn0s8f9w6zYw2U5h1wBhXIH/QmVNqedalLubYJoiaQ2l6foHSuT2aiyYw0Wdbrfuiqb46/+Imn6v6fa5x9Nf/gv4QP127fOTroQfbT7700DLpCm02lDdsBzjHPmZF7M8+C9h+PJ0CcP1jPMyjPg67nSQm3Z8ai+nky8u8FKx+vj1nu6yh9onlNF61ZN3rtMxtS3lO2/wC/n3rmmfDASP8o+8rX7DvPkKzPuTfrEr3aCwA4fgKlAACniDJSQtd096cqgVKGUe71u9dtqL4+59rRZXlB+8jzWzpfwPNLX1v/nwqUAgAADIApHaH05+9VDz/7/GiAqUuZcaQ5g8pnfjq72rZ/f1XCfV+ceWUd5nz5zTerBStX1+G6rMv+bxnZLvtPsO+G5avqUFS+ZpsycmRmIkqYatOeV+vAaPaRfWWfKZcyCVJlfwkXLnv62foYCQRmWYJeeU9w7/qNY5aPF77qFSjNuaYuj219obrq/ofrsF9CYfmc8GE5lzKK6w/nLxw9l6zP7CcJkJXtU6eE0jbt3lOdNuPSo+oxbI4lUJr2zLUs1yh9I9dmouBlmaEp16Brffb71M5ddeD0CxfNqZfl2vcTKM31zwinKZtrm2u8etuL9TVPX0+ZEj7Mst2HD9X7uOmRx/q6l8ryU93xBErzfjAh0TwX0p7pDy+89lp9rRNIT5m8N1z8i/u43IMJaeZYz47cg+VeL4HS3NvZR67pj0fqlHv5kpHrlOdMZlnKvZ5r3rymL/7i+JG6ZB/N0W77qWeeBXkmvB8iXTumH6W+KfPpGZdV37lqfj2DVOTffi8AAJNPoBQAYMDlpVpe9uRFYv66f869D3aWOxV1veSGU93nLry8fmGal++3rX6yfrmal8F5uZpf0JRRJDjaZ+/eXgdK//JDBzrXAwAAMD1MVaC0BOMSqsooewm5dY3mmGUJ3iWAV34OP/fWxdX+d94eDcaVmUQysmnZ7vMXzalDVAlNffyci+tlJcTVnHGkvPPKCH7NwOWM25aMOUYJeyYwWsrkHNbveumo5SWk1zx2W9e7thwj55FAWhlxsIQP00bN9ukKRpZ2yTTrzRELM1JrQmcZubQsG1bHEigtwdDSbmVk0fvWbzyqbNNEYcVo16PfQGn6eN5Lt/thAsYloFrChwkVNv9Iup97aVgca6A07ZV3grkPv3HFdaPlErLcfehQHfzO57R/7ukVz20ecw+mD+W6nX/bXfXn8izKMzDXr5SLrrqV+jRD7pG6pE4lENxvPS9YdHe9v1lLlo6WyfYZQCPPrjzDsszvBADgxBMoBQAYcHlZkxdx+QvhvAQqoxgMAy+PGFbt6Z/yi4Pc/+2XvYz1F6+8pw6UfmX1X+tcDwAAwPQwXqD0V8++uPr0DfdUX1+xtfres4eqs3b9jercg39UnX/oH47rvDf/QfWpa+/s3GdTmRo64af8zF3CpWWq+lIuIaj8see35t5Qf07QqjlSZAnF3f/0s+OOstgVKC3hrXagLsGwhO9KoGy8wF/eFZw+e95Ry8d7j9b1rq3XMZas2zAmVBjtoGFk5NQEINszqkzGKJQX/Xv/10l14c//y87lH9SHfzT++XW1Wy95/9Nsz/TJTBu/Zd++cUd7nSisGKlH81qP179KfcvxmyObFum/ZfsSPsy2zTIx0b00ka/et76z3aebrz+yrbP+RblG5bnTS2nDr152dd2mJZBZ5H7KfTXRO/P2s6frWVR09Z/xrmkCpKWPfNB6NvtbPnc9pwCAySVQCgAAwFD4tfOvrgOlX33yP+pcDwAAwPTQFSj91DV3Vj/e859U1/xJNeqqf/Bvqkv/1r+ofvr2f94ZIm3qN1DalNETM9VywkwZpTN/3FlGUbx48b31KKCZurkEozKSXglH5o8+737q6XpGkYzEmanrr394ZfWJc2eOOUZXiKss66UEuPoJ/LWXjxfC6gpq9TpGlreP0XXcXnUp67qCiP360a7/qDr71f90Wvu9V/52Z92bxmujpgRGExzNTFWlH0amjc9Ik+mTzfJNXYHAtnY9+ulfpc909dMo26dstsm2zX3FRPfSRP7y7as62366SfC1q/5FuUZPvLijnpGo7eaVq+ugbWnDUr6r3aN5H6ctF65+or4GeZY1y5VnT9ezqOjqP+Nd02bfOZZ6fm32NXUYtV2+9LeU6XpOAQCTS6AUAACAofDhc2bXgdKvbfg7nesBAACYHtqB0jO3vDsaIP3hjr9efXnJE9XHZv5y6uQTLWGshKZKQCrLygibCff97g23jpmGvCmh1JseeawOlCZYmkDYzDvvG13fFeIqyxJg7QqWnXPLHXW5fgJ/7eXjhbC6glq9jpHl7WN0HbdXXTKyZYJzHyRQeqro1UZtZdTbZtCu6ZHnt4wZRbepTJWfsF7X+gRUM+18RuNNn82yfvpX6TOZQSuB6XZfveK+B6vPXnB5XTbbZNvmvqLfe+lUV4KXafeu9e02LOV7BVAzhXyua8LtaduUTbt+fc619b5uWL6q7jfl2dP1LCq66jbeNW32nX7rmRFqX37zzeqF116rQ9LZf6zbsWu0v2XfXc8pAGByCZQCAAAwFD70g/PrQOlvPfuHnesBAACYHpqB0kxnnzDp9ze9VX34xxcfVXayJPSWURHXbt9Zffyco4/TFbbKdO6ZWv7e9RuPmqL70zMuq74488p6Cv2y7LtXz68De5t27xk9Rtd+z7/trnrEyYlCdf0E/trLxwthdQW1eh0jy9vH6DruiZzy/lTR63o1JSiawOiew4frgHI7lJewXvriN6+8vnP7cm0T4C2B0aZsl+2bo5/2079KELXXfouUzTbZtmv9ePfSsCjBy7R71/p2GyaAufvwoXGDxPHD+QtH2vTo/bafPV3PoqKrbr2uaery+LYXR/tIv/VctGZtX8+zrucUADC5BEoBAAAYCn/uRxe+HyjdJFAKAAAwnZVAaRmZdKKpoidDgnFlquUyAmiRENTSDc/UIc+EPcvyesTIkfIZebQ9RfeK57YcFYwr4btmEKorxPW5Cy+vXjxwoA7pfXnWL0N6qUdGN034NZ/7Cfy1l48XwuoKavU6Rpa3j9F13DKq5l1r148Jk829/+G63YZxJMq2XterKWHN9IcE9bpCeemXaedrHlxx1LoiI4gefO/d+lo0g84ZwfLRLdvq69EcPXfu0ofq6dHn3Pvg6LL08fT1Zn0TcM1+cy2bdTt99rzqvIV31stSNtvkXMv6pvHupWFxrIHStNH6XS/Vge2ERptlz7n1juobV7w/inPZ75J1G0bX55qkH0xGoDSj02bU07I8x01wvoST+61n9p0+cPaCRaPrT5txafX8q3vH9Leu5xQAMLkESgEAABgKAqUAAACDIYHSH738H9dh0h+8+O93ljkREorafuBAHRxNwG7eAyuq6x5aWU+5nMBcliV8V8on7LRl3746hNUORp51Y0YFfKsOVi1YuboeRTKB1eznvvUbR8uVwGWCpmV68CwvgcvUJ4G91CUBrdSthAZ7hT17BRSz/GQHShMoW7V5a33eOf+0Q0ahzD4zUmvasJQdVmm39JWbf9FPmkqfyDVPGzbDnU1lxNfxwpjpu+VaZFrxWx9dU0sfS3B08cg1bQZCy6ilmYY8fTj9MNu9PrJ98zrnGuZapr8mSJ1637b6yXpUyk0jfSkB6ZTNNiUM2TbevTQsjjVQGglfpu+krdPmaftcgzwnEiBNcLi0bZZlXa5jrkuueb+B0tIXtuzdVz+LvnrZ1aP1SX/adfBg3Uciz7w802bctmR0+37qWZ6F2WfKZHn+nTrla+lveabkWZj9/eyxNUf9AQAA8MEJlAIAADAUBEoBAAAGQwKll/0n/0N1xX/2P5/Qae67fOr8WXWwrg5K/fy9Wv69cPUTnUG9hJ4SbOqaovuMeTdVmdI9QbuEohLISliuGUrNv5dt3FSXefWtt6ofzP/Z6LqM7pgAVwJbkX/PuH3JaOhvEAKlkXPMeef80w4JkS3ftLlu62a5YZV2S7t0Sduf+7PFdag5bZsgX9c+ItPG9+qLRYJ7Vy9bXo8qWffvkX6VvpfjJKzaDJTGT0f6W0ZGTV1y3R7e9Hw9Wm/7On/i3Jl1P87xUzZfHxj5XK5xymabZhiybbx7aRgcT6A0ms+ZXNNc21zj5ii0Gem4BONTbu2OnXVYOSOCpt1TZrxAafpFwscpn33MumvpaH3WjNRn0Zp1o9c+/SX9pr2PfurZ7G/ZX/abAH575NIE9ku5hKTLcgBgcgiUAgAAMBQESgEAAAZDAqUZnfSbj+7oXD9dlJHyhnWKbk4NCRtmhNEEBTN67FT0ZfcSAMD0IVAKAADAUBAoBQAAGAzz/vH/XgdK//yMKzrXTxffuWp+tfvQoaGdoptTR0KcDz7zXLX4iafGjBh5sriXAACmD4FSAAAAhoJAKQAAwGC45h//WXXlH/5p57rpINOO3/TIY9X2AwfqaZdPnz2vsxwwPvcSAMD0I1AKAADAUBAoBQAAGAzX/MmfVZf+rX/RuW46mHXX0np68ATgzl6wqLMMMDH3EgDA9CNQCgAAwFAQKAUAABgMme7+4v/gn3WuAwAA4MQRKAUAAGAoCJQCAAAMhgRKf/rOf9G5DgAAgBNHoBQAAIChIFAKAAAwGBIonXHkTzrXAQAAcOIIlAIAADAUBEoBAAAGg0ApAADA1BAoBQAAYCgIlAIAAAwGgVIAAICpIVAKAADAUBAoBQAAGAwCpQAAAFNDoBQAAIChIFAKAAAwGARKAQAApoZAKQAAAENBoBQAAGAwCJQCAABMDYFSAAAAhoJAKQAAwGAQKAUAAJgaAqUAAAAMBYFSAACAwSBQCgAAMDUESgEAABgKAqUAAACDQaAUAABgagiUAgAAMBQESgEAAAaDQCkAAMDUECgFAABgKAiUAgAADAaBUgAAgKkhUAoAAMBQECgFAAAYDAKlAAAAU0OgFAAAgKEgUAoAADAYBEoBAACmhkApAAAAQ0GgFAAAYDAIlAIAAEwNgVIAAACGgkApAADAYBAoBQAAmBoCpQAAAAwFgVIAAIDBIFAKAAAwNQRKAQAAGAoCpQAAAINBoBQAAGBqCJQCAAAwFARKAQAABoNAKQAAwNQQKAUAAGAoCJQCAAAMBoFSAACAqSFQCgAAwFAQKAUAABgMAqUAAABTQ6AUAACAoSBQCgAAMBgESgEAAKaGQCkAAABDQaAUAABgMAiUAgAATA2BUgAAAIaCQCkAAMBgECgFAACYGgKl8AE8/sL26uU336i+Muuq+vN5C++sdrz+enXZ3cuOKjuIfu/m26t9bx+p7nzyqc71AAAwSARKAQAABoNAKQAAwNQQKIUePnLW+dWT23dUe4+8VZ15/S2dZdqB0pl33le99s7b1dXLlh9VdhBNRqD0CxfNqR55fku9nzd+/l7dPo9tfaH62uxrRsuU42T9eNLe379uQbXnrcPV1n37q89dePmYY33ozHOrpRueqQ6OlL3+4ZVj1gEAgEApAADAYBAoBQAAmBoCpdDD9659P7iYIGOvQGU7UHqq6QqUXn7PA3Wb5GuzbJfvXn1jteuNN+oQ6aNbtlXXPbSyWrV5a7V/ZJ9p2x+P7D/lPnvB5dUV9z1YXXX/w7UcL9s88eKO0WVxzi131OUXrVlXHXzv3eqWVY+POd5ZNy6sA8AbXnq5+tjZF45ZBwAAAqUAAACDQaAUAABgagiUQg8LH3ui2nP4cLVpz6vVln37qtNmXHpUGYHSo7cpEuhMsDMBz7MXLBqz7jtXza92HjxYPTfStr/xk0vGrIuJRkbNtdi0e0+1+9Chel9Z9tEfzajW7dhVHy/B0vY2AAAgUAoAADAYBEoBAACmhkApdEhgMSHSTHk/+55l9WiZFy++96hy7UBpr7Blpml//tW99aiaB959p1q7Y2f15VlzR9cnOJkAZY6VdSmTsglIlnLn3rq42j9SjwRdy3ZRRlJd9vSz9ecP/+C8esr9BDYz9XvX8XrVs9Qjgc58bgY7y7+zXTFemLbU9771GzvX5zxeHznHrnadKFAaGa00ZTLi6UfOOr+au/Shen93rV1fT33ftQ0AAMNNoBQAAGAwCJQCAABMDYFS6FCHId8+Us2598HqMz+dXW3bv7965PktRwUV+wmUZnTOjJqZkU4z5fuClatHtnmz2n7gQPXFmVfWZRKcTBgy5R7b+kI9vfuK57bUYdB8TmAyI3lmRM+NL78yZjr37DOB1/Nvu6uu3+Jf7KvsJ9PDJ3j57O49o6OBHk+gNCOAfumSudUNy1fV2+Zr6p8Aa3MfxaI1a+s2bI9OWpR9L1m3oee68QKlOdelG56pQ6tXLn2o2rpvf+1zF17eWR4AAARKAQAABoNAKQAAwNQQKIUOCXPueP31OkCZzxll85VDb1bfvPL6MeUmCpSWEGhGJ21OmT/jtiV1EHLBqsfrzwlOZjTRhEFLaDUh0qd27qp2vfHGaD1SLqORZlTSUiajqJYp+b9w0Zz6eCue2zwm/JrRQEvoNJ+PJ1BayvTati2B1t2HD1XfmntD5/q0Wdoubdhe10+gNBJoTTA3AdqEb2feeV9nOQAACIFSAACAwSBQCgAAMDUESqEl4c2ESRMqLcsScNx75Eh1zYMrxpSdKFBagpElOFok+JljlDBlO8hZZPTO5v7PvP6WehTTjEqaz1+97Op6fa9p5Yt2vdqfi8kMlLbbpm0yAqVx++NP1vXJCKx/6fcv6izTy6duXV+d+c6f1aGC8fzVt/5tdca+f1l9a/t/WX1t/X9aff6+V6qPX/Ng9aHfPb9zvwAATE8CpQAAAINBoBQAAGBqCJRCS0Kjmao9096XZZli/plXdlft6eYnCpSWz71MFCjN8ub+Sz0ycmlGJ51119I66Nqu68LVT9TbZdTT5vHa9Sqfi8kMlJ6MEUq/ccV19QiuGZ00I7DOvf/hznK9fGTGvOrz971cfX7p7nH95Qf2V195/N+vfuvZv1f99r7/cTRo+tv7/1X1mTu3Vh/6gWApAMAgECgFAAAYDAKlAAAAU0OgFBrKFPLNEGZTRgfNKKGlfL+B0gc2bqquuv/ho5xzyx11uX4DpZHp68v0+xlFtUx3n3Uf/dGMuv7ZV8p9fc619bY3LF/VWa/yuZjMQOmiNWvrYO7ZCxZ1ri/7ziisvdaNFyjNtVq1eWtd7qLF91bPv7q3HvX19NnzOstPpl87b2512sKN1Tdf+Kd1IOGb2/5J9eHfv6yzLAAA04dAKQAAwGAQKAXgVNcrIwAAU02gFBrKlPIJirbDnzevXF2va04vP1Gg9Pzb7qpHzky4s2zTpdd/FrO8HShNHfe8dbi6ccWj1bb9+8fU54fzF9YjlraDmO16tT8X7Xp0BTt7bdtWzr3XdPxpk9ffe7e6ePG9R63rJ1A688776pFJl254pvrQmeeOfl6+aXP9uWubE+GTNz5Wff/w/1p9d8+/qD589qzOMgAATA8CpQAAAINhqgKl5fcT+T1Ikd91PL7txeq3r7mpc5upcsa8m6qnX3q5rl+pZ2aP+9rsazrLD7Ku61LOOW2QtujajlNTP79HbCuDCrUHDzpeuc9yv5X7L/vNAEOfOt/vCulf+3fzxyq/r29nCQBgMgiUQkNCjr3+05YfNDLVfHNE0IkCpZ+78PLqxQMH6pEzvzxr7ui+SgDysxdcXn/u9Z/FLG//JzBT2mfq/UwnH81tyg9QzVE/c6y71q4fU6+MGprRQ5s/aKXcI89vGVOPsr9mufY59pJ6bhj5IT4/QLVHKf3OVfOrnQcPVs/tebX6jZ9cMmZddB23Ke26dd/+MSOS5vrkB7f9Iz+4zbhtyVHbnEi/PntRdebbf1Z9ffMfV//OSQyzAgBwbARKAQAABsNUB0qfeHHH6IAj+Z1LfleT4NjcpQ91bjeRfn+30q/83iW/f0m9Ur/U8+6nnq6XZVCSDEDStd2g6rouzXPu+l0Up66Jfo/Y5XvXLqjvjdyHx7Jdl3L/5XetmU1x/vJHqzUvbK+fEfn9Z2aQ7NoO2tIX05fTp7vWTyTbvfjaawKlAEw6gVL4hYREExbtFXKM6x5aWY+qOeuupfXniQKlMXfkB9qMnLn9wIHqpkceq+Y9sKL+C7j8UHHNgyvqMr3+s5jl7UBpLFj1eH2cdl3LOWTfmQ4/x9s0UubgSNlmvUq5HHPRmnX1eT27e09dz2Y9un4gO/fWxXVoM+HaK+57cDQU2+W7V99Y7Xrj/Zcsj27ZVh8nP1jlB6z80PbjHv857jpukeBrRiXNOV3/8Mox6xJU3X3oUD39fQn9niyfv++VOpzwqVs/2A+hAACcOAKlAAAAg2GqA6Xt30984tyZ1drtO+sQ2Vk3HntYczIDpWWkxfw+JL8Xaa4rvyfJ+pRrrhtk4/3eqIT7TrVzprfx+kMvGVRoz+HD9e9Nm4MHHasy6E2Co+37r/TFDLiTgXea66BL+nD6cvp01/ou6YP3rt9Yff+6BfXv2hMo/a0519aDWd366JqTOpMnAKcugVL4hYxqmaDkeD98fGvuDfWooBnJM/8Z6ydQGuctvLPasndfdfC9d2v594zbl4z+h67XfxazvCtQWv6KrquuGQl13Y5d9XESEF27Y2cd/EyIszlyaab/qOs0Ut9SLkHVZj26fiD76I9mVMs2bqq3efWtt6ofzP/Z6LouX7hozujIp2mbhEsnmnJlvB8Ez7nljnpdrx/GEpDNuS8e2fZk/of5Q797XnXGq/+P6jsv/7ed6wEAmHoCpQAAAINhugVK47tXz68DaeV3RFmWr/l9T/N3QBn0okzBXvaX348Uzd/7fPgH51VXL1teh9Oav69pznrXloFGMuBI1wApCVRmYI+sy+9nyvL276lSx4SRyvpe+0w9U9/8Pqz5OeHa8ruisi5tceEd99QDrJRzye9yvnHFdaP7i/xuZ/ETT9XBu7THK4ferAdjGe93OuNdl5xzBkEp7Vp+V/ezx9bU55m6lN/bpb2vffCR+pgpkzrk90r53VfZX7tM+b1W+5pMdB6lrTJyZZaX/aUPtc/3RLbdqWi8/tClDLST0PHse5bV1/Tixfd2lp3InHsfrK9RAqpd6+9bv7H+fXMG6Mnn9j1UdJ1DP/0z5bPdgpWr65kc63//4ve7GWyolIvy++TmM2sY5NlW3/u/eN7l2ZdnYLNMP22dNhvv+R7l2ZnluTb5fXx5jvZzX5frmX6ZZ3/K5Dj5XX+v7wNn3nDraL1zvAyGVW838jmznH7q/Fmd2wHAsRAoBZgkpy18ug4o/IVZt3auBwBgagmUAgAADIbpGCgtwcWEuEpYM7PUJZyWkFCCfQl5vfzmm3WZ02fPq8NJX7pkbnXD8lV1+CdfvzjzyjrMlLBRBsdIGCiBxUzhnkBTjp9Z5dph0SL1SDAuobXL7l7WWaYpAbiEjTKqYmaSy+x2CTglQFWmiT/WQGkCTxmAJcuzv6wrM/Yl0NRsi+YU4GmPzGiXc8wIeznn1dterPd3y6rHR4/bNlGAMPVIvZqB0tTlhddeq4+XUWVLe+d6JXiXY5dQ7PJNm0cDd6lH6pN6Na9J2izXLmX6OY/SVimT/pD2KG2fZTmnUv8T2XanomMNlNazL46Uz73wmZ/Orrbt33/cIcv0nb1HjlQ/nN89UvH5t91V97GEPPO530Bpv/0z5Us4Mffriuc2V9+79ubqmVd2j3k2RZl583jDs4MoYdKEaHPv5/wj/057ZQTPlOm3rSd6vqdMeXbmPsz1TCg0oe+Ev/u5r3M9c43yPC7fB1KnbJfPvUZdTh2/eeX1dZA1z7uVm7fWI5d2lQWA4yFQCjBJPnzO7Dqg8JnFmzvXAwAwtQRKAQAABsN0DJRGM7j4l37/ouqJF3fUI8k1Q5iz7lpaB4QSZCrLuma4S/CrBMJKgCky8mFCTAmmlWVtCSPVo5qOHCdhpRyza1a3MjJjQkfNKb7L9gmhZbtjDZQ2A1FRjtPe/szrb6mDUqU95y59qD63hKtKmQQl04btMFzTeNelHDshwYQFS1snfJl9l3JlH3etXT/a3vm6dMMzdQAtozl+/JyLq02799Tt9cnzfjnK3/wVj9YhvkuX3F9/7uc8erVVCRwuWrO2/nyi2+5UdKyB0gT00kYJd+dzRhHNCI8J5LXLTqT5DOha365b+x7qVa6f/pllKZ++2J6psf3cKMHzYesbua8SHi5h+SijSyegmc/9tHW/z/fy7GwG9KPf+7rrepY/Xtj1xhujfbZLAqsJqKaeuc7N5wwAfFACpQCTKNPen77+DzrXAQAwtQRKAQAABkO/gdJf/f1Lqr94yXXVp65ZXH36+rvHddp1Szr30TRRUG2iMFl07aMrUNpLv2UzrfEDGzfV4amUL+HS5nTM7dESm5Y9/exoeKqEotrhp3YYrlc4rtdxEoy6YNHd1Tm33FGHpR7f9mJnwK1M2Z22ay4vel2XjMiXkfly7hlJNMt6tV+2bQbziozemIBYypcQXs6x1wiU/Z5Hr7bKyIYJM5ZzOdFt1/Rr58yqvvCz5dPeX7ig9/0VvfpDlwTy0m4JlZZl2T6jjF7z4C/Duf2a6BlQ6jbRPdM+h376ZynXdb2zXbbPfZ3PX73s6vq4/Y7EOjB948Lx+0auaUb3zGigGQm6q0y/bd2lfd16PTv7ua/zudf1XLJuw7j9LKMuv/rW+9P0J7yaIHxGUu36wwIAOB4CpQCT6Lee/XvVt3f9s851AABMLYFSAACAwdArUPrxy2+ufmfjwer8w39czf2jP63LHYvfenj8GcbaYaG2dpgsgaWrly2vpzFPqDFBxqK5j14hx4R/Fq5+ot5nRqlrbj9eqKkpdTjzhlvrETkTYEr9L7lrab2u13HLuoSnEqL6oIHS8Y5TlGM0z7GpK1RVlOvStV3aPcHaMhppr7qkzu1tm0r5hMwSQMyyBD8zgmym0S6hvH7Po1dbleWlf5zotmv6xqrtnffF8Zr51/9Z5/IP6oz1r3XWv5joPm1KwDCh60x7X5blvsvovAlglwBeu3+0r1uR5bl+5RnQ1q5br37QLtc+flvpHynfdb1zHjmfMlJv7us8DzKiZrNcL99Y+WLntTheM//D/6Zz+Qf12xte76x/8YlzZ9bh6zxPc/4Z6TNt1xytuN+27uf53uvZ2c99Hb2uZ5aP189St0zhX46ZgOmM25f0FR4GgH4IlAJMoq+u/ZvVGXv/n53rAACYWgKlAAAAgyHBoWag9C/OuqH6ycG/PxoqmvU3//vqrJ1/o/rmqu3VV+5eW332pvs7RyVt+qAjlJbAVhklMsGdTJ2d0FKmSv7u1TfW4Z+EORNga+6jK1yUgFNGw8zxMl11pivO9jcsX9VXEKlLpnbONMkl3DReqClTqE9FoDRTwF//8MrqqvsfHuOK+x6sPnvB5Z3bluuSqZ2b22Skv2Z9o1ddUudMNX3zytVj9lFkRMdSNtfmnFvvqMOkCZUmnJZrlWP1ex692qosL/3jRLdd229c+bNp71d+dEFn3Yvx7tOmMuJs2rdL+kOmIE/ZjBjZbNMygmRbRjrN6Ka9RrAtI1Nm6vV87tUP2ufQb/9M+WzXDiBGpmHfP3LshGfb0/z3o+taTDe/8qOJR+DMs/kbV1xXj96ZgG3CoHkuJnif9f20db/P93Jvtp+d/dzX0et6Znn6TY7ZXA4AJ4tAKcAk+tIjR6rvHfxfOtcBADC1BEoBAAAGQzNQetr1d1dX/uG/rp2xbl/1sYuvOar8ZBkvqJbgWUJIZQrpBLUSUspoeM1R4br20RUuSiAtwbT2sSYKIv3kZ3dWuw4erG5pTaVcJCxVgkgna8r7sxcsqkNWCbQ1l6ddPn/RnOrTMy6rw30ZLfBYQ24x3nVp69V+Od9mgLBLgqSpW7MNMhLg3U89XYdKL11yf9/n0autyvJyLie67U5F/faHcs/mGrRDgwkTZl1Cg13b9jLn3gfrvpAQeNf67K+EOvO5Vz9on0M//TNSPttl+/a6b829odp9+FD9jEqQst/p7k8V5Z6J5nmft/DOus0e2/pC/bmftu73+d7r2dnPfZ3Pva5nlqffpP80lwPAySJQCjCJvvjwG9X33vzTznUAAEwtgVIAAIDBUAKln7zq9urqP/631cV//Z9VH5t5bWfZydQrqHbajEurTbv31CGkTC2cZSUollEzm4GjeQ+sqEf+bO6jK+RYjrVk3YbRZdnPXWvXjxso/SuXX1MHSp9/dW9dr+a6z114ebV13/7Raa+zfsu+fUeVzWioGe0y035n5NUcN+eR82mO1NkM5OVzr3BcjvvigQOj+yvLv3PV/Gr3oUOjob2bHnmsHi0wYbxmm50+e14d+moua+p1Xbr0CpRmNNMD775Tj9yYkGhZnnbJqINZlnBtQrbtIF4J5pZ99nMevdqqLC/ncqLb7lTUb39IW3WF9aKEdHN/tO+j8ZR7LPdPrlFzXUKEuV82vPTy6LX85Hmz6vuvHTjMVPTN50Q//TOfU368c8qIrNlP9Dvd/amiOYp0M3idZ2GeieVe7Ket+32+9wqU9ntf97qeWZ7jpx7N5QBwsgiUAkwigVIAgOlLoBQAAGAwJFB6wbv/tLr87/yr2q/9/qzOcpOtBNWaU6tndMo9hw/XgcJME1/KlvBWQn5rXthezV/+aLVux676cwKNJXAUGa0woxYmwFamJ094KWG27DfBpgQGN+15tR79cLxAaaQe2S6BowRSU89bH11TbT9woA5KzR35XMpmRMUsSwguo+XlOCmX4FsCcKXcNQ+uqI+dQFZCUznvlMn5TBQojRwzxynbL1i5uh7hL8G7BFhTJuecYG4Jc6Xet61+sh5VMeeeEFZ7v9FvgDB6BUoz+uijW7aNTl+fOpa2SD2/PfeG+pqu2ry1PueMZpj6lfPI1PffvPL6el/9nEe/gdI4kW13Kuq6T5syXX25v9pBv6bcDwkHHmvw8scjx0/wOCNQpr9kP+lbuSeb16xIeDD9bvW2F+v65fpl2ywr/aCf/plyKd8rUBrlPh7W0Wzbz7u0YwK+uafTNinT77Ogn+d7r0Bp9HNf97qeWZ7nhEApAFNFoBRgEgmUAgBMXwKlAAAAgyGB0tl/8C/rr5+9eVlnmROhBNUSGCryOdMe//Y1Nx1V/hPnzqyWb9pch4YSNMpIhBctvrcOHzZHtkuAadnGTXW5V996q/rB/J/Vy788a+5oSCnr1u7YWQdOEzZrjlzaJfVJvUp9s/3mvXurGbcvOWq0yoxguWXvvvo4pZ7fv27BmDKp46I16+pjZ385h0wLnrL9BEpzzAvvuKcOZSWoVc7na7OvGVMubZa2SFg1x8nXB0Y+f+r83qHhyQiURs4xo1bm3FImAcAER5t1bJfJeSQQdsa8sdd/ovM4lkDpiWy7U1HXfdqUNp9x25I6xD1en2lOEd++ZyaSa5O+kz6UY+aa5do9tfOlOjDeLDvmOTFSJtf52gcfOaof9NM/Uz7n3itQmhFrs/2wTXffNOZ5N9KOCXAm3Nlsj37aup/n+3iB0n7u617XM8vTPwRKAZgqAqUAk0igFABg+hIoBQAAGAwJks77R/97de6Bv9e5HmA6SXjwhuWr6mBiRgf93Rtu7Sx3omU05L1HjgzddPcAwOQSKAWYRAKlAADTl0ApAADAYEigND59wz2d6wGmo7MXLKqefunl6jtXze9cfyIl1Lp0wzNDO909ADB5BEoBJpFAKQDA9CVQCgAAMBjqQOk//rPqQ797Xud6AN73kbPOry5YdHe14rkt9Qipi9as6ywHANAvgVKASSRQCgAwfQmUAgAADIYESq/8I+/aASby8XMurja+/Ep14N13qgc2bqo+dvaFneUAAPolUAowiQRKj83l9zxQvfHz9+qvXeuHxZ1PPlXte/tI9Xs33965vhftN7V+4yeXVM/tebWWf3eV4X2ZaufudRuq5199tTp99rzOMpz6zlt4Zz3d0mV3L+tcz4l3vN9vTiUCpQAAAIMhgdI5/8f/qXMdAAAAJ45AKcAkmqpA6eMvbK+DheMpAZKuEFyWZ32CJu19n0jTJRBZzn//O29X5966uLNMlLZLnSezrQRKJ1+5pqX/F5nu5emXXq7OmHdT53bHQqC0fwmULt+0uXr5zTeqr8+5trMMk6Pr+8HBEWn7hY89UX30RzM6tzsZZt55X30PXr1seef6ydT1DMixH9/2YvXb1xx9/+c5nHbqNR1Ur+dtniV5pmTf5RiPbX2h+trsa8aUa5uqYOdUHXc6ESgFAAAYDAmUXv53/lXnOgAAAE4cgVKASTRVgdJzbrmjuur+h0c9/+reau+Rt6qbV64eXXbFfQ9Wn73gcoHSDs3g0SPPb6nDb13lEjZN6DTlJrOtjjfgM13abzoq1/SJF3eMuTfufurp+t6Isxcs6ty2S9d907UM7TLVEihtP/+ve2hl9ezuPXVg8tEt26Y0VHqydD0DlqzbUAdrE/qcu/ShMeXzHM7zNG131o0Lx6yLrudtniEpn31m381nzJ63Dlc/nH/0foqpCnZO1XGnE4FSAACAwZBA6ew/+Jed6wAAADhxBEoBJtF0mfI+gaIEXL4y66qj1nWFvUrwZjJDkv2YLoHIcv6vv/duPR3zly6Ze1SZhEwTNj04UiahrMlsq+MN+EyX9puOxuvTJQT25PYd1UfOOv+o9V267puuZWiXqdbr+Z8QacKkE43EfKro9Qz4xLkzq7Xbdx4VHE25PE9jw0svVx87+8Ix27Wft3l25Bmy+9Ch6jtXzR9TNp+zfLxnzPE+9z+oqTrudCJQCgAAMBgSKL3sD/6HznUAAACcOAKlAJNoGAKlmd5348uvVAfefacOVm4/cKC68I57jhrVM6GdZRs31aGdhHDydfETT40J6XQFIkvYL1MGp2zOIeeScyplol3n7COB0OsfXlWt3bHz/fqNfF63Y1f15VlHB0Sbyr5yzGx3zYMrjirzzSuvr1459GYdNMrXdludt/DOasvefe8HTkdklNjvX7dgTJlIXVKnlMmxUtcVz20ZE/DpFfhpX9eu9vvwD86rrn3wkbqOpd0zhfMwjEjYNF6fTsDrqZ276rY84+ob63shQeIvXDRntEz6Xvp5lq/f9VLdlk3Zb/Nemn3PsvpeyD2xf+S4XW3ez71Trn32d6z9eCKlvumbGb2y2UcyFXrOOfUu92zWz3tgxZj6pUzu41ImX3Of537P+vTR0kZFuQZdz6Wu/d27fmO9POvL/b9q89Yxdct1+entS+oy5bzGu4ZdIfFT1XjP//Nvu6senbNck1zbGSPt2H52pa82t5voukf72VOmfm/22+YzqwQyu65Pnom7Dx+qvjX3hvpz+/hdfbNtvGfAd6+eX+05fHjMiNQpl3t3w66X63a4/uGVY7ZpP2+7vpcWObf02axr9smmcq+3n/Nd+v3+cizPmOZx597/cH297lq7vr6O7XPttW0+ZyTWjPaaOqVuOfbyTZvH9I342uxrfvlMG9n3zoMH6+OOdw1PFIFSAACAwSBQCgAAMDUESgEm0akeKM1obgn07HrjjWrBytX1NMqbRvaTgEiCIaVc9pvplRNQWfb0s/U0wAkIpVxzuuV2aOXrc66tQyabRrY9bcal9bISKMs5lf1Hu85lXzlGQkzNY27dt7/63IWXj9m+qewrYdQt+/ZVz7yyezTQVuRcM7Jf9pv6NNtqzr0Pjh4n5W565LE6yJO2ak6rnnPKuZV2SSAqgaqEa9ohnebnon1d2+2XYM7ikW2z/5x76prQV+qWgM9UBHemykR9utmWCVMmjHzx4ntH1yfIlkBb2vHTMy6rRxxMoCvy7/Txci8lhJbRCBetWTt6T2R/zSm1+713Ut9sm7LtfpzPvUY77Eepb0Jfu0bus9Sj9NUcM+2Rf2dZWd7sh+k/6UepS/pVs3+VER17tVW2b/ffMmJm1/7Kc6Lc/6lz7s20222rn6yvTe7HGbe9Hyqd6BqWZcNgvOd/+74oQcJcvzyP0idefvPNOuR5+ux5dZl+rnvK3bLq8fo6rd72Yl0mAeAcK/3oizOvrMu0n1nl2dm8bp/56exq2/79deg7/b30k+wrYePsO8fIsXLMsl3beM+AEipvhpDLc/eikbokHNkeebRd9xKITT+87O5lo+X6VY7Xfs639fv95VieMc3jlj/iGO97c69t87mE6Nvf0xKCL/vL9U+d07dSt5RL8DXlxruGJ4pAKQAAwGAQKAUAAJgaAqUAk+hUDpSW8EzCKt+44rrR5SUk2RxlLiN8JsSS0EgpV8KOWT7rrqX1smZopYRJI/8u25VAWc6pLIt2ncu+MsJaMzhZQk5do44WZV8JBCaY1gzMRAJTCZnGt+feUNenHDfnn6BbAkglBBvlfJrh1ASBUpfmyHep6wMbNx0V0mnXIdrXtR36KefRbIN8XbrhmXoUue9de/SIdk2/+pMrql/5vYuntV89p3cwuKm0RVefLtcsobWE1868/pY6UJVAVClT+nAJunXdN2VZO9hVgowJgObzsdw7qW9CVrlXyjUs4bds/0FG2iz1bdejjNbYXl5Gs8x9kc85dso88eKOMf0r/XfvkSPVD+e/P314V1tFu/9m2vWE8drXqHkPlvu/fX9ldMb06bRr2qefazierr423XTVu8t4z/+0SfpXwoZ/6fcvqq9lgn/N65Tnc8K5KZPP/Vz3j59zcd2Xc50+ed6s0X3NX/FofbxLl9xff24/s7562dV1XZuh37pfjFz/8sxOMDv9sPn9JEHF1LsZCG0b7xkQ7XZqPnfPueWO+t+Pb3tx3JBlec7nuZ6AZNqu/ccIvTSP17U++v3+cqzPmHLcEiZNmeb+u861vW35nL7S/l6fAHLu7VzLLMv1Tz9ohuw/P3LdXnjttfrY6T9l+ckgUAoAADAYBEoBAACmhkApwCT61vb/S/XdPf/3OqgwWT5/3yudxxrPeIGirrDXRMGbGG+0vwSPyuiAJfzWFfRp76OEVq596JF6RNMEZJojwkUJlOWcmsvbde4VgMkoe5keuYT7ujT3Vaa2v2/9xtH1JfiW80x4KiGqctwSulvQMcpawm0lyJmQTQJZOZeEqJrlsq92SKf5uWhf1/Y5Z7uu4GiuS65Pu22afvXcOZ3974P69kv/Tefy4/X9Q//f6s/98ILOc2jq1aczUu3KzVvrAFhGUMyyBLISBisB01yrBMmaIayu+6ZrWSRQlwBY6bP93jv53OvaL1m3oec93a9e9S33WDMw2Fxe2rCE2yLTV5dybb2O0+6/zfujlInPXnB5dcV9D9b3SalD+/4v91MCdLlG/VzDXr6wbG9nX/ugvrv7n3cuP16fvav3M6yp3c6Racxn3nnfyPKxo492ad87/Vz3EmjMcUuwuEv7mZXrlGnny3XLshw3z+A8i5vXsf39JM/crnul6PUMKNrt1Lz3ctwE8ZthyXbdi0+dP6sO1yYEm/UlXJrp55vl2prH61of/X5/OZ5nTO6xtGtGMc0919ym17m269zrHBJUTXuUMHr5Hnr/SJ37Ddw2feeV/u6l773xr+uyX9/8x9WXH32vOu22Z6qPXnD9UfsTKAUAABgMAqUAAABTQ6AUYBKdvv4P6pDC1zf/w0nxW5v+qPrzF9/ceazxdAWKiq6w10TBmxivTFmX0EuvMFmUdQnbJFRSQivb9r9Wj17WHA2u6BUoa9enVwCmfczmuqK5r3YQrQSeSsCp3+OWdSXIM167ZF/ZZ/bd9bloX9f2sbM+n3vpqmPTp+94vvrMnVuntU/f/ssRKMdTrlNXOyTwlQBYs6+l7ya4lQBXgmu5/rnuuf5Z33X9el3Tsrz02XafaSrrSmCs17XP8onu6eY5dh2rV3173WNleXNfF95xTx1iy/2aAHiC13/l8rEhw17Hafff9ucuveoWqVczkDrRNezl12cv6uxr002/3wvSVs2+0JQAbqZGL2UTNL162fJ6OvLcF82yx3rdcx3S5tk2z8sVz22uR5Jttn/7mRUZ1TN9Ptctz+iMullGnu3q201d90ox3n0X7f6Xcs39lWnac04J4HbVvSlteeYNt9ZTx6cfZl+X/GI07i7t43UZ75hZVr6/jHeuZV3zGZP6lWvVHtU7eh23Xede51Du2xJwzbP27qeervtYRg1OiDUjdX/i3JljtuvlN29eU31+6e4JfemRt6rTn/rb1Te3/ZMxQdOvPf13q1+/7LbR/QmUAgAADAaBUgAAgKkhUAowiU7lKe/HK1NGI5soUFpGbmwHShNQWrBydR1yaU4HH70CZe369ArAlPr0GyjN5wScEnrJlMsJlTaDaf0eNzK978kOlGb64ptH2vKq+x8+Sntk1FNZuU4ZxbLZBhcsuvuo9o8ywl+Ccrle6YvpB2V91/XrdU3L8tJn232mqXnv5HOva5/lve7pBO9yXs3zbI/6Gb3q2+seK8vb9c59lLYp/S0hsYyWmHpkfa/jtPtv+3OXXnWL1KsZKJ3oGg6Lcl2az4GMRvntkfZJ6LGUy/MsbZV2ymic3736xrq9E4JMnzzW6x4JDp5z6x11mDSh0gRQEw4t/aDreVmesalLrmWuaZnuvvSlfI/I94ZmHy/n1R5dsxjvvsu55HtCjltGPk259r2XUV3zvWDV5q3187zXs77tu1fPr8O77Xugqet4bV3tVTS/vxzrMyb7zFTzGfk462bctmTMNr2O265zr3Mo9217xNRc65seeawOlKZd04/Sxs0yk+ljc+6sQ6bfe+NP6wDpabe9P/K4QCkAAMBgECgFAACYGgKlAJPoVA6U9julbsJFxzrlfYIxCTct37S5Dpg0R9DrFShr17lXAOZYp7zP5zLFc87j+odX1YGbTNnbVfZYp7wvU3Q3y7VDOe3PRfu6ts85x0v7nXn9LWO2G0bt6zSR0m9z3RMeS/9tXqeu+6ZrWXN56bPHMx11+9pnea97ul+96tvrHivLSxsmhJdlzWB2/p3t9h45MjrVea/jtPtvwmxpl7RPKRMJJabty/G66tZ1P010DYdFu517Sduk/TIic9qzLG/fO/1c93LNmtc74dV6VMqRZ9SlS+6vl3U9p3PsBPbTX259dM3oaNBZ1/x+cqzXcrxnQJ6ReVY2R7Dtuvdy/PSlEipt1v0nP7uz2nXwYHVLx7M/JroOXcdrO1FT3ud7WoKmuY4bXnq5bt98ryzbdF2nsm2zzr3OoT3l/adnXFaP+NoMNJfQbYKtHz/n4tHlJ8Kv/HhW9VvP/mEdIv3sku0CpQAAAANCoBQAAGBqCJQCTKJTOVCaYE1GmksA5BtXXDe6POHLBEKagZ+MLpcAzrwH3h9lLhLaWTyy/ywvowa2QysJtGQ/2V/2m2VlVNN2OC37SEim1Lnsqz19b0a1y0h6ZcS7Ll3nv/CxJ6r977xdB3YyBXMJU7XLlvBp6ljqHF+fc209ql5z217tklBTM5STgG1CWHPufXC0XPaRfTWva7v9Mkpl9p9QUTO4k3pl1MHmslNdP326LdcnfSpt2J4qveu+6VrWXF5CkMdy76S+zb5QZHk/IcHx9Kpv9pl9l/q2l5c2TCAt7ZN7o1kugbVmnXsdp/1cKv21vb/csyXEXerQvr++c9X8avehQ6NTo5fl413DYTFRkLEobZtgbrOd8nxqPlv7ue5lZNF2m5dAZHlGtZ9ZRY6RcOqew4ePCrhmRMs8w3P85vJ8vzhv4Z09r3GvZ0C579p/vNDr3ivP8tS7WfdM+Z9AabtvxucuvLzaum9/tW3//uozP509Zl3R63hN2W8/318+yDMmbZC2yFT9CQZnWQmENtsu7dz+XpX16Rvt72kZ8TbfP8sfYuR7UvuPHVLnhIXbz4kT6fR1f7sOkn7i+hUCpQAAAANAoBQAAGBqCJQCTKJBD5S2pwcvzrnljrpcCZ4ktJIp6hMoKlPXzh0p1zzGs7v31EGijKKWfSRQknLN0EpXuCghypRrBkMzFXIClqu3vTi6r4RdsqwEXsq+sm3Wp1yOnTo0A6pdyvk3wzMZIS8j5WWfOc/xypY6J0CUsglAbT9woG6rBHNKuRLuKe2SEE5CQNm2GdIpx375zTfrds7+XnjttTq407yu7fZLu6Z9yzTT2X+pS65ZprwudTnVdV2niZR2Tzu3p0ovga1c0589tqa+J7rupSjLmwHNfu+d1LfZF5rL+wkJjqdXfUuwcKJAabZp39cZgTDnldBbuce62irL28+l0l/TBss2bhpzz5bnRKlD+nnaK+2W9ks7tu+vGO8aDot+A6XlOiWsuWZkm/nLH63W7dhVf057H8t1z74ygme2zWjQKVOuU3PE0a5nfmQ064Qe20H6KM/N5rP9ttVP1iNypk8kvNksX5RnQPP7WkZMTWg155LgfrN8r3svSsi2XffsI/tKe2fE3Rwjo6zmmdu+t9tyvGybr6V+TV+97Oq6XL/fXz7IM2bRmnV1uRIMTZvn2qZc1mVf6QMp09w2+0qbZNm9I9+nU+9c//SD5vf6dt2a5fL9vdTjRMtIpb+9/3+sCZQCAABMfwKlAAAAU0OgFGASDXqgNMGQLs2g2Rnzbqo2vvxKHSxJ+CfBlgvvuOeoUeI+ce7MOiSWEEn2ka+Ln3hqdLTO6AoXlWBS9j9ryfuBsOwr0+E3j3ntg4/U55hAS3NfGcVu7Y6d75d97906IPXlWeNPlVzOv+wrcj4ZKa8ZhupVNjJS3pa9++pjRkaU+/51C8aUidSlhLZSx9Q10xk3Qzrx09uX1AGrnFNCRw9ver4e9a15XbvaLwGetEHqXbZNcOdrs68ZLTMMel2n8eSaJ3zWHM2vKaGock3SR7vupSjL2wHNfu6d1LfdF8ryXvd0v3rVt4Q22/Uty5tt2L6vS/9q32PttsqyrudSngd5LvR6TpQ6ZCTDZrnsO/dI2U8x0TUcBuM9/9vGPFt/8dy6aPG99fOjOXJpP9e9/ezJPtPf0+9Lma5nVpGwaOpdgpRNXd9PHhj5/KnzZx1VtijPgJQv8jnP9d++5pd1Knrde5Fzy3O7q+7ZV/ZZjpXz3rx3bzVjpH827+22HK9Zt7bmcfr9/nK8z5g8DxIYzSizP5z//qit2Vd9zJH9ZH9d36vKvhJGTp3K97X0qVyzsv+yv1K3nF/6SfpLCZ2eLL958xN1mFSgFAAAYPoTKAUAAJgaAqUAk2i6BEqH0XhBJehHGZVvWKdKn456hV17cQ3h5BkvhDudfXf3PxcoBQAAGAACpQAAAFNDoBRgEgmUTh2BUj6oGbctqfYeOTK0U6VPR8caKHUN4eQZ1EDpl1YeESgFAAAYAAKlAAAAU0OgFGASCZROHYFSjtf3rl1Q3bb6yWr34UP1tM/N6eCZWv0GSl1DOPkGNVD6m7esrQOl33zhn3SuBwAAYHoQKAUAAJgaAqUAk0igdOoIlHK8EkQ8+N671fOv7q2+ccV1nWWYGv0GSl1DOPkGNVD6sTmL60Dpt3f9153rAQAAmB4ESgEAAKaGQCnAJBIoBQCYvj560fw6UPqdl/6bzvUAAABMDwKlAAAAU0OgFGASCZQCAExfHz5n9vuB0lf+u871AAAATA8CpQAAAFNDoBRgEgmUAgBMX79y9iV1oPS7u/9553oAAACmB4FSAACAqSFQCjCJBEoBAKYvgVIAAIDBIFAKAAAwNQRKASZRHSh9Q6AUAGA6EigFAAAYDAKlAAAAU0OgFGASfeWxn1dn7PuXnesAAJhaAqUAAACDQaAUAABgagiUAkyi09f/QfXtXf915zoAAKaWQCkAAMBgECgFAACYGgKlAJPoW9v/y+obW/5R5zoAAKaWQCkAAMBgECgFAACYGgKlAJPkQ797fnXm239WffnRdzvXAwAwtQRKAQAABoNAKQAAwNQQKAU+kA+deW510yOPVXsOH67e+Pl71bKNmzrLDYOPXb64Dih88sbVnesBAJhaAqUAAACDQaAUAABgagiUwhS788mnqn1vH6l+7+bb689fmXVV9fKbb9ThzKa9R96qHti4qfrU+bPGbP/4C9vr9U9u31F95Kzzx6xrmnPvg9XBkXLZd47RVeb71y2o9rx1uNq6b3/1uQsvH7MuwdGlG56p93H9wytHl8+8877qwLvvVJv2vFpd99DK6nvXLhiz3TDJyKTfP/S/Vr/yo4s71wMAMLUESgEAAAaDQCkAAMDUECiFKdYrUPr8q3urq+5/uDbvgRXVmhe2Vwffe7fa8NLL1cfOvnB0+xIoTeD0zOtvGV3elKBpAqcpN16gNBatWVcf55ZVj49ZftaNC+tjtI+/4rkt1e7Dh6pvzb1hTPlh8+cvvqn6q2//79WXHjnSuR4AgKknUAoAADAYBEoBAACmhkApTLFegdIERZvlMkJoRih97Z23q/Nvu2t0ecolAJqRQxc+9sSYbYoETRMGfX2k3ESB0tNmXFpt2r2n2n3oUPWdq+bXyz76oxnVuh276n0kWNosn+NPtM9T3Yd+cH717V3/rDrznT+rPjLjms4yAABMPYFSAACAwSBQCgAAMDUESmGK9RsojcvveaAeZTRfy7KU27b/tWrLvn21BEKb28R96zfWYdC1O3b2Ff4855Y76jqt2ry1Ht107tKH6jDqXWvX18HWlEl9Uyb1KZr7Tgg1AddXDr1Zr0sQdvmmzWOm7C/nunb7zmrZxk311PnlvHOcC++4p9p+4EAdls26jI76jSuuG90+vjb7mvq8sj7B2ozsesa8m8aUOZH+3A8vqE5/6m/XwYTP3vVCZxkAAKYHgVIAAIDBIFAKAAAwNQRKYYodS6A0Ac0EOy9efO/ospR7bs+r1YKVq+tQ5ay7lo7Z5jM/nV1t27+/nvL+7nUb+gqUJsy5dMMz1f533q6uXPpQtXXf/trnLrx8tEwCo1+6ZG49cmlCoz+cv7D64swrqw//4Lw6hJowagKej219oZ62/971G+vzzOinJfRazjXlMm1+zuWmRx6r180d2Sbns/HlV+op/3N+L7/5ZrXz4MHq63Ourcvkaz4ndJrtrntoZfXCa69Ve946XH3/ugWjdT0hRtro4/OWVd/e+V/VI5N+5s6t3eUAAJg2BEoBAAAGg0ApAADA1BAohSnWT6A0Ic1Ll9xfjzL67O491W/85JLRdSm364036kDnjtdfrx7f9uLoKKKRgGlGB83XHKufQGkkHJqgZgKsCXbOvPO+znI5fnuf5966uA6jNkc0jRISLVPzl3NthkQjgdOMtpqgbPNcy9T9OY+EVhNWzbk3Ry3NNP2Zrn/Fc1tGl31QH/rBjOrXzr+6+vXZd1SnLXy6+srqv1adsfd/qMMIv/P6/1x9/JoHO7cDAGB6ESgFAAAYDAKlAAAAU0OgFKZYr0Bpcyr5ItO5f/6iOWO2bwY6M7V9Rgv95pXX1+sSuszIpBmhNCOVJoDZb6A0bn/8yfq4CbH+pd+/qLNMV6B02dPP1qOEfu/asaOEltFSM+rox86+cPRcm+HZOP+2u+oQ7IJVj49ZnvO5YNHd9ZT8X73s6nrbdnA0+83+22HULp+6dX09umhCBcfirx75t9U3X/in1adv31RPed+1bwAAph+BUgAAgMEgUAoAADA1BEphivUKlCY8mqnii9tWP1lPC59RQ5ujeTYDndlH9lVGAC0jeiZo2i5btu8lo35m9M+MKJpwZ0YX7SrXtc/xjpN1GUn1CxfNGT3XLGuWufyeB+oga742lzeVc20Gbpv6CZR+ZMa86vP3vVx9funucX3u7p3Vp362ofrEdcurv3DJrdWHfvf8zv0BADC9CZQCAAAMBoFSAHrJDJl3r9tQPf/qq9Xps+d1lgEAjp9AKUyxfqa8L2bctqSeSr4ERKMZ3szonM+8srueLj7TxidYmkBpgqXtsmX7LhkJdNXmrXW9Llp8bx1uTQi06z/kXfvsdZz85/6JF3dMaqA0+2sGb4uMZJrz6NoWAIDhJFAKAAAwGARKAU6eT5w7s1q2cVM9uFF+R3vwvXerLXv3VTNuX1L/frdrm6mUOi3ftLn+PXNzIKbjld9VNwcuahvvd9YAcCoSKIUpdiyB0q517fDmNQ+uqEOns+9ZVgdLM+V9CVa2y/Yy88776pFJl254pv4Pefmc/5i3f2jo2ucHnfL+7AWLqv0jbXLdQyvHLM+xM+X/p2dcVn1r7g31DzWPPL9lWv4gAwDA9CNQCgAAMBgESgFOjgxStGn3nnrGyke3bKvmPbCiuumRx6oXXnutDpbe/vjazu2mm8xcmRks+5nFsi2/q84gTTevXN05kNFXL7u6c7sTrZ9BmADgRBAohSl2PCOUJrBZlrUDnV+6ZG49Augrh96s/+M/666lPct2+dyFl1db9+0fMyJpAqmPbX2hPnbq0Czftc9zb11cl71r7foxYc9Mm59gapmSv9e5pg4vHjhQj7aa4GlZ/p2r5le7Dx2qR2jN8vW7XqqDqz+cv3DM9ufcekc9ZX9zGQAACJQCAAAMBoFSgJMjgxUlOHr9w2MH+vnoj2ZU63bsqn/n/M0rrx+zbjr6oIHSiX6HPhUESgGYKgKlMMV6BUozzXzzL5/ufurp+i+jEqD8/nW/HPmz6z+4CVzmP5cZDTSjgo5Xtinhz4xKenBk2/YPDSXMmXrlL9XK8q59linz88NHgqip/70jdcp55i/cyvblXNuB0ijh04xmmr+EW7BydbXrjTeqnQcPjk5dkJFM0yYZqfS21U/Wx1nx3JY6SLtk3Ybqwz8476j9AgAwvARKAQAABoNAKcDJ0f5dddPcpQ/Vv8u9YNHdo8u+Nvuaau2OnfXvcfO74Pzu+Ix5N43ZLgMDLX7iqfr3uPmddb7md8VlIKFe4c/2747L57Xbd9ZT8ueYZV3zd9T5d47TlPOac++D9e+9F6x6fPQYcfHie6vXR+peBkFq7qtZrsjvvjMraAZkyuBOzXX53XR+V53ZNfO5fe4J5OZ33WUQpnJOa0aOmeVZn3J7Dh8eLZdrkWvSPJ9m/braN+3ziXNnjtYLAD4IgVKYYr0Cpc3/IEZCkk+/9PJR/yHv+g9u+U9mgqUTlW0655Y76u02jByn/Ie+adGadfUPBotH6lz+09trn/mrtfwnvPwnOPXPlPmfOn/WaJlyrtlHc9vI/i+8455q+4ED9X/08wNCfjjJDynNcmmPhE7rH1pGyiVwevWy5cKkAAAcRaAUAABgMAiUApwc9QilP3+vevjZ5+vf73aVKTLoT34Xm9/fZlr86x5aWU+N3xwQKfvI1Pn53W1CjhkQqIRBszzrjzVQmt9PJ7SZ5Tlu1jV/R/3pGZfVgyNt2buvln9nv2Vmz/wuufm778wGmjp/79r369zr991NCafmHBJGLcsysFMGeHpq5646dFrOPb9vT4A2575624t1/W/5Rai1nFPKpG4ZVCnnlDYtmYHsJ3W/Yfmq+vfs+frFmVfWv//O79DzO/eu9u31O34AOFYCpQAAAAwFgVIAAIDBIFAKcHKUEGSCi5ktMqN5tkfhjAQmMzNlynzjiutGl5dZLjNSZz6fe+viav87b9eDKpUykYGISmDyWAOlzRksi3YItGufCV8+8vzYEUQzk+aWffuqZsi0n0DpVy+7ui5TzjPqcx05p4Ry8zkjumaQpYw0Wsqkfdft2FWHR79w0Zye53T+bXfV2y5as3Z0WdeU97k2uQZPvLhjdACofH1g46Zq75Ej1Q/nLxwtCwDHS6AUAACAoSBQCgAAMBgESgF+6VfPnlmddv3d1ddXbK2+98yb1Vk7/0b1k4N/vzr/0D8c13lv/oN6u659NmXky8wAmaBiAowlXDr3/odHQ4tdgcpIKDPhzBLkbI/+WXz2gsurK+57sN7PsQZKy+emfgKlUaa3z2iq+Vxm+izT3Uf2Vc67rQRjSzg1I5JmZNIsy7rM1vnNK6+v1z++7cXR4GjZdySkW8K0vc7p9Nnz6n01g7hdgdISiI32zJ4AMFkESgEAABgKAqUAAACDQaAU4JzqN+fdUf14z39SXfOP/6x+LsZV/+DfVJf+rX9R/fSd/7wzRNrUb6C0KSNgLlqzrg49Zir8jHyZ0UlLELMduCxKkLMd9OxyMgOlCXcm5FmmpU+4c++Rt6ozr79lzL6y7OaVq+sp5JuawdhZdy2t2yCjiSZI+8wru6snt++o91uO39U2MVGgtCyfKFAaF95xTx3azfXJSKf3rd9Y/ZXLhUsBmDwCpQAAAAwFgVIAAIDBIFAKDLMP/e5Pqu89e6h+Fl79D/9/1Q+3/wfVl5c8UX1s5i+nmj/REphM6LEEIUugNFOtt0OXccGiu+tgZTvo2eVkBkojo6Zm9M+EQzOaaoKgZbr76KfOkbBtwqkJcGZfCXWW6e7L8RPwvP7hlUe1T0ZnzSitvc6pLO8nUBqpfwKu2U/CsAffe7c+z1yDdlkAOFYCpQAAAAwFgVIAAIDBIFAKDKuESc/Z/5/Vz8Hf2fhGPd19V7nJkIBjwpVrt++sPn7OxUetbwYavzX3hmr34UP1tO9lGvwuS9ZtqMulfHP5R380ow5kJgh5sgOlGVF0/ztvV/eu31jXrTndfbT31UuZ9j7HuPXRNaPT3WddgpwZBTWB05xne9ui1zmV5RMFStN+KdsMxObf2d/eI0eqH85fOLocAI6XQCkAAABDQaAUAABgMAiUAsPq+88dqZ+Bpy97pnP9ZEoI8rGtL9Qjj55zyx1j1iU8uXTDM9Vr77w9OsX7+l0v1aNytkOL59x6R/WNK94fPTUjlR54952jQpsZtXP/yHHOvXVxve+MdJoA5Vcvu3q0TKahz2ibJWzZK3wZ7RDoeIHS02ZcWm3Zt6+uV+rfnO4+2vsaz8WL762Dm3sOH64e3/bimHDtTY88Vo8UmnNvLj999rzqvIV31st6nVNZPlGgNMd//RfHKMviuodWjo4m21wOAMdDoBQAAIChIFAKAAAwGARKgWH02RuX1s+/s3b+jc71J8LX51xbbT9woA6OPrplWzXvgRV1OHHdjl11ODLLMrpoyp69YFEd+Mwon7etfrKeyn3Fc1vqbTMy6Yd/cF5dNtskvLls46a6TKZiL/sv+8pU8Qd//l6VKehzzLufenp06vbjCZQmHPvk9h31Pn722JqjArKZpj7hzPZ095F9ZbubV64eM019kentS9kvXDSnHoU0dZ9z74Nj9pPg6qbde+pzT7tk27RT2mvTnlerz114bFPeJ3ybkVUz8mmZMj9h2WdHjpH2TLvmGIvWrK3rn9Bs6tDcLwAcD4FSAAAAhoJAKQAAwGAQKAWG0XmH/mF15R/+6xM6zX2XT50/q1r85FN1oDFByci/F65+4qjw5RnzbqoSAk1oMuV2HjxYXb1seR0mLWWyzeInnqpDjglx5ms+N/eVYOmiNevqUUtTJtPHJ9D5/Kt7jytQGmfduLAOe2Z/qzZvHVM+Ydgcqz2yZ2Rf2aaXZsgzEhbNsZujqxafOHdmHaRtnvsDI5/Txll/LIHStFH2lbZ+9a23qh/M/1m9vH2MhEsz0uyXZ/Weah8AjoVAKQAAAENBoBQAAGAwCJQCw+aj582un33fWr2rcz0fTEZE7ZruHgA4mkApAAAAQ0GgFAAAYDAIlALD5it3r62ffb9+0bzO9Ry/jPSZKfy7prsHAI4mUAoAAMBQECgFAAAYDAKlwLD54Y6/Xl3xn/3Pnes4PgmPXn7PA9WT23fU08LPuffBznIAwFgCpQAAAAwFgVIAAIDBIFAKDJsLf/5/rs479A8713F8vjjzymrnwYPV/rePVAsfe6L68A/O6ywHAIwlUAoAAMBQECgFAAAYDAKlwLC59G/9i+pHL/3HnesAAE4mgVIAAACGgkApAADAYBAoBYbNlX/4p9X3Nx3uXAcAcDIJlAIAADAUBEoBAAAGg0ApMGyu+gf/pvrt9Qc61wEAnEwCpQAAAAwFgVIAAIDBIFAKDBuBUgBguhAoBQAAYCgIlAIAAAwGgVJg2AiUAgDThUApAAAAQ0GgFAAAYDAIlALDRqAUAJguBEoBAAAYCgKlAAAAg0GgFBg2AqUAwHQhUAoAAMBQECgFAAAYDAKlwLARKAUApguBUgAAAIaCQCkAAMBgECgFho1AKQAwXQiUAgAAMBQESgEAAAaDQCkwbARKAYDpQqAUAACAoSBQCgAAMBgESoFhI1AKAEwXAqUAAAAMBYFSAACAwSBQCgwbgVIAYLoQKAUAAGAoCJQCAAAMBoFSYNgIlAIA04VAKQAAAENBoBQAAGAwCJQCw0agFACYLgRKAQAAGAoCpQAAAINBoBQYNgKlAMB0IVAKAADAUBAoBQAAGAwCpcCwESgFAKYLgVIAAACGgkApAADAYBAoBYaNQCkAMF0IlAIAADAUBEoBAAAGg0ApMGwESgGA6UKgFAAAgKEgUAoAADAYBEqBYSNQCgBMFwKlAAAADAWBUgAAgMEgUAoMG4FSAGC6ECgFAABgKAiUAgAADAaBUmDYCJQCANOFQCkAAABDQaAUAABgMAiUAsNGoBQAmC4ESgEAABgKAqUAAACDQaAUGDYCpQDAdCFQCgAAwFAQKAUAABgMAqXAsBEoBQCmC4FSAAAAhoJAKQAAwGAQKAWGjUApADBdCJQCAAAwFARKAQAABoNAKTBsBEoBgOlCoJQP7PEXtlcvv/lG9ZVZV9Wfz1t4Z7Xj9dery+5edlTZQfR7N99e7Xv7SHXnk091rgcAAAaDQCkAAMBgECgFho1AKQAwXQiUMq6PnHV+9eT2HdXeI29VZ15/S2eZdqB05p33Va+983Z19bLlR5UdRJMRKP3CRXOqR57fUu/njZ+/V7fPY1tfqL42+5rRMuU4WT+etPf3r1tQ7XnrcLV13/7qcxdePuZYHzrz3GrphmeqgyNlr3945Zh1AAAwzARKAQAABoNAKTBsBEoBgOlCoJRxfe/a94OLCTL2ClS2A6Wnmq5A6eX3PFC3Sb42y3b57tU3VrveeKMOkT66ZVt13UMrq1Wbt1b7R/aZtv3xyP5T7rMXXF5dcd+D1VX3P1zL8bLNEy/uGF0W59xyR11+0Zp11cH33q1uWfX4mOOddePCOgC84aWXq4+dfeGYdQAAMMwESgEAAAaDQCkwbARKAYDpQqCUcS187Ilqz+HD1aY9r1Zb9u2rTptx6VFlBEqP3qZIoDPBzgQ8z16waMy671w1v9p58GD13Ejb/sZPLhmzLiYaGTXXYtPuPdXuQ4fqfWXZR380o1q3Y1d9vARL29sAAMAwEygFAAAYDAKlwLARKAUApguBUnpKYDEh0kx5P/ueZfVomRcvvveocu1Aaa+wZaZpf/7VvfWomgfefadau2Nn9eVZc0fXJziZAGWOlXUpk7IJSJZy5966uNo/Uo8EXct2UUZSXfb0s/XnD//gvHrK/QQ2M/V71/F61bPUI4HOfG4GO8u/s10xXpi21Pe+9Rs71+c8Xh85x652nShQGhmtNGUy4ulHzjq/mrv0oXp/d61dX09937UNAAAMK4FSAACAwSBQCgwbgVIAYLoQKKWnOgz59pFqzr0PVp/56exq2/791SPPbzkqqNhPoDSjc2bUzIx0minfF6xcPbLNm9X2AweqL868si6T4GTCkCn32NYX6undVzy3pQ6D5nMCkxnJMyN6bnz5lTHTuWefCbyef9tddf0W/2JfZT+ZHj7By2d37xkdDfR4AqUZAfRLl8ytbli+qt42X1P/BFib+ygWrVlbt2F7dNKi7HvJug09140XKM25Lt3wTB1avXLpQ9XWfftrn7vw8s7yAAAwzARKAQAABoNAKTBsBEoBgOlCoJSeEubc8frrdYAynzPK5iuH3qy+eeX1Y8pNFCgtIdCMTtqcMn/GbUvqIOSCVY/XnxOczGiiCYOW0GpCpE/t3FXteuON0XqkXEYjzaikpUxGUS1T8n/hojn18VY8t3lM+DWjgZbQaT4fT6C0lOm1bVsCrbsPH6q+NfeGzvVps7Rd2rC9rp9AaSTQmmBuArQJ3868877OcgAAMOwESgEAAAaDQCkwbARKAYDpQqCUTglvJkyaUGlZloDj3iNHqmseXDGm7ESB0hKMLMHRIsHPHKOEKdtBziKjdzb3f+b1t9SjmGZU0nz+6mVX1+t7TStftOvV/lxMZqC03TZtkxEojdsff7KuT0Zg/Uu/f1FnmV4+MmNe9fn7Xq4+v3T3uD53987qUz/bUH3iuuXVX7jk1upDv3t+5/4AAGC6EigFAAAYDAKlwLARKAUApguBUjolNJqp2jPtfVmWKeafeWV31Z5ufqJAafncy0SB0ixv7r/UIyOXZnTSWXctrYOu7bouXP1EvV1GPW0er12v8rmYzEDpyRih9BtXXFeP4JrRSTMC69z7H+4s18unbl1fnfnOn9W/WD8Wf/Wtf1t984V/Wn369k3Vn/vhBZ37BgCA6USgFAAAYDAIlALDRqAUAJguBEo5SplCvhnCbMrooBkltJTvN1D6wMZN1VX3P3yUc265oy7Xb6A0Mn19mX4/o6iW6e6z7qM/mlHXP/tKua/Pubbe9oblqzrrVT4XkxkoXbRmbR3MPXvBos71Zd8ZhbXXuvECpblWqzZvrctdtPje6vlX99ajvp4+e15n+Q/iQz+YUf3a+VdXvz77juq0hU9XX1n916oz9v3L+hfyv3Pg/1V9/NqHOrcDAIDpQqAUAABgMAiUAsNGoBQAmC4ESjlKmVI+QdF2+PPmlavrdc3p5ScKlJ5/2131yJkJd5ZtuhxLoDR13PPW4erGFY9W2/bvH1OfH85fWI9Y2g5ituvV/ly06/FBAqXl3HtNx582ef29d6uLF9971Lp+AqUz77yvHpl06YZnqg+dee7o5+WbNtefu7aZVCPH+Pg1D1bf3vlfVRnl9NN3bO4uBwAA04BAKQAAwGAQKAWGzbAESn/jJ5dUz+15tZZ/d5UBAKaWQClHScixK9gZGREzU803RwSdKFD6uQsvr148cKAeOfPLs+aO7qsEID97weX153aQs8jydqA0U9pn6v1MJx/NbUoQsznqZ45119r1Y+qVUUMzemgzsJlyjzy/ZUw9yv6OJ1Caem546eU6hNsepfQ7V82vdh482PM/y13HbUq7bt23f8yIpLk+mWZ//ztvVzNuW3LUNidKprw/ff0f1L+c/40bVnaWAQCAqSZQCgAAMBgESuHEa/+Otx/n3rq4/j1kfp96LIPbfPgH51VXL1te/2704M/fqw6+9261Ze++asbtS07OIDkDYCoDpZ84d2a1bOOm+vfu+R34ibw+AqUAMP0JlDJGQqIJi473H7jrHlpZj6o5666l9eeJAqUx9/6H65Eztx84UN30yGPVvAdW1NPSZ/TOax5cUZc5lkBpLFj1eH2cdl3//+z9eZgc133f+//zo0SKlEWJlMSdJimTlChSJCVSIAhJEEDsy2Af7ADBAQgQGCzETuwrsQ4wBAb7NgAGALGDpBwvSuLk2rIiL5Ljaye5vk9uthvlSZzEiZ0b27J1fv050Ld5uuZUTw+mZ7p7+v3H6wG66lTVqaW3qU9/j+2D1q3h8LW9xkwbfTkJ+2XttM25W7b7/drbfNr3s61AqX1ZUrh27JKV2VBszAsTXnfHL13y/dnQdNBvR8PUK8yqKqvfSeyvyRco1Qd3VSXVPk1dlRvgVFC1+coVP/y9hX67wm09+7vnT/5b9/LVv3Z3DpwSbQMAAACUEoFSAAAAAKgMBEqBzncrgVLde9X9Vi335MgJ0TZJd31rgL9Hqvuah86fd7M2bHH17+7w9401Tf8nVFq6QKnuJzc2n87ey9Z9fN1f17lSsPStzduiy90qAqUAAJQ/AqXIoaqWCkqmVcWU52qn+V8n2S/PCgmUSr/Z8/0vmfTBM/arpvYGSl+aXOcDmbG+qhLq9qPH/XYUEN129JgPfirEGVYufXHijJt9yvTX2imo2lagVF989CstLXPm2jXXc/qb2Xkxjw4Zk618qmOjD+SqJPr0qEnR9pIvUNpn5jw/T9VPVQU1OV8BWe17V38B+/SwN1yPGz93T2z4KDofAAAAKCUCpQAAAABQGQiUAp2vvYHSx4fX+pET95xq9vdorWhQW1QcR/ctNZqkKpXadAua6n6rRrUMl6lGpQqU6jzq/CSLGOn86H77qSuX3bPjpubM6wgCpQAAlD8CpQCK6le2ft9989rP3O19RkXnAwAAAKVCoBQAAAAAKgOBUqDztTdQquChivcMqV/sQ6UNJ09FC9+ELDyoIKoCqcn5CioqsKiRLe/s1d9PSytCFOuvtl+/dYc7e/2aL+qjdanCphXcUVsts+3IsWyxIK1n88HDPhSrAka2LlEFVq2rx9SZOdO7QqkCpWnHW2qXvuOP3+B5bxd8zFRQSUWcdKxV1OnYxYt+NFM7J2mBUht2386l/tW5TV5jare6cf/N9X/0oR+1UyOEah+SBa8AAMCtIVAKoKg+M2quv0l/35s7o/MBAACAUiFQCgAAAACVgUAp0PnaEyhV2FOhTwsBKsBXSPDSRpzUyI2x+VrvjmPHfeBUIz5qWqGBUqtwqraLdza48ctWuU0HD/uQ4cz1m30bC5Rqmkbg1Do0nHvNgqXu/cw0jVxp639w4Ch38Nw5V0hQtjOUtELp9z5yq/bu88c01kYKOWaPDRvnjly4kDnml13duk0+3Kt5Wr+dk1igVP/u/cWw+8t37/XnUkFVhUZ1jq1fds51PnVNWTstp20QKAUAoDgIlAIoqtte6e++ef1n7vF116PzAQAAgFIhUAoAAAAAlYFAKdD52hMotWDo7I1b/WOrLLpkZ0OrtiGFQhX4VEg0Nl+S/Sg0UKrqmQoSKrRobWyYdguoWqBUVTKfGTM5286G71eY1Sqj9p1V785l1hcGJrtSqQKlFtJUVdDjly75/Y9Vky3kmI1YuMwHO3VubLlHMufh0PnzrrH5tLunz9BooFShVoVHw3Opiqb1mWtB0xVm1TTb3tJde7IVT0XD9RMoBQCgeAiUAii6rx74P91XGn8/Og8AAAAoFQKlAAAAAFAZCJQCNwN1n62Z5h6v3+Re2HbG9Wj60PU+89uu35WfuP5teOXA96LrDLUnUKogaTjcufqmIdCbWlrc/QNGtGpvCg2UhgHSQgKltv2wsqlRuNGWt0Cplg3biCpbqmrpc7XT/GNttz3D3fdo+shN/Od/l/H3RaHXvXG/91du0Ef/qqieWXMg2v/Q7T37uQnLV/tAqYKlFi4Nh6qXto6ZBT6X7d6bWuU1GSiNVak12o62p+3qsbZ3LnNue9fNzWln1xmBUgAAioNAKYCi+5Utv+6eO/Jn0XkAAABAqRAoBQAAAIDKQKAU1eyO3kPdV9cecqN++8/9c8H745+7MT/8Czf01/91NECaVMxAqQKjCo5qyHurTCmqGqkKoUPrF+e0DxUaKA37UUig1EKJFn5MKiRQqn5rCHcN36/wo4Zm33OqueDh7l9qvOom/NHP3MQ/+rui0Hke87v/3X235XeL6smlu6P9T6NKpHO3bPfHTVU/VzQ0Zs97W8dM1U4X7djth6RXZdHGzDlS9dB7+w7Lrj8ZKE0+tnZhWxtSP+2aJVAKAEBxESgFUHRPbPjQvXiWP/QAAACgvBAoBQAAAIDKQKAU1eqeUW+4Ub9zM0g6/Pv/zj238Zh7YOoi98lvDYq274hCA6VWdTIW3JS1+5pyqliGbKj8jQcORedbdUpVw7Rh1tsTKNVQ9gosjl+2KsfYJSvdQ4NG5w2UPjhwlDt47pwPyr4ybVbOkP6lUKoh79NYgDM8F4UeM53LGWs3+kCpgqWqYjps/hI/LxkgTT4O1/OFfjVu35mzBEoBAOhiBEoBFN1jq6+4ly79VXQeAAAAUCoESgEAAACgMhAoRTV66PWlbvwf/rUb/YP/5kOksTbFVEigVEFRBUZPX73qA4LJ4ObWw0fdqSuX3bPjpkaXt7CghjO3wGhIy2n5sPppIYHScJj02HpNvkCpLN+9129/8c6Gdg133xlKEShV6FYVRrcdOebu6TO01XwFNBUaDoOa+Y7ZAwNGuseGjfND6Nu0FyZM94HhxubTfhvJAGl4Ltsa8n7ulm3Ra4NAKQAAxUWgFEDRPbb6snvp0v+KzgMAAABKhUApAAAAAFQGAqWoNncPneTG/d5f+SHt73xtVLRNsRUSKFVYU0G/zQcPR6uQ9p+zwFcvnbRyTat5RhVENQT6gm07c4KGGh59Q9NBX8HSqldK7dJ3/FDrYxavzE5TdUoFH8P+KuCq9apCZti3p0ZNdP1mz/fT2gqU+uqrN677PrRnuPvOUIpAqcKcqh6rMGafmfNy5un4Ld21x53PnF+dZ5ue75gp+JkMmVpg1AKkyUCp2uj60fomrvj4OtL269/d4afXLFjqp1m1XPUrPOf+GksEXwEAwK0jUAqg6AiUAgAAoBwRKAUAAACAykCgFNVm0If/0k34yc/cZ4fHK312BoUsFf57Y92mVpVHbch4Bf0U2gzDnSEbAj1fGFPB0fX7D/j1HDp/3s3asMU7cuGCDwEqNBiGA61q6cnLl11dpm8Kjmq59zPLh4HS+weM8FUvFThUkFH9nrPpXV/RUkOtPzw4/5D3to6mlhZfhbOUw91LqYa8f2bMZH8uFBxVwFehzinvrHPbjx7350zTdA6tfb5j1uv12f6aUkVSnTudEwVWtZ4lOxt8m1igVP/uzZxL9UEVULWczqnObbh9CyFrfVqvtdNyBEoBACgeAqUAio5AKQAAAMoRgVIAAAAAqAwESlFNNLy9rvmvbTkZnd9ZFLJUKDBGFSv7vlnvQ4UKZD45ckJ0HaIAYFvDxasy6YTlq92xixd98E+BQIUFtR2FVcNAqQx8a6GvjKq+KCy4qnGfr0oZBkrl3r7D3PKGRr99tdW/KzKP7+tf4+e3FSiVhdt3tdn/rlCqQKnoeCnYq2Plz0+G/j9709ZoUDjfMXtx4gzXcPKUP786JwoHK3hqodBYoFRi57J+645W21e71Y37/fp1He07c9YHYBnyHgCA4iFQCqDoCJQCAACgHBEoBQAAAIDKQKAU1eSVg7/qh7u//dulG269FJ6oqfUVRhUKXLyzoSTDzWs49nePHC35cPdSykBpe5TTMTMjFi6jQikAAEVEoBRA0REoBQAAQDkiUAoAAAAAlYFAKarJyH/6n13Pw78WndfdKZC4cs97vhKlqpjG2nSm58dPd81XrpR8uHuplEBpKY+Zqpwu2r7LD9Nv01Tddu2+Jnf62lX30uS6nPYAAODWECgFUHQESgEAAFCOCJQCAAAAQGUgUIpqcfewKf56f3Lp7uh8dA4N4T9j7UZ35MIFP7T+U6MmRtt1pXIPlJbDMXt48GjXdLbFNV+94uZseteNX7bKbTxwyFe5Xb//gK+eGlsOAAC0D4FSAEVHoBQAAADliEApAAAAAFQGAqWoFo/Mesdf7/dPWRidj85Rs2CpDyEqGNm7bm60TVcr90BpuRyz+/rXuBUNje7s9Wvu0vc+cqeuXPbVUlW9NNYeAAC0H4FSAEVHoBQAAADliEApAAAAAFQGAqWoFl9atM1f75+tmRadj+pRKUPeAwCA7o9AKYCiI1AKAACAckSgFAAAAAAqA4FSVIuvrNrvr/dPfbcmOh/Vg0ApAAAoFwRKARQdgVIAAACUIwKlAAAAAFAZCJSiWjy74ai/3j/Za2B0PqoHgVIAAFAuCJQCKDoCpQAAAChHBEoBAAAAoDIQKEW1IFAKQ6AUAACUCwKlAIqOQCkAAADKEYFSAAAAAKgMBEpRLQiUwhAoBQAA5YJAKYCiI1AKAACAckSgFAAAAAAqA4FSVAsCpTAESgEAQLkgUAqg6AiUAgAAoBwRKAUAAACAykCgFNWCQCkMgVIAAFAuCJQCKDoCpQAAAChHBEoBAAAAoDIQKEW1IFAKQ6AUAACUCwKlAIqOQCkAAADKEYFSAAAAAKgMBEpRLQiUwhAoBQAA5YJAKYCiI1AKAACAckSgFAAAAAAqA4FSVAsCpTAESgEAQLkgUAqg6AiUAgAAoBwRKAUAAACAykCgFNWCQCkMgVIAAFAuCJQCKDoCpQAAAChHBEoBAAAAoDIQKEW1IFAKQ6AUAACUCwKlAIqOQCkAAADKEYFSAAAAAKgMBEpRLQiUwhAoBQAA5YJAKYCiI1AKAACAckSgFAAAAAAqA4FSVAsCpTAESgEAQLkgUAqg6AiUts/ot1e4S9/7yP8bm18t5r+7w7XcuO6+/cZb0flpOH4AAKBQBEoBAAAAoDIQKEW1IFAKQ6AUAACUCwKlAIquVIHSzYeO+GBhPhZY/Pxrw917p894+r+W13TNV7Axue7OVC6BSNv/cx/ccH1n1UfbiB079bmYx4pAKQAA6GwESgEAAACgMhAoRbUgUApDoBQAAJQLAqUAiq5UgdI+M+e58ctWZe07c9advX7NvbFuU3ba2CUr3UODRhMojbD9V1/W7mtyt/XoG22nsKlCpwRKAQBApSFQCgAAAACVgUApqgWBUhgCpQAAoFwQKAVQdOUy5L0qlp68fMl9qWZ8q3kESluz/X//ow/d0fffd48Pr23VRiFThU0vZtpcJFAKAAAqDIFSAAAAAKgMBEpRLQiUwhAoBQAA5YJAKYCiq4ZA6YsTZ7iGk6fchQ8/8MHKIxcuuMHz3m5V1fPevsPc8oZGXylVoUf9W791h7u79+Bsm1ggsnfdXN9244FDvq32QfuifbI2kuyz1qFA6NRV6922o8du9i/zePvR4+6JmtYB0ZCtS9vUcpNWrmnV5tlxU92pK5fdrhMn/b/JY9Vv9nzXdLblZuA0Q1ViX55Sl9NG1Bf1SW20LfV1zXtNOYHStIBp8rzGjt/tPfu5ySvX+j7acZ+7Zbu761sDsm0AAED1IVAKAAAAAJWBQCmqBYFSGAKlAACgXBAoBVB03T1Q2uv12T6gePzSJVe3bpOb8s4615hZj4KRtctWZdtpvXubT7vzH9xwy3fv9UPuKzSpdhuaDmbDjclA5DNjJrtjFy+6xsyy9w8Y4ae1J1CqdWkbCoaG2zzQcs49PHh0zvIhW5fCqE0tLW7Pqeac4KtoXzXcvdar/oTHaszildntqN2MtRt90FbHSgFZa6d90r7ZcZm4Yo1798hRH8wtRqBUod76zLJav/ZdfVWoV31b3bg/dSh/AADQ/REoBQAAAIDKQKAU1YJAKQyBUgAAUC4IlAIouu4cKL2zV38fflSY9Ktjp2SnW0gyHCpeFT4VYlRg0tpZ2FHTaxYs9dPCQKSFSUX/t+XaGyhdsG1nTnBy5vrNvhporOqosXXN3bLNzd64tVWYU+FShUzla7XTcgKl2n+FUFWR1EKwYvsThlMVNFVfpq5al22nvq5oaMzZ5q0GSm0/wmOgf5fu2uNOX7vqXprcumIqAACoDgRKAQAAAKAyEChFtSBQCkOgFAAAlAsCpQCKrjsHSp+rneaar17xlS+T81SVU8PND61f7IOnO44d9wHTR4eMyWmXXIcFIie/s9ZXNFUA8/nx03OWaW+g1MKV5qlRE/3w76paGk4Pheuyoe2X7GzIzu87q95XJ9V+vjpdVVo/3m7/OQt8RdC69Zuz7Y2qkFqQU8HOrYeP+n15cuSEnHbJAGnysWkrUKrlYsFRnRedn+SxAQAA1YNAKQAAAABUBgKlqBYESmEIlAIAgHJBoBRA0XXnQGm+NjZPocrY+o3Nazh5ylfttEDkwXPn/bDvmw8ezg6HbzoaKE1uM5xnwnUp+Kl+WMVVPV67r8mHTBU2LXS7Ns+CtvmOi9aldWrdscemrUCp5utxmlgfAQBAdSBQCgAAAACVgUApqgWBUhgCpQAAoFwQKAVQdNUaKO1dN9edy8xrK1D6hX41fmj4ZKBUlUnr1m3ylT7D4eClqwOleqwh+TU0v4bJV6hU4VKFShUuLXS7Urv0nS4PlJ69fs29kTmW45etaiVZGRUAAFQPAqUAAAAAUBkIlKJaECiFIVAKAADKBYFSAEXHkPe3NuS9gpcKa65u3O8Dkb1en51dpqOB0vYOea/H9w8Y4ZpaWvx+TF213odlNex9rG17h7w/fumSD6mG7ZIB0uRj01agVNvT8esxdWbOcgAAAARKAQAAAKAyEChFtSBQCkOgFAAAlAsCpQCKrjsHShUUfffIzUDkV8dOyU5X+LKx+XR2iHhNU2VPVficuGJNtp0ClfWZ9Wu6KoBqWjIQqfCn1qP1ab2aZlVNk5U9tQ6FWK3Ptq4F23b6bVk7VTy9mGmnPtm0pNj+z9641Z374IYPhO451Zytbppsa+FT9dH6LM+Mmewrr4bLph0XVT8NA6QK2F7M7MuYxSuz7bQOrStfoHTQ3EV+/Qrs3t6zX3ZZ9Wt45niF0wAAQHUhUAoAAAAAlYFAKaoFgVIYAqUAAKBcECgFikBhuBlrN7rTV6/6YNvyhsZou2pR6YFSVdCMDZXeZ+Y8306VQ1UBU6FSDVGvyqSNmfUoxFibaRduY2/zaV+5U1UztQ6FHNVuQ9NBd9e3Bvh2yUCkKESpdmEwdMnOBh+w3HTwcHZdqhqqaclAqQUq1U7bVh/CgGpMLFD67LipvrKp1qn9zNfW+nyg5Zxvq+fEkQsX/LHqXTc32059UF/suChYqpCulg0Dpbbtk5cv++Os9R06f94HaPMFSnVcdXx1XLRerd/6onP2tdpp2b4AodjrAm56ceIMHxqfvWlrdD66FtdqfrH31VLQjyDW7z/gth893q7zVC79764IlAIAAABAZSBQimpBoBSGQCkAACgXBEpRFhRKC4NkCoopMKab6SEF01Y0NLr7+tfkLK/goOYrPKYKkuG8kAJvCpmlhQzl5Sl1vhqjQnEPDx6dM0/BvqW79vh1qOKjTR82f4kPwylUqCCdhvYOl6s2lR4oTV53Ruuz5RWuajh5yp93XQ8KKw6e93ZOVVC5t+8wHzDWtat16N/6rTuy1TolFhzRdawQitZfs/BmJVOtS8Phh9ucvHKt38dkoFSVRbcdPXaz7Ucf+jDLEzW5Q8wnxUKi2p/NBw/7YKcCnvnaSr/Z813T2Ra/TVHFUj2nwjaivqhPaqM+qq8aLj98HZCBby301Vq1Twqgrmrc55+D4XmNHT+FSnUMLAyrZTXc/9OjJmXboLKkPT91bnefOOmfk7Hl2oOQXjo9j5uvXnGLduyOzkfHtfUeZPReVCnXauz1uSuUartJeq/flXl9Slbvbku59L+7IlAKAAAAAJWBQCmqBYFSGAKlAACgXBAoRVlIC5TqBrxVh1SVwS2HjvgAmm7Oh4E8C5QqrNdj6szs9JANVa52aSFDM3fLdr+dmes350y3ypTJ7asSpII2z1H50CuXQGk1IoSC7srCdskKwgo46nVZwkq4bYkF8iolpNfZeB0pjYcGjXZjl6zMXtv6bKTAdPKaV7XscrtW7fmZ/JFBR66lUi1bDiq1//l+yFNOCJQCAAAAQGUgUIpqQaAUhkApAAAoFwRKURbSAqVhRUhRtURVKFXAov+cBdnpaucrIn7vZmXGcBmjoKkCR8nhsmNsSO7mK1fc8+On+2mqeKiKilqHgqVh+0q5gd5VCJSWTqWHaIA0aYE1UZBUr81tVakOxQJ55RbSKxVeR8pDvmu+3K5VAqXFVan9J1AKAAAAACgmAqWoFgRKYQiUAgCAckGgFGWh0ECpxG6yq93Bc+ddU0uLFxtWdMnOBh840tDahdzsVgUw9UnDjiugVLv0HR9GXbBtZ3ZYcwtQqD8mXHds2G0NWR4O2W/7uu3IMT80uob/tv3WdjSMuoY2V1hW81Qd9atjp2SXFw3jHQ5vrsquxRj++VYRKC2dSg2hAG3JF67Ta/SOY8f9a+mLE173Ibuj77/vHh0yJttGVaUbTp7y03ceP5Hzui1abxjSG/X28uxr77nMdlW5Wq/p4Xb1Oqt1+tfeTDu112u2vUeIvb9pfeHrtH6g8ERNbc762hK+X6g/ek9T3/Ueo/V/oV+NW9W4z7/XWH8GvrUwZx057xeZNscuXnS1y1b5Pud7T0s7/hoKX+85/kcdGU1nW3J+dKH2p69d9e+h1k7b1nvhvX2H+TZjFq/0falLVAUfWr/Yv++m/VCku8t3zRd6reb7jCHh+dM8XRvJ61LXzMYDh/x1ZdebqsbrmrH1h9eM+qy+a9nke1Ih/c53HVqf2up3oe+FbX1+svVMXrk257ml6zzW7s2NW2726xfbTgv+tvW8KXS79voy5Z11OcdDVW1V8VbPfzvGWofWZc8709YxsHOsUQJ03u0z7emrV7PXgfqhaaHwOsv3umNtuhKBUgAAAACoDARKUS0IlMIQKAUAAOWCQCnKgt0QtwCC3byOBUoVLFHAREETm6Z2ullft26Tv1lds2BpzjIPDhzlDp4756vXLdq+q1UwIUY3uZfu2uPOfXDDjVv6jjvQcs57ePDobBsFHx4fXuuDQbrB/ur02e6xYePc7T37+YCTwqi6Oa8ghoapXbyzwe+nqp9a6NX2Ve00bL72ZcbajX6ebrZrfxRY0k177d/Jy5f9jfhnxkz2bfSvHiswoOUUKjh0/rwP8CiwYH3tSgRKS8dCKPo3Nh+oVPnCdaLXTnttj71PPFc7zb/GrnmvyT0wYKSvPq1wluj/CntZ+EvhNlWonrtlm39NbcxM0/oUirT1KfylQOfxS5f8a7O102u2XrutnfqrZdXW3gvUB7XT40Irqkr4fqEfT2ibcza96/dL71Xqs/oQTj+eeX/48uhJfnm9P+m9Qu8j6rPeV/T+ooDXzPWbs+9p01av968j+tfe02LH3yrD6j1H2xT9X9M0T23UXuvXMV2+e6/fpt6LNU3vnbZNBX3VFwV/bf1qr/eylyaX5r2s1PJd84Veq/k+Y9j5s2vGPmPoGtF5Vxv7jGHXuV0zun4VBNa1obbDM5+71Bd9btL5tEBr8j2pkH7nuw4L7Xch74WFfH6y9Sg8qR/0aP/ts1wYErV2Oi5ax4amg/41wvY3bFvI86bQ7eZ7fum8a1+0XDg9vJ4KOQZ2DWnbep7qeKutltE0Xafqj9qFn4f1OqvldU7UNu11x/rSlQiUAgAAAEBlIFCKakGgFIZAKQAAKBcESlEWdHPbbkrrsd28DgOlChKMWLjM33Df23w6ezNd1E5hB93A1s3uzQcP51Q9UsBUlZn0r7aldWsbNj+N3QRXyEEhgWHzl0TbhUEmm9Z3Vr0P+IQVTcVColZxzfY1DImKAqcKDIXBAbGh+7UfCiIpkKR9D6uWKhyloIZCSzatKxEoBVBs+cJ19nqpHw7oBwT2OqmAlbWZtHKNf+21kKleV5NBL5sWBrvEwqh6vdVjvfYqnJV87VU/9IMBvQ8pEKdp6q+CU/WZf+29wCqqanlrVwh7v1AFwbAS96C5i/x7XHK6wmHatt479Vj/6nEYjH1kyBgfIFO/7+kz1E+LhfGSxz97DILAqlg41N6H1V7voQqRWRtNV4VSvUfqvVKP1+5r8sdYx1pt7JwmQ6bVpJBAaVvXatpnDFs+ec0MmLPQnxerFqvgoM6nPl9ZGzvHuoYtEJ3W1+S1VGi/Y8tKof2OLRsq9POTrUcBUQvJil5bzl6/nj0uae2sv6L/F/q8KXS7seeX2qsKc3K6/bjKnuuFHoO0a6j/nAX+dUehYJsW+zxc6OtOVyJQCgAAAACVgUApqgWBUpixP/qf7qW9V6LzAAAAuhKBUpQF3RCPBUp1Mz1JIQLdiA6XD29ga2h7VUd6dtxUP89u3lvQSDfPkze783lr87t+uwqxfu67Q6JtYjfQ0yqr2Q19C8nYvmodYTu7UZ8cAlj7o/CQhuR/cuQEv2wyOGpDO1uAIZyXdO/kVe6XV14oqudP/Qd/o/7Fs/+5OE7/J/eFGaWpYgVUskeXt0Sfo+XkkaXN0b4npQXWVDV63S+qQWvIbE2z10B73VdAS0GtMOiZDHqlTRMNJa/3HnudDqudWhujEKeCXBZcTb6/mYUFVssOpb1f2LHROmPTLVRnP3RYlnl/yhfStDCbLSfJ45/vGOj9Se9Ter9K23+FCVVV0cJoNry9jp8e2/YKGe7+0eVno9dWOSn0Og8lj3mo0Gu1rWsm+Rnj0cznKx9sTLQPxbad1tfktVRov2PLSqH9ji0bKvTzU9p6FI5UP9QfPU5rl9zfQp83hW437fml46DPwk+NmthquvWl0GOQdg1p3dpGeM7VRm3D17VCX3fy+eIbW/xzqC0P1jdlPi9ucZ8d97a747Wx0XUJgVIAAAAAqAwESlEtCJTCjPitn7pXj/5GdB4AAEBXIlCKspC8IW43rxUu0PDAxobwVdXQsEpSeAPbwgYWQrFKdQqaJtva8mlUsUmVm1TVTuHOcBjjUGyd+bajeQo+KABh+6ppYZu0MEHI9lXtYiwMEFvWKHD28pW/LqoeN37uvn7+v/mb9cXy9O7fjfYfQNzt3x0VfX521PMn/210+q373+62nm0P+57v9U5h0hUNjS6s5KfAmV63Fc63sJmqYCpcqvnJoFfatHC6vU5bX2JBP5tngbe0wJem23uEvd6btPeOtPeLtP7YdHsf0fFZtGO3P156X9OQ4VNXrXP39h2Ws1zs/Se5jbRtJqXtv+2LhdnsHFnVSx0/vXfrPTxcLun23iMj11THPX/i30Wnd8RtPT++PguR7xgXeq229RkjTdheQ5/vPnHSP5/CNuG20/qavJYK7Xds2XBaGls+tmzI+ptc3lj/0tajx+F1ndYuub/5zmmo0O2mPb90HGKvI5qe7Ivtc5K1S7uGbHq4L7HtFvq6k4/9UKm9vnHhv7snt/0j90s1b+asj0ApAAAAAFQGAqWoFgRKYQb/6p+577b8IDoPAACgKxEoRVlI3hBPu3ktNqypBUQlvIGt6kd7TjX7oXI1HKqCpWEoJXazO0aBlvX7D/h+Dalf7MOtCrskqz1JbJ1p21GYaevho0UNlGp9YfDWWKWr2LKdiSHvgfLwiVcHRaeXk0JDdmmvd3qdU+gp2d4qAeq9QtUvFYarWbA0Oz8Z9EqbFk6312nrSywUZpU32xMoVaXAcJ80RHSskl/a+0Vaf2x68n1EVVo1lLmCXQp46T1y2Pwl2fmx95/kNvIdg1Da/tu+hNURVdlbFQ9V2VsVEvVeXkhFw+50nYfyHeNCr9W2PmMoiB1ee0bVMtVOgWz1QefjtTfr/fo0JHrT2Zacbaf1NXktFdrv2LLhtLb6HVs2ZP1t6/NT2nr0OLyu09ol9zffOQ0Vut2055eOo867zldyerIvbR2DtGvIpof7krZdaet1pxhu7z3CfXroG+6eicvdI0tPu680/n42XPr07h+4T377ZoiVQCkAAAAAVAYCpagWBEphvnX8H7qaf/wfo/MAAAC6EoFSlIXkDfG0m9dp85I3sCetXONDp6PeXu6DpRry3oKV+W52h3STWze8l+7a40Og9nh14/5shTsTW2dHh7y3UJIN/2u0bQ35/8CAkdnAVFh1rxwQKAVQbIUGsYxe81XtUu8B+nGAQvw23L0kg15p08Lp9jpdjCHvNb2Q96JQ2vtF2rGx6RZK0/vGY8PGudt79su2eWHCdF+Ju7H5tLunz1A/LRZmS27DhrvWe521MdqO3qf0vpS2/8kh70XhRb13L97Z4I9vIcPdd2dp51UKvVbTrhkda4Ws2zrGGw8c8ssr9GzTYttO62vyWiq037FlpdB+x5YNFfr5KW09ehxe12ntkvtb6POm0O2mPb90HGOvL5pufSn0GKRdQzY9POex7Rb6utNZbu8zyj267LQPkD5/8t+5O/tPJFAKAAAAABWCQCmqBYFSmC+vbPTXwmeGtC5u1F0k/96ov00u2r7L7TtzJlrUCQAAlAaBUpSF5A3xtJvXYhVKw5vxyRvYCg0pPKSb9smqdLGb3UkPDx7tDrScy6lIqnCSghXatvoQto+ts++set92wbadOTfqNWy+gqkWhkjbV/Xh8IULrSq0qTJY85WbVfc0fefxEz64+ur02TnL95k1zw/ZH07rKgRKARRbWmAtH/24QOFOveYmQ1PJoFfatHC6vU7r/UA/VFAgKnydVVVsBaTC8Gpa4EvT23ovSkp7v0g7NjbdQmkKwCaHkbfgbbjPsTBbchv2/pMM6tr7p/2QQ+11DiauWJNto/OgH2voPVLvlTZdx08BYJ0vva+1Ndx9d5fvmi/0Wm3rM4bO3xM1H58/nRv9gOahQaP9Yy2nz1LhH/J0zevaD7ed1tfktVRov2PLSqH9ji0bKvTzU9p69Fj7q/3O1y65v4U+bwrdro53+NjoOMZeXzQ92Ze2jkHaNWTTw3Me226hrzud7e4xC9w33v+f7vkT/9bd0W8cgVIAAAAAqAAESlEtCJTC3Dt2lr8WvrblVHR+V7D7H8m/6RVL8u+N+tuuijlp2jNjJrdqH8r3N3MAAFBcBEpRFpI3xO3DpIaZD4ffXLRjt/8Aq5vfL0/5uPJn7Aa2Ape6Ga9qoKoKmq9tyIIuFzPLTl2VWx3Uwpzql4IvNj22Tn3gVlW8ix996IOo6r+qrmk/FTiy5ZMfnEMWPlU1U4Vx6tZt8iGOYxcvZj9Uq8qbjomqTM3Z9K7fjm7eK0i7cPuunIpQXYVAKcqZnuNd/WvHtHAQCncrfyh4dtxUH4ZToDH8YYGEfxR5c+MWP1R2MvxlbW16+Drd6/XZflm9Juu1WZVJbShnvXZbu7TAl6bney+KSXu/SDs2Nt2uu2Sf9X6h9ye9T+k905azH0Qo8DV2yUof0ottw95/Dp0/7/dfFIrTNM1TG7XXta9l9R4YbnND00F317dyh4K39+5Ch7vvzvJd84Veq4V8xjhy4YIfilyfM/Sc0OcHhbHVRv/q85DOsdpoqHmdX52jcNv2XNNQ+FqPVTRNvvYV2m+JXYeaXki/C3nNLeTzU9p69Fjnxp7Xae1i+1vI86bQ7eraCB8bHcfY64umx/qS7xikXUM2Pbw+9fzVudF7rA2ZX+jrTle4d8oaHyR9cts/IlAKAAAAABWAQCmqBYFSZOmH87/xb9y4H/2lu6P3sHibTqaRN3UfXn+fjP1tuqPy/c06FPsbZ76/mQMAgOIiUIqykLwhbh8m9WE1pBvcu0+cdC9OnJGzfL4Plcmb1bG2IYWKtNyuzHZiYZa5W7b7m+D1mT4rmKZpaetUUEaVSBWysP7rV1b39a/JtrF9jX1w1voHz3vbhyYU6NBN+m1Hj7mnR03KaafjodCp5qudAqcTlq8uSZhUCJSinLXn147FkhYOQuFu5Q8FOtdbDx9tVQ3QKOikeTo3+gFALPwlNj35Op187dVrtV6z7b1Bku9v4fTY+0Y+ae8XacfGpofXXdhn7bfen/Q+FQY79f/lDY2+zZlr11zP6W+mbkM/7tCPLPS+KPp/+IMP2//pqzdk22m9eg7e27f1H8QUcNNQ+G0NaV4N8l3zhV6r+T5jSL/Z830I1M6f/j/grYXZa1j/KrB5+urNPyDqelFoUwFO/d9C+Wo3a8MWf+60HgtwJ1/7Cu23xK5Dm9dWvwt9zW3r81PaevRY58ae12nt0va3redNodu155c9NjqOsdcXTU/2pa1jkHYN2fTw+tT14Pcrsx6t04azL+R1p6v8ytbf9GFSAqUAAAAAUP4IlKJaEChF6NHZa/z10Ovob0bndzb93U5/D1YBDY0oFhZYKoa2/mZtYn/jzPc3cwAAUFwESgEUHYHSzpUWNEH54pyVhg2hnhzuHl0nLfCWRmFFhrsH0Fk+1W+8++b1vyNQCgAAAAAVgEApqgWBUiQpTKpr4qllXTvCj91TUUGBUW8v94WShtYvzmmT9iP6WFA0VrBg7pZt/t+wXRge1T0FtQ1Z2/YESgspCCJP1NS67UeP+x/+q60KO9Wt39yu+xoAAHRHBEoBFB2B0s5FOLHycM5KY8Cche7s9euthrtH12lPoFTVCvWHG4a7B9CZnt79AwKlAAAAAFABCJSiWhAoRdInevZ3fS78yF8XL+290mXD3/edVe9HoRqzeKV7cOAod/DcuVYFO9oTKK1dtsqHNA+0nHNT3lnn5mx61zVfvZITEpUwUKp16l/dK1Dw9NXps90DA0b6doUGSjU63dnr19zxS5dc3bpNftuquKq+qE/WTgHaxubTPji7fPdeH35VmFbtCJQCAKodgVIARVdugdKnR01q89dlaYG/WBhKQaf6rTv8lxEtoy80+pJhX6jsS9O2I8eyw/Xqy9Dmg4d95b2XJn88rK2sea/Jr6utinz2RUnbNNrO6MU3h8DVesL22o62Z1/2tC96XLv0nTaHnm5rH/WFbm/mS5a+TD48eHTOskbH8v3MNqauWu+Pubalbepc6Bd/YVsNrashdjXUrn4paOcp2U7nMruuTDu115e/8Mtssu/6V+ch3Mfwy+mz46b6/Ut+KX50yBg/HPqOY8fdnb36+2k528/si46jfuVoy4i2o2MattGxV1/s+rJrZEumHzqu2r7m61ea4XGWQvYHuXTt2x8mdJ2Gf9RA1yokUKprXM8N/aFGf7jRH6ti7QCgGB5ectIHSl8889PofAAAAABAeSBQimpBoBQxt73ymnv5vev+2pjwk791rx75dffEwq3u7mFTou2LQfcZdV/s8eE3780t2dng71/pPpq1KTRQqnuHhy9c8GHS8D7iCxOm+3thaYHSfNPsPmm+QKnu5+leg8KkXx378bGy8Gi4fxoxTffxpq5al22n+3NLd+1p874GAADdHYFSAEVXToHSx4aN88MY6IP/4swXn/HLVrmNBw75sF/4ZaDQQKkq6G1oOpizvk0HD/svHDPXb/Zt7EuTpinQpi89M9Zu9FUSFbBUmNXWb7/w07ALClXZ9BhtW19ypq1e7/uqf7V/Chaqop++BCkEae31izttz4aj0L4ohKlfF4a/tNM0hTy1fttOW/uoL4nqc9PZlryBUvVTx1rHXOvRl1E9Dr9A6stZfaZv6qu1m7tlu99+GAa0c3ny8mX/i0L1X31Q/61fWpeFORW61Los1LvrxMnsMQ6/iGoZhX2Tx6//nAU+3KbjqMfPjJnsA6zqg86nph86f96HdF+ecjMkbMdOxyq5z+H1ZdeI9lHb1f5onXat2vVW6P4gl8KkOgcK84Z/MEDXKyRQque2nlt6bZq9casPmMfaAUAx3DNphQ+Ufr3lv0TnAwAAAADKA4FSVAsCpcjnixPmuu+c/qdu0h//3F8nMv7Hf+NG/NZP3cAbf+L6X/lJXv0u/9jdP3VRdN0h3X/U/aqweI3+rq9R4BS8tGmFBkp71831f/NPhj+T7SQWHo1NU390vyFfoPS52mn+3myyCI8k75tqGwrMPjVqYk473ctr674GAADdHYFSAEVXToFShZP05UDhQ5umkN6KhsacLwOFBkpV3VMhw3B9ChEqkGmBRPsypICUQojWzr6MhRUv/fARmfWFIdO2xPqq/VS/FILUY/sFXhiS1L7EjoUCi+qD+qJpheyjTc/H+rlg206/HZuu8KfCfvYFVOvTF8817+3PaZfcpxELl/nwqPpnbR7JLKtQp35VeE+fof4Y61eHWw8fza7Lzre+9GpoDE1LfhFVRUSFNO1LpIS/vNTxVEA0+YvG58dPd81XPv5iOmjuIr8e/Xox3JfpazZEA6XJa8RCrHO3bPOPC90fAABQmLuGTr8ZKD3/X6PzAQAAAADlgUApqgWBUhTijt5DfTD0mTUH3DcaLrlex77vXrv4B9EQaajQQKnu2SkAavcKRUVNVNAmLIpTaKA0di8z1k5i4dHYtEICpfna2Dzdk03bD1Gfw3vDAABUIwKlAIquXAKlCgEqvKlAnoJ54bxkUDTti03YTkG+WCVLCYfQj30ZMgoe6pdx+oWcHmv9GsK8reHuQ7G+2vD2qjyqx0+OnOD7EA7jntxnY78SVIix0H0Mp6dJO6b6pZ+CmgpohtOTkstb+HZZZh/TKnNqyIqmlhZPw9PH2kjyi6iFfe342RdJhXJ1HdnxTP6iUf3QF2n7wrlw+67oMUruS9o1YsfGvugWuj8AAKAwnxowwQdKv3Hhv0fnAwAAAADKA4FSVAsCpSg1K1Kj+1gx4X3MtCBm8r5X8r5YWjtJ3rNLm6Z7b7oHZ/fQYvK1sfuhBEoBAGgbgVIARVcugdJ8XwaS4cq0LzZhO1tf+CUqZO3SwoKiCpiqEqphFSyMqF/3pQUkY2J9tXVp+HwNo6/tqNKlhtm3Nsl9NtZfhSUL3cdw+TRpx9S2Ef6iUf/O3rTV90NVSMNt2vKqkrpox25f3VRVQBsz65i6ap0f8j9c/+B5b/twrdajCqCqNPrl0blhzOQXUQVpFb6146cvxlqHVVHVPmvfw36F7BqLfcGV5LFIu0ZsevhFt5D9AQAAhflk7+E3A6Xv/4/ofAAAAABAeSBQimpBoBSlpntiCo3qntX4ZatyvLFuk5+ne1Nqa/f47L6YrSN53yssZmNtYu0kdm8tNs3u1cXCoqY9Q96r8I22oaIyYTvdy2vP/VAAALojAqUAiq67B0oV6lOQMfmlauySle6hQaOjX4aMwooKLeqXfq9Mm+WDghraPdkun7S+6ouQDV2vL0qquBlWZm1PoLStfQyXT5PWT9uGBUoVFNXxUN90LDQEvPo0bfX66PLapxlrN/pAqYKl+iI7bP6SnDZar8K0OgearxCqqo/qV5aaH/siqvYaRl7HT79OtOHuNc++pGro+eQxEQ11r3XH1ivJY5F2jdj05JfhtvYHAAAU5va+owiUAgAAAEAFIFCKakGgFKWme3Ox+4ei+1AaDVIj6WlUPRVo0b0y3csKg5hhKFWPbQS+fWfO+v+ntZPYvbXYNLtXly9QatVWNXrlV8dOyU5XHxqbT+fcO1VRGd1v0/1Qa6f9W7prT+rxAACgWhAoBVB05RIotS81seHbbyVQal+akkHNpLSwoFEQUGHFxTsb/JcmGyaiUGl9tV/dWaXNcLh7SQuUhr8SLHQfC5HWz+SQ969On+2DnMkvgMnlHxgw0j02bJy7vWe/bJsXJkz3Xwr1JfCePkN98FLHX/9aG/1f50Lb0LY0LfZFVPur/V60fZc/BjbcveaFxzY8pkka8j7cjknuS9o1YtPtWBS6PwAAoDAESgEAAACgMhAoRbUgUIpSsuBnrDiPscqeNiqiD2J+7yOnwjETV6zxowtaMZTwvlftslW+MMyBlnN+HXM2vevvDybbxe7ZqSKqltU9OyvqYoHStOIvfWbO88v2el33Ha/5+4d16zb5bVuRGvXJtmEhU434qHu32hfdG1Q7AqUAgGpHoBRA0ZVLoFRUaVJfcvQlwKYpELiioTHny4CFKsNQo9opQBi2U2VMfdHRr/XCYKFCkv1mz/fT0sKCRhUwtS19IWnvcPeSDCca+9Wd1ivhcPeifYsdC/3Sziqbaloh+2jT8rF+Lti2M2cZ/dJP608OJ68wprVRey0X7qcqqCYDuBaAtS+6GqZC+5is+qovi+F5jH05FW3j9NWr/vyMWbwyO13naOfxE76ibDLE2WfWvOyvHP25zRzL5D6rP7cSKC10fwAAQGEIlAIAAABAZSBQimpBoBSlNGDOQn9fK1n0JZQsuqKRB+du2e7vpenel0KiGhpf1UjD+15qq3uSuu+Wr13snp3uSaqdBVdVVMbuJ2pdMeE6X5w4wy+n+6Vax5ELF9zgeW/n3LuTJ2pq3fajx/19S7XddvSYv7fMPTgAQLUjUAqg6MopUPrw4NH+l2/64K+KoPqFmipj6otB+GXAfoGnafoSpMDe3ubT/stDsp1+rabpCh9qffpFnb5I6ddt2l5bgVLblr7cJIOChbDQooKUySHo7VeBsQqj+jKobcaOxYamg/4LoNoVso8KWO44dsI/VvtwO8YCpeF69As//dJP67fl7Hhoutop0Kr1aj+0vIUwk78oDPuvXyqqjUKlOm/2a0K1UeVVLadt2DZjX07FApyaFw7VIQodaz06DjoeWrf6q20pDKvKqTqGOpbqk/oWttH+tDdQWuj+AACAwhAoBQAAAIDKQKAU1YJAKVBedB8uvDcMAEA1IlAKoOjKKVAqsV+XKeSX/DKgX6s1nW3xwT9rF/sV2r19h7nlDY0+1KfAo/5VxdP7+tf4+W0FSuXm0OjtH+5eFFrU9tXHM9euuZ7T38zOs+HkY0OzK6SofZm+esPNX/X94nisbtzv9yls29Y+WtBRQ+srYBouayxQqtCsjqW2pW3qXOichG1j50hhWf26MaxcGv6iUOvWvmr9FoaVZN8VxlS4M9xmWqD00SFjfBh388HDrY6fJH/ReOziRTdh+eqcYfi1fR1T218da6so2t5AqRSyPwAAoDAESgEAAACgMhAoRbUgUAqUhoq2aBTHsHiLCupodEndK9Q9w7A9AADVhEApgKIrt0BpjIUrS/HrMhuaPjncvVUQTZMvoGpUvfTs9euthruXrt5nC5RaiBIAAKDUCJQCAAAAQGUgUIpqQaAUKA0VkTlx6ZIfDl+jJ2p4/l0nTvpiMRrNMrYMAADVgkApgKIjUJrf8+Onu+YrV1oNd//S5Do/pHmaPjPn5bRPUkVN/ZIuNty9ECgFAADVjkApAAAAAFQGAqWoFgRKgdJ5etQkPyqgRge0kQlrl62KjmIIAEA1IVAKoOgIlMY9OXKC/4Wbfumm0KeGp4+1ay9VPB00d5Efxl9feNJ+NUegFAAAVDsCpQAAAABQGQiUoloQKAUAAEC5IVAKoOgIlMZpGHoNk6Awae+6udE2t+KePkNdw8lT7sKHH7gVDY05w+iHCJQCAIBqR6AUAAAAACoDgVJUCwKlAAAAKDcESgEUXSUESgEAAFB9CJQCAAAAQGUgUIpqQaAUAAAA5YZAKYCiI1AKAACAckSgFAAAAAAqA4FSVAsCpQAAACg3BEoBFB2BUgAAAJQjAqUAAAAAUBkIlKJaECgFAABAuSFQCqDoCJQCAACgHBEoBQAAAIDKQKAU1YJAKQAAAMoNgVIARUegFAAAAOWIQCkAAAAAVAYCpagWBEoBAABQbgiUAig6AqUAAAAoRwRKAQAAAKAyEChFtSBQCgAAgHJDoBRA0REoBQAAQDkiUAoAAAAAlYFAKaoFgVIAAACUGwKlAIqOQCkAAADKEYFSAAAAAKgMBEpRLQiUAgAAoNwQKAVQdARKAQAAUI4IlAIAAABAZShVoPTbb7zlWm5cd/Pf3RGdDxQbgVIAAACUGwKlAIqOQCkAAADKEYFSAAAAAKgMBEpRLQiUAgAAoNwQKAVQdARKAQAAUI4IlAIAAABAZSBQimpBoBQAAADlhkApgKIjUAoAAIByRKAUAAAAACpDJQRKX5w4wzWcPOUufPiBu/i9j9yRCxfc4Hlvu9t69M1p90RNrdt+9Li7+NGHvu22o8dc3frNfjvanrW7t+8wt7yh0Z29fs1dyqxP/9Zv3eHu7j0422b02yv8vMkr17pVjfvc+Q9u+G03nW3x/bF2Mbf37OeXO3Xlsl+Hlt144JDvn7XRfif7JZsPHXEnL19yX6oZn9Nuyjvr3L4zZ7P7tvXwUffQoNFu4FsL/fFQ37Qd9VX7p2U//9pw997pM365N9ZtyvZH+zt741a/v3O3bM8eB82fuGJNznHVvkxYvtodu3jRb8OOa7gvdqze3LjlZh8z/9f+a9tH33/fPTpkTLattqlzqemPD/94HV2BQCkAAADKDYFSAEVHoBQAAADliEApAAAAAFSGcg+U9np9tg88Hr90ydWt2+SDlY2nz/hgY+2yVdl29w8Y4RqbT/tQ5fLde30w8t0jR327MLipkOXeoN34zDrWvNfk221oOuju+tYA385CkqevXnW7Tpz061u8s8GvS0FJrce2nTRz/WYf/Nx08LBfv0KbWk7Bz8eGjfNt2hMoVUDzXKZtuF+apnanr131/Qqn2zG1QKn6cvziRX/8Zqzd6Pvxfmaaltf/Nc2mh31SsLQ+sy61VSA23BcdQzsGdqx0DA+dP++Po86bQqtadmj9Yt9Onqud5pqvXvHH3KZ1FQKlAAAAKDcESgEUHYFSAAAAlCMCpQAAAABQGco5UHpnr/4+JKkw6VfHTslOt/BoWOVy0so1Pjg5ddW6bDsFIpfu2pMTklQ7BR8VwAzbKTip6TULlvppFpIMQ6aiUOfZ69fdq9NnZ6eF7ukz1PdNlTq/0K8mO336mg0+7Dli4TL/uD2BUoUyw/6qPzuPn2g1/cGBo9zBc+f89tUPC5Qmj98LE6b7oGxyev85C3zQdu6Wbf6xKotq+TXv7c+pWqqgqNqpvR6nHaseU2f6MLCOmU2z4x+GTLsKgVIAAACUGwKlAIqOQCkAAADKEYFSAAAAAKgM+QKld/Qe5h6Yusg9s+aAe2nPZdfr2Pfdaxf/wPW/8pO8Xrv0h+6+yfOj6zSFBErzVbNUpdKw+qWCmBqy/alRE3PaKexowU0FVHccO95qGHZJbstCkvo3bFe79J1oENRYCFah0LTQqbQnUJrWLra/mm4VVC1Qao+tjdatbWjY/DAoatPznRNJHpu0Y2XD2yvkqrCrtrX54OF2DXc/+Ff/zI370V8Wha51uaP30Oi2AAAAgK5GoBRA0REoBQAAQDkiUAoAAAAAlSEWKL1v0nzX+8xvu0l//PNsCG/8j//Gjfitn7qBN/4kGiINFStQmq+Nzatbvzk1OCkKOVogM187m6cApIKQaSHJcH3h9NBLk+t8aFLLK/SpCp8vT6nLCW8WI1Aatgun2/6l7a8FR9U2XDYWKNWxmL1pq5+uCqvaJ2PHJu1Yic6PVTNViFfHZe2+ppxjkQ+BUgAAAHRnBEoBFB2BUgAAAJQjAqUAAAAAUBnCQOltr7zmejR9eDNA+od/7V498uvuiQVb3N3DJrdarqMsEHqrgdLedXPducy8YgVKNTy9hqkvRqBUNPR7n1nzfJhUoVKFMVW51LZbCYFS7YP6rO1rmPtnxkz2baatXl9woNQqvy7Z2eCrySpcWrNgaat2XYEh7wEAAFBuCJQCKDoCpQAAAChHBEoBAAAAoDJYoPQTPfu7vhd/3z/+RsMld/t3OreKY76wqGnPkPcbDxzyYcgnR07IaRcGQIsx5H24vnC6UQhTw7mHAc7be/Zzi3bs9qHSEQuX+WmVECjVkP1nr7c+R8ljk3asxI55U0uLW7//QLuGuy82AqUAAAAoNwRKARQdgVIAAACUIwKlAAAAAFAZLFBqlUmfXLIz2q7YCgmUKoyoCpnHL11yXx07JTv9/gEjXGPz6Zxw4qSVa9zFjz50U1ety7bTsOpLd+3JCWSq3YUPP3ATV6zJaVef6YemW/XMtJCkHucLlGq4+9PXrrYa1l1Dvqs6p62vduk7PmA6ZvHKbBtVRt1zqrlsAqV2jhZu35Vto31asG1nwYFS0TFX+FfHtz3D3RcbgVIAAACUGwKlAIqOQCkAAADKEYFSAAAAAKgMCtiN+eFf+H81xH2sTWewsOLWw0fd+GWrWukzc55v1+t1Vcm85kOldes2+cqkjafP+HBibaadrc9CpgptLt+91wdGFUZVuzCQqWDl3qCdtqWqpGq3oemgrzCqdmkhST3OFyhVCFaVOBVuVdVUrV/9Vv819P2z46b6dvpXj09evuznz1i70R06f94HL8slUKpjqsqiOlY6Ruqjjr2CsO0JlNq+at9KNdy9ECgFAABAuSFQCqDoHl97jZv0AAAAKDsESgEAAACgMihgN+Enf+vG/d5fuTt6d+4w9yELlCqIGBOGHV+cOMM1nDzlQ58KMx65cMENnvd2q0qXT9TUuu1Hj/swp9puO3rM1a3f3CqQeW/fYW55Q6MPqmpb+rd+6w5fIdTapIUk9ThfoFQUSp29casPUWod6ov6r/0I2w18a6Gvsqo2Cm2uatznK6qGQdFSBkoldkzHLlnpzmX6ZJVL2wqU6jwpOFzK4e6FQCkAAADKDYFSAEX35Lvfdy80/4foPAAAAKBUCJQCAAAAQGVQwE6eXZ8bLuwu5m7Z1mYAFJ3LKp2Wcrh7IVAKAACAckOgFEDRfWXvj9yzh/5FdB4AAABQKgRKAQAAAKAyWKD00wPGRudXCoUWVd1T/9o0VRzdc6rZV8Z8dMiYnPboOgPmLHRnr18v6XD3QqAUAAAA5YZAKYCie+H0f/RVSmPzAAAAgFIhUAoAAAAAlUEBu4n//O+j8yqJhpM/cemSHw5/xtqNbuKKNW7XiZN+qPa5W7ZHl0HnemlynZuz6V3XfPWK29t8OmfY/VIgUAoAAIByQ6AU3V6/2fP9rzxHLloend8daYgUDZUy/90d0fmd6Y6+Y/xN+gfm7YvOBwAAAEqFQCkAAAAAVAYF7Mb/+G+i8yrN06MmuY0HDrnzH9xwF7/3kTt28aKrXbaqpMOsVzOFSRXo3XfmrPvq2CnRNl2JQCkAAADKDYFSVJS7vjXA7Tx+wv9q8LnaadE2jw+v9QHSppYWP4TIsPlL/Jf0CctXR9t3R6UMlD44/6C/Sf+ZEXOi8wEAAIBSIVAKAAAAAJVBAbvaP/zr6DygOyFQCgAAgHJDoBQVZ9LKNf4XnHXrN0fn1yxY6t7/6MOShClDo99e4S5l+ql/Y/Pb0pHlSxUove2Vfu7rLX/uXjzzn9z/j1/WAgAAoMwQKAUAAACAyuADpb///0XnAd0JgVIAAACUGwKlqDhPjpzgTl6+5BpOnnJ39x6cM0/Dg6zd1+TOXr/mekydmTOvq1VjoPSRJSf9Dfr7Zu2MzgcAAABKiUApAAAAAFQGBezG/f7/is4DuhMCpQAAACg3BEpRcSw0evraVffS5LqceRY23XOqORs2TQYz7fGbG7e4fWfO+mqnmpZsZxTKVDhTIU2b9vSoSW7b0WPuwocf+OWPXbzoapet8n2zMKfWZdSnL9WM98tq2P7ZG7e6U1cu+3kajn914353X/8aP7+t5W/v2c8P369tatvqg/ryRE1ttn+lCJTePbre9bjxc/fVpj/OHIfXom0AAACAUiJQCgAAAACVgUApqgWBUgAAAJQbAqWoSDasfXLY+zGLV/qQ5ZR31mWnJYOi9lhBzEPnz7sNTQddr9dnFxwofWzYOHfkwgV38vJlV7duk5u4Yo2vlqrtzsz0R4HRx4fXummr1/v16V8toyDonb36u/X7D7iLmb5vPHDIjV+2yi3e2eDX39h82t0/YETe5RVYrc/0R/tuy8/dst0vvzez/OdfG+772NWB0s9PX+++ee1v3Qtnfupu730zGAsAAACUGwKlAAAAAFAZCJSiWhAoBQAAQLkhUIqK9ODAUe7guXM5w94rbLn54GFf+fPZcVOzbdMCpQqSKryZ1s4kA6UjFi7z4dHape9k2zwyZIwPpyoUek+foX5abH19Z9W7cx/ccAu27fT9temqbqqAqyqX2rTY8o9mtvPe6TNuzXv7c5bXcqp02n/OAv+4KwKln+pX6+6f3eCePfin/qb8s4f+pbu994hoWwAAAKAcECgFAAAAgMpAoBTVgkApAAAAyg2BUlSsJTsb3Nnr11yPqTP9Yxvu/t0jR30lUGuXDGbGgpr5picDpRYKXbZ7bzbMGhNb3/LMMrGh+mMB2bT+xCTbtidQeueAie6RJSfdI0ub8/rlFefclzb/mvvK3h+5b1z47/5GvHy95c/dg/MPuNt6fnzMAQAAgHJEoBQAAAAAKgOBUlQLAqUAAAAoNwRKUbEs2GlVPSetXOOrfGo4/LBdMmyZfJzWziQDpapqumjHbj9svbbXePqMm7pqnbu377Cc5WLr23zoiA+9fqlmfE5bm3f0/fd9FVI9TuuPAqezN23161GlVLUx1rY9gdL7Zu10PT74eTYgmuab137mXmz5L+65I3/mnt752z6Ees+kle62V/pF1wsAAACUGwKlAAAAAFAZCJSiWhAoBQAAQLkhUIqKpWDlnlPN3hf61fjKpApkPj68NqddMpiZFtRMm54MlBptZ8bajT5QqmCpqqUOm78kOz+2vrRAqYav33r4Zv/zBUoVZtV+qj8K0j4zZrJf17TV63PatidQCgAAAFQLAqUAAAAAUBkIlKJaECgFAABAuSFQioqmUKWCnGMWr3Snrlx2a95ratUmGcyMBTXzTU8GSh8YMNI9Nmycu73nx5U5X5gw3R2/dMk1Np929/QZ6qfF1tfRIe9fnT47s7+tg6LJtgRKAQAAgNYIlAIAAABAZSBQimpBoBRAues3e74vjDVy0XL/+POvDXfvnT7j6f/J9u0Vy0UAAEqLQCkqmoKZCmgePHfenbtx3Q+Dn2yT/ACS9oGkd91cv44whKnKoWv3NeUEShVaVYi1x9SZ2XZ39urvdhw7nvOhKbYdG6Z/wbadft02vXbZKl/l1Ibvl9jyFhRduH1XdprWo/WFbQmUAgAAAK0RKAUAAACAykCgFNWCQCmqVVv3szVKp0b+1AigsfnIT8dV+YE0YQahLRql9fwHN9yE5av941IESsk/AEDXIlCKiqYgp4aA1weMppYWd/+AEa3aJD+ApH0g0bJahz6IzN2y3U15Z53b23zaBz01zQKlvV5XldBrviJp3bpNbvyyVW7jgUPu4kcfuiU7G7Lrs/CogqZjl6x0Dw0a7fu7fv8B31bLaNnFmWW0flU3DfsfW976qA9sCrbakPsXM/sT7hMfqAAAAIDWCJQCAAAAQGUgUIpqQaAU1aocAqVat7ahbcXmVzIdV2UK9K8yCUlPjpwQXa4QBEoBoPsjUIqKN2nlGh+oTPvwkPwAku8DyYsTZ7imsy1+fQqSbjt6zNWt35wTKLV2Gp5ebbQuDbev6qJ3fWtAto3+v7yh0bc5c+2a6zn9zex0tdUyWlYf5FY37nf39a/JLptv+Sdqat32o8d9KNX6qMCpqqta5VI+UAEAAACtESgFAAAAgMpAoBTVgkApqhWB0s6l45rMOBQLgVIA6P4IlAIAAAAAqgKBUgAAAACoDARKUS0IlKJa3Wqg9OlRk3zBJRVeUgGmfWfO+mJQYZvbe/bzw7Mfu3gxp5CUCjdpvrapAGPItmNBTI1mqnVbkaeth4/6EUUHvrXQHblwwa9XhaNWNe5z9/Ydlt32bT36ugGZNr6IVWbZWB8tkKkCVtNXb8gWojp99aqbuGKNX4e1vVWFBkpV9GpFQ6MfoVV90L8azTUspJUMfKYFSgs5NzpWKrYVtrHzUaxAaVgcTOdJ52vwvLdbHddYIa9YsTH1WYXAwmNUv3WHu7v34GwbO0aTV67114SuDW1b10HyGOQcp0wbXae1y1YV5bwDQLEQKAUAAAAAVAUCpQAAAABQGQiUoloQKEW1upVA6TNjJvvwnQKCM9Zu9KHPQ+fPu9PXrrqXp9T5Ngrl1WfW+f5HH7qNBw754d0VkNS29jaf9gFI0foVJlSY89Xps90DA0b65dUfhfw0Oujy3Xt9wPPdI0f9NPVH21q8syFnergPCgYqTLjrxEnfpm7dpsxyl93R9993T42a6NtYIFNBxuOZ/VEb7Y8FVaeuWpdd361Sn9oKlN4/YIRrzByTmyHSbf5YbTp42PdLx8zaFRIoLeTcKKS6oemgX7+dmzXvNflgZbECpb1en+335/ilS/64qh+NmX5qGzo31s72XecqPM9qFx437Z+uG2sX9ln7YsFbO0YKBdu513WidYXH6bFh4/wx0jWh/qmdwq867zPXb872DwBKjUApAAAAAKAqECgFAAAAgMpAoBTVgkApqlV7A6V39urvQ4gKCn517JRsu+fHT3fNV674kJ8ePzpkjA/wrXlvf07Fx9kbt/pQYP85C7LTYkPeqz8KoyroZ9MUGtx5/ESr6Q8OHOUOnjvng4n39BnqPvfdIb6SqYKqFiCUmgVL/bIKN+qxBTKT+2Jhw6aWFh94tOm3opBA6aC5i/z+1yxcmp2mqpt7TjX7/dL+aVpbgdJCz422pyDm0l17cs7N9DUbihIoVT8UCk32w8KjCvU+PvxmldpJK9f4YGsY3lWf1LfwuKmd+hyedwsta7rOrabZMQpDpqIQ6tnr131oWY9HLFzmw6O1S9/Jtnkkc80qfGvXkU0HgFIiUAoAAAAAqAoESgEAAACgMhAoRbUgUIpydkfvYe7+qYvcM2sOuJf2XHa9jn3fvXbxD1z/Kz/Jq9/lH/vlYus0FhBUCC8fC5Q+OXKCDz9aONEoAKkKj2EVyJhkKFLSAqWxIKbaqpqpVRkNp7e17WQYMhnIDNsu3L4rJ4B4q7St5LE0+UKZkjwuyWOX7H+h50b7Fju2sXOTlDyGMc/VTnPNVz8OsIYU5lWod2j9Yv847XyqD9ZHBVR3HDvug6gKKoftkttK2wcFR8N97jur3p374IZbtntvzpD5AFBuCJQCAAAAAKoCgVIAAAAAqAwESlEtCJSiHH1x4jz3ndP/1E3645/761PG//hv3Ijf+qkb+MGfREOkofYESlXRU8OIJ72xbpMfulzBv7C9hSKTwnCmgnqzN231IUdVgwzbhYG/ZHBS8gVKk21terjt23v2cxOWr745fP1HH+Zsu5BAqfoXBh9DyWMQ66fRtlSRVf8mj+1Lk28OQS9Pj5rkq4smj224r8mwZLL/hZ6btGOYXH+MbSNfoDRfG5tXt35zm8df7dQ+Xzubp8Csrre0fQjXp8eqXrpox25/bajCqYbjV5XUe/sOy1kOAEqNQCkAAAAAoCoQKAUAAACAykCgFNWCQCnKyW2vvOYrkeqanPCTv3WvHvl198TCre7uYR8PH14sbQUEFTpU+DAZKE0LoGo4dVWUVGBPw56rrYa5f2bMZL+uaavXtwr8FTtQqqHQl+xs8EFODZ3+woTXffvhC5a6c8G+5gsqqn9pgdKHBo12Y5eszO6z/q9pyXaSth8hVdk8efmyH25dQ7err6Ih+8N9TYYlk/0v9NyUMlDau26uPwfFCpR+oV+N23fmrGtvoNRo6P0Zazf6QKmCpQpPD5u/JKcNAJQSgVIAAAAAQFUgUAoAAAAAlYFAKaoFgVKUC4VJ+5z7ob8ev9FwyQ93H2tXLG0FBBU6VPjQAqU2xPjafU0+uJlsbzRUvIaMT643FvgrdqBUIcHjlzJ9Png4p4/Jfc0XVCzmkPex/QjN3bKtoH1NHrtk/ws9N2n7Fjs3SW1dL9KeIe9VlVX7qOH6w3bqgx2TYgx5H65Pjx8YMNI9Nmycr2RrbV6YMN1fN43Np909fYZmpwNAKREoBQAAAABUBQKlAAAAAFAZCJSiWhAoRbl4ufGavxafXrE3Or/Y2goIJgOlqgK58/gJd/ra1VaBxD6z5rmvjr1ZRdXWq/CizVfIccG2nZ0eKLU+q1JnGKycuGKNDzMmA6UKEVq/5eHBo92BlnOuqaXF3T9gRHb6rSgkUKo2qtqp6p02TdtV5c1wX5NhyWSgtNBz03dWvTv3wQ1/LsLjo0qyxQiUKgCq6rTJ46p9UlhTwVCFfjVt0so1fth5DTdv7dQnVZYNj5vaqYKozmHYrj7TD01XZVdNSx4jo8fh+hRAVTXSHlNnZttYcNWOp00HgFIiUAoAAAAAqAoESgEAAACgMhAoRbUgUIpy8NDrS/112Ov496PzO0NbAcFkoFQUfFQYT5Uh52x61w+nroCehphXgFRVHxUeVCBT0zTPhhW/+L2PWgX+NDy9QoGLMsvasOxpQcxCAqUWaFRQcUtm+vTVG/zw8Xqsbdu+WiBT045cuOD7qAqaCpOqP7WZ/Qq3cSvS9iNkAU/tl46njqH+r361J1CqaYWcm7u+NcBtaDroj4cqhIZtdH6SYcyQXS9pw+r3mTnPt+v1uirUXvOh0rp1m/xxtWHlw+NqIVNte/nuvT4wqnOnduFx0/7tDdppW+qz2mlftE9qlzxGRo/D9SX7p/XpWOiY6HoMlwWAUiJQCgAAAACoCgRKAQAAAKAyEChFtSBQinLQ7/KP3bjf+6tOH+Y+ZAHB9gRK5cWJM1zDyVM+0KcQ4rGLF92E5atzhhB/oqY2G+RUu21Hj7mxS1b6apxh5dKnRk301Ti1Hq1Tw413JFCqx/f2HeZWN+6/2b/M9rX+IfWL3akrl7OVSy2QqXl16zf7eQojnr561Qcbw+qdt6qQQKkMfGuhr9yp7SvoOHfLdh9sDCuXJsOSsUCpFHJuYsdHoU/1NRnGDNn1on7EhNdJsh8K7Q6e93ar4xq7TnQ+ksdNfV7e0OiPj7alf+u37vCVWa1N8hiF05PrC/unZXT+VaXVwqkAUA4IlAIAAAAAqgKBUgAAAACoDARKUS0IlKLU7uo3yl+Dz206EZ2P4ksLZKL05m7ZVlAQFwC6OwKlAAAAAICqQKAUAAAAACoDgVJUCwKlKLUvLdrmr8HPDJkYnY/iI1BaehryfumuPf5fm6aKo3tONfuKrY8OGZPTHgCqDYFSAAAAAEBVIFAKAAAAAJWBQCmqBYFSlNqrR3/Djf0hfyvrSgRKS0/Dzp+4dMkPhz9j7UY3ccUat+vEST/8vYb9jy0DANWEQCkAAAAAoCoQKAUAAACAykCgFNWCQClKbdCH/9L1v/KT6Dx0DgKl5eHpUZPcxgOH3PkPbriL3/vIHbt40dUuW+Vu69E32h4AqgmBUgAAAABAVSBQCgAAAACVgUApqgWBUpRazT/6f923T/7j6DwAAFCdCJQCAAAAAKoCgVIAAAAAqAwESlEtCJSi1Mb+s//pXm68Fp0HAACqE4FSAAAAAEBVIFAKAAAAAJWBQCmqBYFSlNr4P/xr9/VdF6LzAABAdSJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAAAAgKpAoBQAAAAAKgOBUlQLAqUoNQKlAAAgiUApAAAAAKAqECgFAAAAgMpAoBTVgkApSo1AKQAASCJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAAAAgKpAoBQAAAAAKgOBUlQLAqUoNQKlAAAgiUApAAAAAKAqECgFAAAAgMpAoBTVgkApSo1AKQAASCJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAAAAgKpAoBQAAAAAKgOBUlQLAqUoNQKlAAAgiUApAAAAAKAqECgFAAAAgMpAoBTVgkApSo1AKQAASCJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAAAAgKpAoBQAAAAAKgOBUlQLAqUoNQKlAAAgiUApAAAAAKAqECgFAAAAgMpAoBTVgkApSo1AKQAASCJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAAAAgKpAoBQAAAAAKgOBUlQLAqUoNQKlAAAgiUApAAAAAKAqECgFAAAAgMpAoBTVgkApSo1AKQAASCJQCgAAAACoCgRKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAAAAAqgKBUgAAAACoDARKUS0IlKLUCJQCAIAkAqUAAKAi9Zs93x19/303ctHy6HwAAJIIlAIAAABAZSBQimpBoBSlRqAUAAAkESgFAACeApr7z551Fz78wF363kfu7PVrbkVDo7uvf020fakNm7/Enf/ghpuwfHV0fnvMf3eH3+c0mh9bDgBQWQiUAgAAAEBlIFCKakGgFKVGoBQAACQRKAUAAK522SofJD1y4YKbtWGLG595vOa9Jh/YPJyZ9tSoidHlys3ot1f4AKj+jc1Po8Co9lX/at+TXppcF12us33+teHuvdNnPP0/1gYAUDgCpQAAAABQGQiUoloQKEWpESgFAABJBEoBAKhy9w8Y4ZpaWtyBlnPu4cGjc+YNmLPQnfvghluysyFnernqSKC05cZ19+033orOLxUCpQBQXARKAQAAAKAyEChFtSBQilIjUAoAAJIIlAIAUOW+VDPenbx8yW0+dKTVPIVNd5046bYePuo+990hftrtPfu5ySvXulNXLvvwpobGn7tlu7vrWwNyln15Sp3bd+asu/jRh17T2RbX6/XZ2flpIU71Q/1Rv8J2des2uaPvv59dJgyP6rGm67HROkYvXuGnq9pquA1VHD197apbu6/J3dajb0GB0r6z6n24dvbGrTnTbV3Ld+/NTgv3XZVftx095p6oqc3Ot+2Nenu5n6c2arv96PFsOx2HcH9Ey2le8hyouurGA4dytgEAaI1AKQAAAABUBgKlqBYESlFqBEoBAEASgVIAAKqcVSg9ffWq6zn9zWgbo/Bl/bs3h4dXSFPDwS9vaPSByNWN+/18tetdN9cHTQ+dP++mvLPO0/81TfPUpj2B0ovf+8hvQ5U617y333117JScQKnCrI8Pr3XTVq/30/TvY8PGuXv7DnN7TjX7IOqjQ8Zkt6H+vP/Rh25o/eLsNtoKlFq10IaTp9zdvQdnp2tdOh795yzwj23fGzNtNU9B2JOXL7sjFy74PqmNtqftq52CoDqOOp7aRz2+s1d/98CAke758dN9EFf0f6tSOnP9Zh9A3XTwsF9WgV71P9wGAKA1AqUAAAAAUBkIlKJaEChFqREoBQAASQRKAQBANgSpQKMCnX1mzWtVcVSsEuiCbTuz4VH9u3TXHl+lU9U6FYZ898hRd/ziRffl0ZOyyyrwqWDn5oOH81YFTQuUKshq25QwUJpvmiqKhoFP618YMi0kUCpqZ/upx7YuBXIVzLXQqaqT6rEtN2DOQl/dtG795ux6kvukde04dtwdv3TJHytNiw15f0+foa6x+bTfxhf61fhpMn3NBr/OEQuXZacBAHIRKAUAAACAykCgFNWCQClKjUApAABIIlAKAAC8p0dN8tUxFSpVKNOqZSaHag8DlUaVPlVxU0HO52qnuearV1oNMy99Zs5zg+Yu8uHJtBBn2pD3yXaFBkqTQ9I/OXKCX78Nd69p2oaWi1F/bF09ps70wVtVHg3XtWRng39sgVsLjhoFV32Y9hfrStunhdt35ex7LFBqIVa1e3X67JzlC/XC6Z/6QFVbXrr4l+75U//ePbP/j9wT6z90989ucHcNmhJdJwBUAgKlAAAAAFAZCJSiWnxl1X5/vd/x3Zt//wW6GoFSAACQRKAUAADkUGXSfrPnu+1Hj/th1TVce49ps/w8BSJjoUujIKeFKhWaTK471FWBUg1Pr2HqD5475x4cOMqHX1WxtGbB0mwbbUPT9K+GkA8pBBuuS0Poq5Kogp1ax9nr113fWfV+vm0/TVuBUk1vK1AqCskqoKp1nrpy2a15b797eUpdTgXXfL5Yt809srS5TY+vveae2v5P3LOH/oX75rWf/SJo+nP39O4fuM+MmBNdNwCUMwKlAAAAAFAZCJSiWnxp0TZ/vX9myMTofKCzESgFAABJBEoBAEAqBUsVfLRh6hWIVIXON9ZtahW8FFXsLLdAqaiiqIacV/BTlVMVxrRh5SVtGzEaQl8hzmfHTfXrsuHuNc+2v6KhMXp8LJyatj1NLyRQKgr+9pk1z4dJ1R8Nd6/Kpcl2xXJbzwHu7tHz3RPrbriXLv8v983rP/PB1FhbAChXBEoBAAAAoDIQKEW1eHT2Gn+9f37szaIOQFcjUAoAAJIIlAIAUOUmr1zrq5CqcmdyXjLQqGHjFSjV0O/JtuapURN9wNGGmA89MGCke2TIGB9O7cpAqQ3Dr2HuVak0HO5e2hMo1b5rCP3X12zw67Lh7qX/nAW+0qlCp+EySR0JlCpIqjBsGBy9vWc/t2jHbh8qHbFwWXZ6Z/nkd4a7Z977Qx/KemjB4WgbAChHBEoBAAAAoDIQKEW1+GzNNH+9P7Ew/9+Ugc5CoBQAACQRKAUAoMop1Khwo6qQKqwYznt+/HTXfOVKdoj3QXMXuQsffuArcyrEaO1UoXP4gqV+moaF33n8RKsqoA8PHu0OtJzzVTS1rtql7/gA5JjFK7NtbEj5YgdKtT1tV32XcLh7aU+g1IbQV0BVwmW0j4cvXPD7/kTNx/uu8Oqw+UvcQ4NG+8cdCZRquHsFWpOhWAuzJve902S2/fTO33Y9bvzcfW78kngbACgzBEoBAAAAoDIQKEU1GfPDv3A9D/2D6LxyodHM9HfvkYuWR+d3BbuXob+j67H+Pr5o+y6378wZX+gi2b4zlGKbnY1Aaa7YfZlKVA7P2bR7dgCA8kegFACAKqc/gMzdst1d/OhDd+TCBTdrwxY/PPvC7bt8uFEVSXvXzfVtFTjd0HQwO7z6xBVr3Iy1G/1yxy9dcl+rnebbqb2WO3T+vB9uXhQmDdelIeNVyVTVUevWbfLrUfv3M/241UCphrTX0PYKwI5dsjIb4JRJK9f4fieDrqJtKIypf5PD1IsNVW/q1m/22479QaE2016hVR0T7ZOOkY6V1q8+qE3aPml6uO8WhNVxe3PjFt8PTVu//4A/XxsPHPL90/HT8beh+MN1dqZPfqfGfePCf3cvtvwXHzCNtQGAckKgFAAAAAAqA4FSVJPeZ3/H1Wau99u/MzQ6v9j0d34VhVDBBI3uFWujv6Hrb+lNLS2+oIQKJuhv3BOWr4627wqxQOnqxv3+b+rPjJncqn1nKMU2O1upAqV2PnWvJaT7HLM3bfXFPWLLdcR9/WvcioZGf+1rW7qXo3Dwy1Pqsm3KMVCqe0u6l2b3f5Ij/cWUw3O2UgOlOq46vjrOsfmVKu05l9Td9hvArSFQCgAA/B9BFFbcfeKk/4KpLwz6V4/DL9KiPzZpSHd9qbd2CjY+PWpSTjstt+/MWR98FP0/ua6Bby30f5Sy9axq3OeW7tqT80U4LXwZ+yKqvi1vaPR/BDhz7ZrrOf3N7Dwbij9Z2VO0Da0rTfLLk1UJtT9cJemXn01nW7L7rv8PyOyrbTdtnzQ9+UeAXq/Pzh4jBUk1LXkOtL+qmvrixBnZ5brKF2du8+GsL8zYHJ0PAOWEQCkAAAAAVAYCpagmvzxnnb/mv7b5ZHR+Z7ACDCqeEJuvUb5U/CHtb+ClkAyUdrau3l6plDpQuvXw0WxxDxUn2dt82l+bCu4m7+V0hALAKgSie1EqnKJiIHM2vevvyegei4qFqF1XBkoL2ZaKjKiISnhvq5BAaTnoykBpMbfVXQOlKsKjYjz2fNNrm54P4XNQkkV2AFQnAqUAAKAqqHrp2evXWw13jw7q0de9cPo/ui83/DA+HwDKCIFSAAAAAKgMBEpRVXr0dcN+49+4CT/5mfvciOnxNkX25MgJPjClQgXJSpAKrSm8ppGzekydmTOvlAiUdo5SB0qTx7eQCrrtpVCmCqNoeyoIEs57ePBoP8KewqaPDRtXdoFSPQd1LMJ7WwRKWyNQ2n7V8hoH4NYQKAUAAN2e/gCmyqex4e7RcY+vve5euvy/GPYeQNkjUAoAAAAAlYFAKarN50a+7mr/4P9zQ3/tX7s7XxsVbVNMFhrVSFwakSucZ2HTPaeas2HTWFhLo5ZtO3rMV3dURcljFy/6Ko9WRTEtLBcLa93es58fmlvr0Lq0Tq37iZqP/54fCz+FwTrbnvoZk+y7Aoaqzqd5Gg1MFSvVd+tfuKy2q+0nt2nru7fvMD96mkK4aq9/67fuyAnr2jGcvHKtH61N29a+aoSzUow+ZsotUCqaFh5zseHqw2M8d8t2H0BNLle3bpO/H2TrsFHnYiPYiUaEU0XeofWLc67bUW8v90FTnadzmXUlt6d1aXS6cMQ6jdYXnk+7nrYdOZYdYU/XkKsWLJkAAP/0SURBVNj1ZdKORVNLi7t/wIjstNg1mJR8zlo/tmSW1bVuI+Cdvno1e+3bsno+6jq1NrFjLTonqiQbPo/eyBx7La/57bnmb/X8qsqy/tUypq1j0xY7VjrOsfmhQp771k7Hyr9e/uI6UUVe9T18bdL+JkeJ1HI6PtamPddoPmnPQZu+5r2mnOnh8+gL/Wr89vWjhOmrN+S9niTn/SLyPGnVJrOu5HsKgK5FoBQAAHRb+tXpoLmL/JcefenSF6lYO3TMF9/Y6gNav1TzZnQ+AJQLAqUAAAAAUBkIlKIaPVy3wo3/8d+40b/zX90D096OtikmG9Y+Oez9mMUrfZhHYSeblgynqZKjQkwnL1/24S4FiBQs0nIzf7G+QgOlCgvVv7vD90UhTw25rL/lK9Ck4c9t2Vj4KQzW2f2AcOjmqavWu+OXLvmQV6/XZ/tlNPS5gkqaHvZdISbtu8Jw2r/hmeOjgJaKVahQhYW0kmE+9U/91D2I5bv3+u3qnoTWp6HVbTk7hgpc7Tpx0m938c4Gv0/JY9SVyi1QqushGXZWmLIxc4xvhgy3+WO86eBhH0wL7/toXboGdex1TNe8t999deyU7LGvXfpOtm1I4T+dT/1r163OffOVK357ei40ZqbpGg3XobCbzrudT11Pek4o7PjUqIm+jV3v6qsqjer6mbF2o3tgwEj3/PjpPlwp+n/yGtB+K0y6JHOdhNOT12BM8jlr/dAxV//UV/VDz2NN0/lQO3s+ar90HetYWxBWwUYL99k50bI6B+E50fJqV+g135Hz++LE1/3zc9rq9X5b+lfPXwu13go7VvYalabQ577+1WPtj73G2X1L7Y+dI72Grd9/IKedHS8dHwsVt+cazSftOajngX5QoOvk0SFjstO1jWTwWn09nnk9Da8n7dPUVR+/f9hrruapjdZz6Px5/xx/ecrN53gh7ykAuhaBUgAA0G3d02eo/8KhL2/6ZWPyF4Eojs+MmOMDWl94fVN0PgCUCwKlAAAAAFAZCJSiWn1+3Gw3+gf/zT8Hhn//37nnNh5zD0xd5D75rUHR9h3x4MBR7uC5c/5v6Pa3c4XANh887KvNPTtuarZtMpw2YuEyH/QJg0uPDBnjQ0IKPulv8xY4CoNjkgxrKbBk4TALq4mq9Clw1X/OAv+4rUCpTQspjJYMpSnQpKDUq9NvBkxFgTRN23HsuA91aVpa2Cq5zUkr1/h7EApAWRsL5Wm6DVVuxzAMmomCaGevX8/pT1cqp0CpjosqXOq4KVhn50JBYR3zmoUfD/tuoTddw7qWNc0ChxZotLaaHl6/+dh1q3Bj77q52ekafl+BUAX99Phz3x3ith4+6rYfPZ5zfVtQ2wLZdr0rUKdgnbWTtOeI0bq0TQ17H07vSKA02Q89v/Q8UyhRj+28LNi2M3sM9a+C1WHI16778DVA7RQG1nZU6bjQa76j51eS+9sRdqzaCpQW+tzvO6vencscYx3DsN8KXWp/rM/WLjz2ouCy1qfXRD0u9BptS9prnCRff/VcfPfI0WzI1PqgYL5C27acBUOtqq6WU3+S7RSgVhhWwVo9LuQ9xaYD6BoESgEAANAhdw6c7ANa98/eHZ0PAOWCQCkAAAAAVAYCpahmt39nqHtm7UE36rf/3D8XvD/+uRvzw7/wQ+L3v/KTNr1y4HvRdSep8qFCSRZYs+HuFRyyMJ8kw1oWfFq2e29qIYe0sFyhYa3kNtsbKFXQSvumwF8YZouJ9bWQQKmOkUKoyUp+YuEuC0wl98coQKXtaHvh9DQ99n/gJv7zv8v4+6Lwr7c/+is38MafFNVXVucOl51kx1fHJEnHWEOEx5YLJc+/zlXsWGq61ps89jFp162G+NYw3W1dt8nrJt/1nrYtsXBmGPg2yf2OSV5vaf1QJVUFyK2/+jcMjhpVpVRQVuvLd93rNWTskpXuoUGjW/XBFHrNF3p+JW1btyLfOTPtee6r36omGoY/xa4V67OCtrFjnwz/d/QaNclrNWTD26tPemzvDbomdW3mu3YXbt+VDQzbcnYsjPZD+2PLF/KeAqBrESgFAABAh3zyO8N9QOvB+Qei8wGgXBAoBQAAAIDKQKAUuBko+2zNNPf4/M3uhW1nXI+mD13vM7/t+kUCpEmFBkotxGOV76zinlXWM8mwlgKai3bs9tU/1V5DLavaXhgCTAscxcJaChDN3rTVT1eVOm3L2DZj4adk4MyoMp6CVcmqeEbDLO8+cdJX4Au3FfY1LWwVbjNtH8XmWQgseQyNHqeF5GJebrzmJvzRz9zEP/q7otDr7dgf/g/X98LvFdXTy/dE+2/s+KrKp4b2NjbEt46bDfEtT4+a5Csdal54zsLznxY41PTYsY9JO6c2PbxuNaz6hOWrbw7znXkuhP2y6yZfODFtW6IKwRr+W8/JcLqkXfeh5PWW1g+bbv3V/HA/krS+fP0OJfsQTk+ep46cX0nbVsjaxNYdSjtWoXzHwObZcz/tfNlzwPqc77xqXrI6aHLbNj1fv0O2fTv3IQt8WoVYBYr1emnvDWl9EO2PDY1v2wiPe8iWL+Q9BUDXIlAKAACADvlEr0E+oPXQwsPR+QBQLgiUAgAAAEBlIFAKdA2FhjSstKi6nQ1prCHgw3ZpYS210xDyCv8oBKSKoMPmL/Hz0gJHybCWgkTarkJHCrZqOG61mbZ6fc42Y+GnWABLQVwNLa3+aKhom240hLPWo7DUa2/W+2U1/HLT2ZacvqaFrcJtpu2jWLXAYgdKi62chrw3fijwjz7MhilV8VHhSg1/rUCbjr2o+mx4/tMCh3bsw+G0Qzo/Wof+TTunNt2uW11nqvCrkJ2utxcmvO7XMTzTP1WjtP3SNPUxFvLLd/1oyPzYc1Fi131S8npL64dNt/5qvp7Hb6zblBP0Nao4ma/foWQfwunheero+ZW0bYXU93BfNMy6znmyXdqxCuU7Bsnnftr5sueA9Tmtna41Ba+7MlAqugb1gwP98EAVRsPrMd/+a3+SgdJkcNwMmrsopxp2vvcUAF2LQCkAAAA6hEApgEpBoBQAAAAAKgOBUqDrKMSp0M6YxSv90NfJoYklGdZ6YMBI99iwcb5Co7V5YcJ0XxG0sfm0u6fP0NTAUTKspWGRNTxyMtSU3GYs/BQLYCl8pCDS+v0HcoJKRlUQtYzCZTYt1te0sFW4zWIMea/H2o62F07vKuUYKE3Om7tlW/QYJc+/2sfa2fDdNlx3OE/0HLAAXNp1a9PtulXwTdf75oOHc9aZ7Hvyeg+lbUshRIW80/obu+6TktdbWj9suvVXQ5zr9aDH1Jk57UIWcLTqleE89V3HRkHxQq/5jp5fSdvWrch3zkx7nvtp+2fXivW5o0PeJ6/Rttj27dwn2X7oOtT2w+sxrQ8SDnkfriN2LZtC3lPC9gA6H4FSAAAAdAiBUgCVgkApAAAAAFQGAqVA17Gw3cFz531lRVWjS7ZJhrUUlEqGzixgZQEjC50pmBWGN7WMlrXQk4WaFEKyNlp2wbadOduMhZ+SgTNVNz128aIfglzhJGsX0jIKzj41amJ2mobFV3ApDEelha2S21QVTQVYJ674eGhy9b8+s5ym2xDRyWNo9Fjb0fbC6V2lbCuUZo6VKiTqsdro2uxdNzfbRsPhqwpkeC7ULnYsdW3acOp9Zs7Lmffw4NHuQMu57DWTFpSz6XbdWuhQ13gYlNN1oHCq7Ve+cGLatvQcPH31avS5KMlrMCZ5vaX1w6Zbf1UxUtetnuNhuE/HW9VXbZqqSIbXt+g4qFqrnl8asr/Qa76j51fStnUr8p2zUKHPfZ1HVfrUsQmvFbvOrc/WTq99YTtVWtb6FHzW40Kv0bbkew6KnjeqHq1tJ8+1bUuvm3r9tOn2fGpqafHnUAHYncdP+PcYBUytnfSZNS+7bCHvKTYdQNcgUAoAAIAOIVAKoFIQKAUAAACAykCgFOg6FhpSGMtCQMk2ybBWr9dVVfSaDxPV/WJobAX2NEy5hgG35RS4UmBKlfUUulq0Y7dfTu0s9KTtabsaOlyhIhvuWMuF24yFn8JgnfZDVUm1blX6Sw6tbEFC65OG19a2VjQ0+j5pW2FwSYE4BeM0FL76bqHYcJt6rPZ7m0/7/tt2tR8KYG1oOugrNapd8hgaPU4LyXWFUgdKk0Nh69jpWIahYAva6bjP2fSuDx/r/zqe4bnIFzi0sLFdZ9qW1qXlNc2Gw7egXDLEZtPturXnja63LZlp01dv8EO067H6ZddpvnCirUPX35sbt2SvUT2H0p6LonVpmdiw9GOXrHQPDRrd6npL64dNt/7qetV1q+eI+qZrX88TnQ89379WO823U99UOVLHe+6W7X7beg1QmFaBSgUiC73mi3F+bR0KINoxSLYplB0TBVqTx1dsmPZCn/vZY5o5NjpG1k7LhYHS8DXM2i3OXAvaZx1rux4KvUbbYs/B8DU1yV4vw+Huxbalc6RrQ9eIAuAKk2r/FYK1tgoK63pVpVKd33D/da4VUi70PQVA1yFQCgAAgA4hUAqgUhAoBQAAAIDKQKAU6FoWGkoLFsWCYS9OnOEUFFV4SPMUvlQFPQtRif6vsJmqD1obheAU1ApDT0/U1GbDeFrftqPHfChMy1nl0lj4SeuwwFkYcIqx7SnoppCcKkBquvqk/Vd4Tv+3yqVqN2vDFt8H9cuq84XbtH7c23eYWx4EU/Vv/dYdvjqftYkdQ5ueFpLrCqUOlNr5MZqmIJmuibD9wLcW+lCb2uj46rpS0CysbJkvcCj39a/JCRAr0Lb7xEn38pSPhxhvT1hP5311435/zeoa0XU9pH6xv46scqmFE8PlQgrS2X4pTKjQnh5bNcoYrcuOV5Ltf/J6S+uHTQ+fV3reavvaD61Dx0nn5OlRk3KW1fHU/mu+2qn95JVrs1VM23PNd/T8qs96DupcnLl2zfWc/marNoWyY6K+xITXRiHPfWuXvFYUwNT+hMcnduy1nI61tWnPNZqPPQfTXvdFr4fqS3LIetuW9qNu/eZsf/W6qtfXsK2E7xd6r1G4e8Ly1TlVcAt5TwHQdQiUAgAAoEMIlAKoFARKAQAAAKAyECgFgK5RqkAp4hRcbr5yxb00+eOQK7qnEQuX5VQoLUeq/Hr2+vWc4e4lLdQKoPsgUAoAAIAOIVAKoFIQKAUAAACAykCgFAC6BoFSoHOpwuai7bvcM2MmZ6epgqeqfp6+drVsw8Pq49Jde1oNdy8ESoHuj0ApAAAAOoRAKYBKQaAUAAAAACoDgVIA6BoESoHO9fDg0a7pbItrvnrFzdn0rhu/bJXbeOCQH/p+/f4D7s5e/aPLlYr6M2juIrfmvSY/5P7cLdtbtSFQCnR/BEoBAADQIQRKAVQKAqUAAAAAUBkIlAJA1yBQCnS++/rXuBUNje7s9Wvu0vc+cqeuXHazN2711Utj7Uvpnj5DXcPJU+7Chx/4Pt/de3CrNgRKge6PQCkAAAA6hEApgEpBoBQAAAAAKgOBUgDoGgRKAQBAEoFSAAAAdAiBUgCVgkApAAAAAFQGAqUA0DUIlAIAgCQCpQAAAOgQAqUAKgWBUgAAAACoDARKAaBrECgFAABJBEoBAADQIQRKAVQKAqUAAAAAUBkIlAJA1yBQCgAAkgiUAgAAoEMIlAKoFARKAQAAAKAyECgFgK5BoBQAACQRKAUAAECHECgFUCkIlAIAAABAZSBQCgBdg0ApAABIIlAKAACADiFQCqBSECgFAAAAgMpAoBQAugaBUgAAkESgFAAAAB1CoBRApSBQCgAAAACVgUApAHQNAqUAACCJQCkAAAA6hEApgEpBoBQAAAAAKgOBUgDoGgRKAQBAEoFSAAAAdAiBUgCVgkApAAAAAFQGAqUA0DUIlAIAgCQCpQAAAOgQAqUAKgWBUgAAAACoDARKAaBrECgFAABJBEoBAADQIQRKAVQKAqUAAAAAUBkIlAJA1yBQCgAAkgiUAgAAoEMIlAKoFARKAQAAUC0+/a2B7pmxU9yr02e7QXMXudql77jxy1YB7TJm8UrXf84C12PaLPfkyAnu9p79otdbZyBQCgBdg0ApAABIIlAKAACADiFQCqBSECgFAABAd/aFfjWubt0mt//sWXfxex+5S0CRXfjwA7fz+AlXs3Cpu+tbA6LXYbEQKAWArkGgFAAAJBEoBQAAQIcQKAVQKQiUAgAAoDu6rUdfN33NBh/2U+hv29FjbvrqDa7n9DfdEzW17t6+w9wnMm1iywL5qCLpF/vVuKdHTXKvvVnvZm/a6ppaWvx1dvraVffdmfOiyxUDgVIA6BoESgEAQBKBUgAAAHQIgVIAlYJAKQAAALqbT73a363au88H/BZu3+V+eejYaDugWG7LeH78dLf18FF/3U1csSbarqMIlAJA1yBQCgAAkgiUAgAAoEMIlAKoFARKAQAA0N0s373XD28/ZN7b0flAZ/nkK6+5uVu2+VDpkPrF0TYdQaAUALoGgVIAAJBEoBQAAAAdQqAUQKUgUAoAAIDuZOBbC32Yb9rq9dH5QGdTtdIth4649z/60P3KiAnRNreKQCkAdA0CpQAAIIlAKQAAADqEQCmASkGgFAAAAN2FqkMeuXDBNTafdrf37BdtA3SF+weMcOc/uOGW7d4bnX+rCJQCQNcgUAoAAJIIlAIAAKBDCJQCqBQESgEAANBdfHfmPF+dtNfrs6Pzga40Y+1GdzFzPT4wYGR0/q0gUAoAXYNAKQAASCJQCgAAgA4hUAqgUhAoBQAAQHexoqHRHb94MToP6GoPDhzlA86j3l4enX8rCJQCQNcgUAoAAJIIlAIAAKBDCJQCqBQESgEAANBdaLj7Yg8xDnSEAs6LdzZE590KAqUA0DUIlAIAgCQCpQAAAOgQAqUAKgWBUgAAAHQHn3zlNV8NcuSiZdH5QCmoau7O4yei824FgVIA6BoESgEAQBKBUgAAAHQIgVIAlYJAKQAAALqDL7w23AdKX50+OzofKIXpaza4Q+fPR+fdCgKlANA1CJQCAIAkAqUAAADoEAKlACoFgVIAAAB0B0/U1PpA6TcmvRGdD5TCpJVr3LkPbkTn3QoCpQDQNQiUAgCAJAKlAAAAXezzrw13750+4+n/sTaVhEApgEpBoBQAAADdwVfHTvGB0ufHT4/OB0ph7JKV/rr85CuvRee3F4FSAOgaBEoBAEASgVIAAICMe/sOc8sbGl3z1Sv+j98XP/rQNZ1tcQPeWuhu69E3usytIlBaPN9+4y3XcuO6m//ujuj8mGfHTXWnrlx2e041u7t7D462SfP0qElu44FD7vwHN/x1cvb6Nbcic93c178m2h5AeSFQCgAAgO6AQCnKEYFSAKhMBEoBAEASgVIAAFD17h8wwjU2n/YhwQ1NB93EFWvcjLUb3aHz532w9K3N26LL3SoCpcVzK4HSuvWb/Q0OLaflY21ietfN9QHSc5nl1u8/4Kav3uC2HDrir5tjFy+6Z8ZMji4HoHwQKAUAAEB3QKAU5YhAKQBUJgKlAAAgiUApAACoepNWrvHB0amr1uVMv+tbA9z2o8d9NUtVtQzndQSB0uJpb6BUFUlVmfRAyzl3/NIlt2RnQ7Rd0sODR/tlFBxN3rCzoOmuEyfbXfEUQNciUAoAAIDugEApyhGBUgCoTARKAQBAEoFSAABQ9RRGTKtWWbv0HXfy8iU3eN7bbvPBw+70tavupcl1OW3WvNfkA4U9ps70jzUs+rajx9yFDz9wF7/3kQ8h1i5blR06Py1Q+uLEGa7h5KnsckcuXPDbTQ65/0RNrQ+6KgSrttqWqm62t+JmsVRSoLTvrHpfYXTyO2vd2n1N7uj777tHh4yJtg2NWbzSn5PZG7dG5yuYeu6DG379evylmvH+utl86EhOu1h/b+/Zz01eudYHl3XjRdfS3C3bfaBZ8+162XfmrF9O/dfjKe+s833Subd1ydD6xe79zLWR1legmhEoBQAAQHdAoBTliEApAFQmAqUAACCJQCkAAKh6vkLp9z5yq/buy4b4YmoWLPVBvTDA9+DAUe7guXM+CKrqlI8NG+eDoCcvX3Z16zb54fM1T+uf+YvlYoHSXq/P9kFCVc3UcgoLNmbmKzCqMKptLxyef/nuvX797x456tsRKI23CSn4qaDnkyMnuP5zFvhldV5jbUM3Q8PX3avTZ0fna106J3ZtFBooVVi4PvN/LattjM+c6+UNjf58rm7c7+fb9aIAsZZVgLh+6w5/A1GBWLv2bBu6LmLBZwAESgEAANA9EChFOSJQCgCViUApAABIIlAKAACqnkKkG5oO+j96+0Dn+s3u8eG1rdppmgJ8O44dd3f26u+n+YqXQZBwxMJlPjyqyqa23CNDxrhD58/7IOg9fYa2CpRqXQqFatu6KWTLWXhU27T+xIbnV+hw6a49BQdKb//uSHdbz/5F88nvDPMBrYffPhqdfytu710T7XtSewKlqkSqY6nKpDpmOr5NLS3+2Nv5TKNgqAKiCorG5if7UWig1B4v2LYzW4nWzqeFQu16UeBYw+vbutRO+9J89Yp7rnaan2b7lAyZAriJQCkAAAC6AwKlKEcESgGgMhEoBQAASQRKAQAAMjTs+ITlq32oU3/8tnBpOFS9qIpkGOBTMDAc7t4Cpst2700N9CUDpVqX1ql1J9uqUqmqomoYcz1WQFFDoz81amJOu9FvrygoUHpHn9E+TFVsz5/899HpHfGJVwdF9yHUnkCpKpGqEqgdS9FyOp7Pjpua0zap0ECpBUgLDZTq31g1URu2Xuc1eb3E2uk60WNbfyHD3X+xblvmOP+81XFP+ua1n7kXW/6Le+7on7mnd/2Oe2Rps7tn8js++BtbL1DOCJQCAACgOyBQinJEoBQAKtPYf/Y/3cuNV6PzAABAdSJQCgAAkKBqoHO3bPeBQFUbXdHQmK1gGQb4FBhVJcg9p5qz4VFVO120Y7evIqphyzVsvaqJ3tt3WHb9yYBgvlCkzVMF1HzBwkIDpaKh6R9++1jRPLL09M3w4Y2/dy+c/mlRPLP/J9G+J+U7diGrAqvqnariadMVBFYg2CrMpik0UGr9KDRQqvkWYI5pK1BqVVetaq72Iww45/Op/hPcI0tO+oBoPr+84pz70qZ/4L6850fu6y1/ng2afv3cf3UPLTjkPvHqwOj6gXJEoBQAAADdAYFSlCMCpQBQmWr+8X90vY7+ZnQeAACoTgRKAQAAUigkqsBfGNR8cOAod/DcOR9OfGXaLF9dMlYNUqHUGWs3+kCpgqUK+Q2bv8TPSwYE84UiNcT5ucy8YgZKi+0TvQb5gJaCqrH5nSnfsQupAqkqkYZhzVAYCo5R9diz16+7V6fPjs7vP2eBr346d8s2/7g9gVJdG2+s2+TGL1vVypMjJ+Q977J8916/b6pymgw4d4ZPvVbr7ntzl3v24J/68/7c4X/lK9/G2gLlhkApAAAAugMCpShHBEoBoDIN/t7/5b7b8rvReQAAoDoRKAUAAFXtoUGjfQBv25Fj7p4+Q1vNV1BTfwzXvzbNAnyLdza0qgb5wICR7rFh4/wQ+jbthQnT/fD5jc2n/TaSAcH2DHm/8cAhH1RU0DBsR6A0f6BUod9zH9xw87ZubxXaVAVazes7qz66rIxZvNJXq00bSn5J5loI11FooFTXUlsVRdsKlCrMqm3retR1VMhw98Xy+Wnr3MtX/9q9eOY/udv7jIq2AcoJgVIAAAB0BwRKy599/9ffBtJGO+luCJQCQGXqdfz7bsRv/TQ6DwAAVCcCpQAAoKppmHCFNPVH/j4z5+XMu61HX7d01x5feVKhPZuu0KCqhqryaLIa5M1KlrkBQW1DQ5JbIDAZELTh2BU61U0hW05DsyuEqiHNVfFU0yatXOOH09cw+tbO+kmgNN5G50fnKS2QaYFehUKT88zDg0e7Ay3n3LGLF1vdsFMVWZ3zXSdOZq+FL/SrcfvOnG21zZoFS31A2Po7aO4ifx3puglDyDr3wzNtNS15vVibsK2G8td6VDG3kOHui+kzI+e4b17/O/fswT/JXIvFuWkEdBYCpQAAAOgOShko1fdZbTv5A0rkam+g1H6YGvvbjo61jnmM/kZ05MIFN2vDlk4draQQBEoBoDI9vaLRv+Z+ZsjE6PxqkVYkopLos8D6/Qfc9qPHo/cSukJb9zMAAJWBQCkAAKh6z4yZ7P/4ruDohqaDbuKKNb4yqL506w/zmnbXtwZk21uAT38kT1aD7PX6bB8uVDi07hfDmCuwqvVYYDH2hTq5nLZvw+XXZtZh67eQqfqq6pbqq8KoalfNgdKth4+2qjwqCgn7AHDmeKWFTi3wGwZ3Y76T2ZYCmwoT648yOke6NnQuFDTVdRS21/lWVdNNBw/7vig0qmU1zfqi60rr0DSdR53PGWs3+utR18LXaqcV9AcYbUvXY2cPd5/m4UXH/DVw/5z0UC5QDgiUAgAAoDvoToHS7hpQ7YxA6c31Xc6hv0lonuhvCcm/TXQlAqUAUJnuHTPTv+Y+v7U5Or+z2HulvY8ZjQ43e9PWLv87d7kFSu0+lN1XsuOVdp9DdMxU+ELFLrR8rE1nK+R+Rrmwz1gh3avRdaB7f+F9QQCoNgRKAQAAMu7rX+PqM1/E9UVRXxizXxpT/nCxcPuu1KHKX5w4wzWcPOVDntk/gARfPtO+UIfLafu6ETB43tu+Aqm1kSdqarNhV7XddvSYq1u/OXrToSuUQ6A0+aXf6A8ChVRvVThUlUNVQTQ23zw9apIPCNsNGztXO46dcA8NGp3T9t6+w9zqxv0553PyyrX+ugr/6KPrQteHrhOtU+vWNrQtzS/kDzCqkqqwalcOd58jc40+f+o/uBdOa2ik3OsVKCcESgEAANAdECgtf/b3Cv0NoFiB0liARH8z0o+U9aNUtdnbfLpk4Q0CpQBQoTLvJUN+9c/cuB/9pbuj97B4m05g75VhsQj9nV7vZfp7uv62nrw30pm6MlA6+u0V/j1T/8bmi0bN0z0DuwdlxytfoLQcFHI/o1g6ui2da93ne+MXxWGS12Cy2AwAVBMCpQAAAO1kQ9SXqhpkzNwt29oMTXaWUgZKS01/0Jq2er0PgarC6SvTZkXbdbZJK9eUZLj70BdnbvPXwWdGzY3OB8oBgVIAAAB0BwRKy19XBUqNRmjRsvqx7ND6xdE2nY1AKQBUrkdnr/Gvu72OfT86vzOkBSQV4Nt5/IRrvnrFPVc7LWdeZyqnQKnuO6zd15RzD4pAaWvFCJTGPqvpGlSYVCPfaQS8cB4AVAsCpQAAAO2kGzbNV66UpBqkhilRxc1wuBL9QUF/WFCg8dEhY3Lad4VqDpQaVQjdfeJkSW7m6Y8bqlhb6oDzJ7897OZ1sKB6rwOUPwKlAAAA6A7KMVBq0/VvcnQPVX5a3tDoR/JQW7v5r3lJsZv6GlVm0Y7dPliiNhoJZP/Zs27AWwtzKpeF661dtsrN3bLdb1sVpkYsXObbtKefSS9PqfPf/cNRS2L9kK4OlOrvARr1pq12nYlAKQBUtl5Hf9O/9j617OYQ650tX0BS05Lvifo8sCLzPq33a3vf1nt9WEHSlqtbt8nfLwnXoeVV9dTex1X9U5Upb+/Zz8+39+ItmffdiSvWZEcUO331qn8cvtffal9spDktY2KfFZ4cOcFXH1chCZuW73iZWMDS+jHq7eV+tDs/otpHH/p7ChoNL1xenyfqt+7I7peOQXLf9X+NrKcR2fQZS+vTMPv6fKr5YR+0TWunEdaSx0jr0ueoprMtvk+i4fo1mp+1sfOy7cgx/zlN29NnI/t8FMp3bGLSAqWiCrG6VrRO2yf1TY+1L+ExLnT0wbauQdH/NdKdXX+xa0v0uVT9seOmY9hv9vycNvqsmz3nmXUdu3jRf0a2frXnmle/Jixf7ddh513rDq8hC0rrOg+vtcbMsdL1oeNkfY4tL4Vcg8k2+jffZ3gAt4ZAKQAAQIH0JX7G2o3+y6D+APDUqInRdp1JX7hOXLrk+6C+6IuUvqzrC5i+VMaW6WwESktDX5r1BV3VcvUHiDGLV0bbdSUNe//lht+NzgPKAYFSAAAAdAflHCjVzeyTly/7G826GR2GJmzYULsJrMcHz5338/S3Dj3WzeAHBozMrveZMZP930DURt99te4z127ePLa/hdgNZrvZr3k2BLyxCmDt6af1QevXdrQ9zdf2tawtoxviujlv7cVCH2khhSS7oa9lwvCMFBIolULbdRYCpQBQ2T7Rs7/rc+FH/vX3pb1XOn34+7SApN53VZ1TI3K9NLnOT1OBjcbm078I123zQ5NvOni41X0RrcvCbvpMsOa9/f5zky2v7al9uHx9ZhltM3wv1v0fBUHtflD4/tyRvrw48XX3+PBaP+qZ3jP172PDxuUECkVBUn3WeHbc1Oy0jgRKVcFc/dUPadTfNe81+X7psUbkUzt99tFnIG1j8c6GnP2auX5zdhv6zKNlFaDU/SkdJ/VVQUN9brM+KHSpwiw6RhpGXqFC9aN26Ts569LnO93jCtcV3n+z86J+6MdF+ryj86LPi/osrM90ov/bPhcqX6A0PN62T+qDpikIqc+y+kzb6/XZ/tjqs6f6b/ua/HxYyDUo+r+Oic6R2liIVkFUzde6FCbV8+PQ+fN+e6L/q92w+Ut8G11XunZ1PNUvHV+dM12Tdj4LveatXzp/dg1pH9Rmb2af7LhboNSurfBa0+dt9dn261avQfVFx0LL6tiEx0jXkc6J2gHoOAKlAAAABapZsNR/cdEXK1WkjLXpCmEFDX35S/6qsKsRKC0N/UFA515/mFG13OQfnUrh6d0/8KHS2DygHBAoBQAAQHdQzoFS2XHshP/Oqun6W4VuYOsmb3hjOrlccn2im9O6Sa2/fazauy97g1jrVHhRw5DqxrRuqlt73ezX+rQ93YjW31B0A9+WvZV+aqhTbUthgeELlmb//qLv4Qu27fT909+KFBCxZSyEkBZSSApv6CePUSFBUe0fFUoBAB11W+Y1/OX3rvvX4Ak/+Vv36pFfd08s3OruHnaz+mQx2Xtl+L6lQJkqNur9eP3+A9mg2aC5i/z7ZM3Cpdm2eu/TqF0Hz51zDw4c5adpXXpftoCetVVAU+sMw4yar+Cq1qtiIvZebMFIa2eVKhWM1OOO9kUseGc/eAnZutS3cLnY8Uqyz0LJQGmyHzquO44d9yFI+/yiY6P91OchW5/OhyqZ6nOORsfTZ6qmlpac9UuPqTP95yRty/qgx+F9tOdqp/lAqO5t6fHnvjvEbT181K8/XJfuwym4qJCkHqedF4ntb3vkC5TqmtFxUz/S9knHUQU/dBytQqtYeDT8fFjINWjnWJ8v7VzpX41aGAasdS3qvlDYlxcmTPehTTu+qs6v/ofbeyRzDhU8Vd/u6TO04Gte5177r1B0eE3qvpTaqb0e23Wd7L+q+San3+o1qLZaRtdOchtnr193r06fnV0WQMcQKAUAAECHECiF+ZUtv+6+fu6/RucB5YBAKQAAAMpej77u9u8McXf1H+PDG/eOneXumzzfPfTGcvfLb613X3p7uxvw7mF/U7b30k3xdXQiBQW07bRAqYax1E30cN4X+tX46Zof3tSWtPWJ3chXqMICoUY3jnUDXssu2XlzaF672a9p4Q3r0K30UzfkVS1JVZuS69SNbd3gDm+mS1cHSvvMnOeXVQBjaP3iaJvORqAUALqPL06Y675z+p+6SX/8c/96LON//DduxG/91A288Seu/5Wf5NXv8o/d/VMXRddt7L1S7x1Jeu8rZPjqZCBQ75XJ91ILrlkgzaaLQnx6/3po0Ojse3HyM4kqZWrY7Xzvw1JIX0y+QKnCmQpeJt/P7Xjl60csYJnWj4Xbd2X7q883mw8ejh4jG6Zfy1vQUNPCNjrGCtrq80isD2Kfs5LHNym5n2nnRdK2VajkORP9YEhVPsNKqWnbsZCsqm3aNKMgqn0uK/Qa1D6HwVGjdWhddr1YOFWfTdMKjdgPopbt3tvqc7Tp6DWfvI7TrmutJxmAtentvQYt1Cz64VjYDkBxESgFAABAhxAohXls1SX38pX/HZ0HlAMCpQAAACiVu4dO8qHQL698zw8j2+f8P3MDP/hTN/Q3/h834p/8Jzf2h//DVwKz0EY+8//gv/qbteUYKE0LCWi65idvTKctZzeVNS8ZWjB201qVOXWj3G72a1ryRra51X6mSdumhSGSIYU0dkM/FvjI1yftt8KvCj2oTTjsaFdb8w9/4vswJQgfdcTIzPNi4j//++i2AABd447eQ30w9Jk1B9w3Gi65Xse+7167+AfREGmoPYFSVRrUsNVGFcY1Xe/v4Y8/bOQ2zdP7jQnfa/VemXwvTQsDJqWF62x6+D58q30xacE70Q9lFJZL/vDFjle+zyixfU3rh6Zbf225cF9Ctny+fpu0423Tw+OrMOSE5av9EOsaHTDcpu1n2nmRtG3F2sTWbZ+xYlQFU8PZh+tIbiffObF5+hxbSD8lX3/EjrvC1vqcrB9eKeCrsKrmqZqnrUv/X7Rjtz+uCp9qGP6pq9blBLXbc83rM+fsTVv9dG031q+060PrKdY1qOUGz3vbB2/VD1VX1XPmy6MJlwLFRqAUAAAAHUKgFOaXV55337z2t9F5QDkgUAoAAIDO9EuDxrsHpy9xTy3f477RcNF9t+UHbthv/lsfiosF5m5VNQRK27qpHLKb8+EyyRvZ5lb7qYDrgLcW+mqpuqFt2w6F27QQgd0kD9cVYzfuYzfbrU9taTrb4itKhct2pbeOnPf9mPxHf+ev+Y7StT72n/3P6LYAAJXP3iuT77mi4JuCcKrEqMeqBKkfT2i4bg2JrvdN0VDY4Xut1pV8L7XPB/Z5waYn2Xtx8jOCTbd+dqQvJi14p+HyNWy+hhIPp0u+42Vi+5rWD023/tpyCufp2IcBX7EKmmn9DqUdb5tux1efrRQEVCBSQ7q/MOF135fhmWOqapa2n5oWOy+Stq2QVU8N98cqgGqdGsb+jXWbcvb1a5lzHFb+TNtOvnOiapzaj/YGSpP9CamaqbXV8dMw+3O3bPfXjJ4vCsH2mDYrZ50aIn7G2o0+UKpgqdavCqyal3Zsbbrtl8KpGtpf+6prU8Pjq8201etzroe066OY16Atq4Crnn92zLT/y3fv9ec73AaAW0egFAAAAB1CoBSGQCnKHYFSAAAAFMOn+tS4R+esdV/fed595/T/4Yb++r8uuLqoGfejv3SDPvwXvlLpt0/+Y/fKwV91L+257F5494z76vrD7ukVe92XFm1zv/zWOl/ZVMPef37sLPfZ4VPc11+f52/WvjDx9Wj/OpNu/GrbyRvPadONpmu+3Zg2acvZTWXNO3PtmlNwI822I8fcPX2G5iyTFnS4lX7qJvr6/Qd8FSSr8KQh8EXVoVQhKblNCxjYTXKbnsZu3Mdutlufbq6v9f6rKtXIRctThzztKrrRr34Wc8j7Adf/z+g8AEDlyxfGS86bu2Vb6ntk+F6r9sl2Ct6pCqpCdwps2nRRKE2BO73X23tx8jOCTS9GX0xa8E4BOQ2hrmHvw+mSPCYxseBiWj803fobDsmu4xG2C1lIUsO5h9N1jB8ZMsY9MGBktA9i0+34ajsKQOqzlJa3dsn9TDsvkratQiXPWZq07RQ65H2h16ACkQpHxs6/sWMt4XHrN3u+P26qnKvHOhePDRuX8/nwhQnT/TFvbD7tPzunHVubbufg1emzM/1qfe0lr+O067qY16COl9rr33Ca9kF9VF/D9gBuHYFSAAAAdAiBUhgCpSh3BEoBAABwK+7sN8o9NneDe/m9a27or/3rVuHQNKqyOOw3/o3rfea33dd3XnBPLtnp7p+8wN3V7+PqOrdC1Yh0s/b58dOj8zuTbvxq28kbz2nTjaZrfvJGdNpyduNe85I3pdMUssyt9FPV0WxIzeQxT9umhSEKCSmI3biP3WxPO3blhkApAKA9ksHBkK9QmnlPseCi2ijIqECjtdGQ8PvOnM15r1W72HupKjTqRyEKbNo0hfFUGfPUlcvu2XFTs+/Fyc8INt362dG+SCx4p0CdqkBKrMpivuNlYsHHtH5oethfHSNVeVQFyjCoqOrnCitq2sODR7vDFy74iu1hoE+fj5qvXPEVR2N9EJtux9eOq4KW4fYmrljjg5i2n2nnRdK2VSitMzwGadK2Y+dMIU19Prfpuh4U2gzDkYVcg6qkqjYKqIZBUK1PlVs1Tce94eSpVsFLq25rx0nrSIZTLbRp+5F2bG26nQO79hZu35Vto74v2LYz5zqOXddSzGtQAV1dH8kqvnqtSHu+Abg1BEoBAADQIQRKYQiUotwRKAUAAEAhPj1grHt8/mb3zfeuu8G/+metgqJJI//pf3avXfoD9819N9xXVje5h+tWuLuHTY6uuxiqIVBqN7w1T9Wawnlp7Ga/lkneyDa30k9VetK08Ca6Sdum3XgvJKQgduM+diM87diVGwKlAID2sPdKBQrDoa0VhNMw6EcuXPAVFtW276x6dy4zTe+Vcza969+T9X+974TvtXqvjL2XWsBP8zREuLaj93cF0+ozyyioZu/Fyc8INt3ehzval3Ad+qxjQ3krUKjK4zbMf1La8TJ9Zs6LBh/T+qHpYX/tGFmgUevU/qkCp6qzK0yqdrWZ6WqjUKPCn3XrNvlApX54o6HQY30Qm27H18KYChBuyUybvnqD2370uH8cfu5JOy/hOhScfHPjFn8Mkm3y0TrDY5AmbZ+k1+uq3nnNHwMdCwUbbXh5HStrV8g1qCqlqoCvMLX2S8dXIUs9F7R+DcWvdY1ZvNKv/0DLOb89tdt14qQ/dnb9JPtl21MbBX/VJu3Y2nQ7B+p7U0uLf17q2lCftI/qp85VsQKlhVyDOv57M23UF31HUBtVDda+qo9aR7gNALeOQCkAAAA6hEApDIFSlDsCpQAAAIj5pUHj3eP1m9xLe6+4QR/9y1aB0aQxP/wL960T/8h9aeG77lN9uv6mZTUESqVu/WY/L1n1yejGuyozWQUju9mvZYoZKLVpsUCpVeRKbtNCH4WEFMRu3Mdutqcdu3JDoBQA0B72Xqn3jpCmKfj2RE3usNcD31roqzKqjcJjCuUpGBdWC9V7Zey9VO7rX+NWN+73QTStQ1UhJ69cm60EWWi4TjraFz+8eUOjD86duXbN9Zz+pq+4mG+477TjZdTvWPAxrR+anvyccm/fYb5f2ietU/+uyDzWsbM2+tw1eN7bPuSoQKH2YdvRY+7pUZP8/Fgfwunh8dX2dE60DgUdVeV1SP1if26scmnaeTEKTtq5WL//QLRNGq0zeQxi0vbJvDhxhlPA1u9Hph86NjpG9hnVtHUNiq4NXQuapzZqq+eDHV+jip1NZ1v8cdM2FehVgDXcZtgv257WrW1ofnuueT0fLfBr51yf/XTN22fkjgZKpZBrMNnGjlHyNQNAxxAoBQAAQIcQKIUhUIpyR6AUAAAA5t4xde65TSfcwBt/0iowGjPwgz91L+4454esj62vK3XHQGly6FRRVTLdkNd8BQzCG+kWQAiHRLWb/WpfzECpLaPgqFW+0s36196cnw0wJLdpoY/kTfI0duM+drM97diVGwKlAADcGn0G0mchVWWMzQcAoKsRKAUAAECHECiFIVCKckegFAAAoLp9ZvAE9+V33nN93/+9VoHRpFH/x39xvY593z21rMF9emB5VbvpToFSG/JV8xSmPHz+fE4AU1WnNPyr5qsikkKdqq5klZYOnjvnnho10bftrECp1n/4F8FW0bZt+6oqZtWRNOSoLUOgNN6mPfQ8JFAKAAAAAF2PQCkAAAA6hEApDIFSlDsCpQAAANXnju8O80PTf/vUb7nxP/6bVsHRUP8rP3HPv3vaPTh9cXRd5aI7BUpV6XPWhi3ZUKYqkqoyadhGQ1xqqMvmqzeHltewngpfzt60NaeqaWcFSsX6kBxaU1VT9a+mrd3XlB1mlEBpvE176DlJoBQAAAAAuh6BUgAAAHQIgVIYAqUodwRKAQAAqscjb65yrxz4nhv9g//WKjgaGv79f+dDpPeMmhFdTzkqZaAUSEOgFAAAAAC6BwKlAAAA6BACpTAESlHuCJQCAAB0b/dPWei+vvt9HxJNBkeTep/9HffEgi3utlf6RddVzgiUohwRKAUAAACA7oFAKQAAADqEQCkMgVKUOwKlAAAA3dNTyxrcoA//RavQaNKIf/Kf3Nd3XXCfHzc7up5KQaAU5YhAKQAAAAB0DwRKAQAA0CEESmEIlKLcESgFAADoXhQkHfy9f9UqOJrU9/3fc7+yeKe7/dtDouupNARKUY4IlAIAAABA90CgFAAAAB1CoBSGQCnKHYFSAACA7qGQIOno3/lv7uW9V919k+ZH11HJCJSiHBEoBQAAAIDugUApAAAAOoRAKQyBUpQ7AqUAAACVrZAg6dBf+9fu6RWN7s6+I6Pr6A4IlKIcESgFAAAAgO6BQCkAAAA6hEApDIFSlDsCpQAAAJWpkCDpqN/+c/fshqPuk98aFF1Hd0KgFOWIQCkAAAAAdA8ESgEAANAhBEphCJSi3BEoBQAAqCyFBEkn/ORn7hu733e/NGh8dB3dEYFSlCMCpQAAAADQPRAoBQAAQIcQKIUhUIpyR6AUAACgMhQSJJWeh3/NfX7srOg6ujMCpShHBEoBAAAAoHsgUAoAAIAOIVAKQ6AU5Y5AKQAAQHl7YNrbrt+Vn7QKjiZ9t+UH7sHXl0bXUQ0IlKIcESgFAAAAgO6BQCkAAAA6hEApDIFSlDsCpQAAAOXptlf6ueffPdMqOJo08MafuMfrN0XXUU0IlKIcESgFAAAAgO6BQCkAAAA6hEApDIFSlDsCpQAAAOXnodeXugHX/rhVeDQ0/B/+e/flle9Fl69GBEpRjgiUAgAAAED3QKAUAAAAHUKgFIZAKcodgVIAAIDy8cleA92LO1pahUeTvrbllPtUn5roOqrVkyMn+ODeS5ProvOBUpi4Yo27mLkuY/NuhZ7/BEoBAAAAoOsRKAUAAECHECiFIVCKckegFAAAoDw8XLfCDfzgT1uFR0ND/sH/nWm3Mrp8tXto0CgfKO1dNzc6HyiFWRu2uOOXLkXn3Qq9DhAoBQAAAICuR6AUAAAAHUKgFIZAKcodgVIAAIDSuv3bQ9zXd11oFR5NeuXAR+7OviOj60Af99nvDvGB0iHz3o7OB0rh7R273Xunz0Tn3Qq9FhAoBQAAAICuR6AUAAAAHUKgFIZAKcodgVIAAIDSeWTWKjfoo3/VKjwaGvPDv3BPLt0dXR65Tly65N7cuCU6DyiFXSdOupV73ovOuxV6TSBQCgAAAABdj0ApAAAAOoRAKQyBUpQ7AqUAAABd747ew9w3Gi61Co8m9Tn/Q3fvmJnRdaC1FQ2NRa0GCXTE535RNXfU28uj82+FXhcIlAIAAABA1yNQCgAAgA4hUApDoBTljkApAABA17p/8gI35Ff/rFV4NOlrW05Fl0e6kYuW+wDfkyMnROcDXalm4VJ/PX5lzOTo/Fuh1wYCpQAAAADQ9QiUAgAAoEMIlMIQKEW5I1AKAADQdZ5YsMXV/sH/bhUeDQ35B/+3e7huZXR55Hdf/xp3/oMbbs17+6Pzga5yZ6/+7sSlS67pbIu7rUffaJtbodcIAqUAAAAA0PUIlAIAAKBDCJTCEChFuSNQCgAA0DWe23isVXg06ZUDH7k7+46MLo/CzNqwxVeF/O7MedH5QFd4c+PN6/A7b7wVnX+r9DpBoBQAAAAAuh6BUgAAAHQIgVIYAqUodwRKAQAAOtdtr7zmejR92Co8Ghrzw79wTy7dHV0e7fPpbw90+86ccaevXnW/MmJ8tA3QmXrXzfVh0pV73nO3ReZ3hF4vCJQCAAAAQNcjUAoAAIAOyQZKFxAorXYESlHuCJQCAAB0ns8MneQGffgvWwVIQwNv/Im7d8zM6PK4NQ8OHOUOnjvvzn1ww732Zr37RBGHHAfS3PWtAW7KO+t8mHTjgUPuU6/2j7brCL1mECgFAAAAgK5HoBQAAAAdckef0T6gdf+cPdH5qB4ESlHuCJQCKJZP9Ozn7hpQ4z4zfIz77JgJ7t4JU9znJ00FgKr1ywuWud7vf7+Vvld/2w343h+6od//v1yvE7/u7nyNIe47w2e/O8S9e+SoD/cdv3jRTV213r08pc7d3XtwtD1wKx4YMNJ9+4233Nwt293Z69f99bZg2073yVdei7bvKAKlAAAAAFAaBEoBAADQIZ8e8roPaH3h9Y3R+ageBEpR7giUAuiIT35rgPvi9BnusaWL3a+sfQcAcAu+tHqFe7i+3v3S8DHR11rcOlUm/faMOa7h5Ckf9DPNV6+4xtNn3OaDh30lSaA9th4+6prOtriWGzcDpHIxQ0PcPztuavRaLBYCpQAAAABQGgRKAQAA0CH3TFrpA1qfm7AsOh/Vg0Apyh2BUgC36p7xk92XVq2IhqMAALfm4fp57o7vUkGzM3yhX42vUDp2yUo3c/1mt3hng1vfdDAaGATyWbuvyS3cvsvNWLvR1Sxc6r42frr7zHe65nlLoBQAAAAASoNAKQAAADrkkaXNPqB1e59R0fmoHgRKUe4IlAJor0+82s89OOetaBDqiXeWuYfmznX31810n588LToENABUO1V2fvDN2e7RRQtTXkuXu18ayndJAK0RKAUAAACA0iBQCgAA0Ib57+7wQ3t9+423/OMXJ85wTS0tbvamra3aVqMv7/ln7oXm/zc6D9WlOwVKR7+9wg/lp39j87vS518b7t47fcbT/2NtUBgCpQDa65EF83OCT4+vWOrDo596bWi0PQAgnUL6nxk+xofxw9dW+aWho6PLAKheBEoBAAAAoDQIlAIAgIrXd1a9O/fBDT8M1209+kbbdEQyUKph45qvXnGLduxu1TZp86Ej7uTlS+5LNeOj8yvdp14b516++jful1eci85HdSlloFTPNQVAQxc+/MDtP3vWDXhrYbtfGyotUKrXGL3W6DjE5hNKvYlAKYD2uH/mrJywkyqV3tbztWhbAED7qCrpl1atyL7G6v+f6jsk2hZAdSJQCgAAAAClQaAUAABUvDXvNfngl8JUT46cEG3TEclAaUxa+Ky7B0p/Zev3XY8bP3efeq02Oh/VpdSB0rPXr7k31m1y45et8uZsetc//xQsrc08ji2XpiOB0mI/7yslUKrXSL1W6jUzNr8cECgFUKg7+w3LCZNqWPtYOwDArdNr7ROrlmdfax+cPSfaDkB1IlAKAAAAAKVBoBQAAFS0x4fXuqPvv+/2nGp2p69ddZNWrom26wgCpXH3THrHB7Oe3P6Po/NRfUodKI091+w1or1BSgKl7UegFEB38sCsN7MBp4fr66NtAAAd9+kho7Kvt3LHdwdH2wGoPgRKAQAAAKA0CJQCAICKpgDpuRvX3ZD6xT5U2nDylLu7d+4NqLRAaCz09URNrdt+9Li7+NGHvqrhtqPHfAXUcPkwNGX/V/DMhOssNFh2b99hbnlDo6+wqHXo3/qtO1rti4btnrhijTt99apvd+rKZV+Rcd+Zs6lBss5w9+h699LFv3Qvnv3P7o6+Y6NtUH3KMVCaNu/lKXX+eRM+1/X8t/mxQOntPfu5CctXu2MXL7qLmXnJ5fSaYK8DJnxeavnJK9f6563m6Xk+d8t2d9e3BmTbyMC3FvoQrLVZsrPBNbW0dEqgtNDXnqdHTfL7qn3WvusYqOqrXpNs27bPEr5mal1aZ7gNbVPbDrfRFQiUAiiEf21bszIbbtKwzLF2AIDiePTtRdnX3HsnTom2AVB9CJQCAAAAQGkQKAUAABXrzl793btHjmYDUlPeWeeDSj2m5g5JWmig9P4BI1xj82l3/oMbbvnuvT64qfUrPJUWKFUQTBUQp61e74NS+vexYeN8cCy2jRj1fW+wXQ3VrRCrglsbmg7mhM0U4NL0Ay3n/P5qSG+F0xSKSwuSFdMdfce4R5edcd+88ffu+ZP/1n2q/4RoO1SncgyUKsyosLkCmo8OGeOn9a6b618rGjOvHXoe1a3blFn2sjty4YJ//qpNMlCqcFF95jn/fua5tvHAIf88VRhUrwV6/up5LNq+Qul6Xr46fbZ7YMDInOX1PNfzW8srVKnn8+rG/X5+2Lfjly75fs1Yu9H3S30pdqBU/xby2qNjoj7oGKlPem1UeF+vjTPXb/avd2ozfMFSH/BfumuPf13U8tov7Z/Wqf0N93vXiZOtgqudjUApgELcNbAmG2x6fPnSaBsAQPF8buyE7OvuQ3PnRtsAqD4ESgEAAACgNAiUAgCAivXS5Do/zP3sjVv942fHTfUhLlXzC9sVGihVcEvBzKmr1mXbKAy1oqExNVBq7WLVDKWQQKmqrCpcpZCWTbPwmabXLLgZZFDgVVUKFSZ9ePDobNvnx093zVeuFDlQ2tfd3nuE+/TQGe6eSSvdI0ub3Vfe+wPX48bPfRjr6d0/cJ/89tDIcqhm5RYo1XNG4UYFHxds2+mfVxasVHVSzbe2A+YsdOc+uOHq1m/2j5PPaYVRtdya9z4Of4pefxTI7D9nQXZarC/2umH90DT9q/7pdUyvZwpX7jx+wj+f9by2ZR/JbPvQ+fNFD5QW+tozYuEyfwxrl76TbWd9Ugj/nj43Xwtir40Kliocu/Xw0Zz91uvq2evXfejW2nYFAqUACnHvhCnZYNP9M2dF2wAAiueOPkOyr7tfWp35/N0j3g5AdSFQCgAAAAClQaAUAABULAW5LIilxwopbT542Icuw6BYIYFSLavAkx4/OTK36mZy+WIGSlVldcex4znVE81ztdNc89UrvmKgHtt2F27fldPOgmJpQbKkF07/1Aeq2uvr5/7c/crW77tPD3sjul7g2YN/6l44c2vXV5pHl7dEt5Wk61/PwSQFI1UR06pt2vPIgqNGzz89D+15lPacToq1iz3v9XoRvl6ZofWLfdVTLf/UqIk+FK/XMQtfSiwMmmSB0nDfY2wd7Xnt6Tur3odtl+3em7eiaOy10YLwomHzw/aFeP7Uv49eF0kvXfzLTNv/4J7Z/0fuiQ0fuvvnNLi7Bk1ttT4CpQAKcX/dzGyw6bNjqMYOAF3hiZXLsq+9t39nYLQNgOpCoBQAAAAASoNAKQAAqEgWUtKQ9ApG2XRV1FO1QIW0bFohgdJ8ga3ODJTm267N09DSCnGlbcPaWRCuLV+s2+Yrjua15JR7sH6/+8KMze6z4952t/cZFV0XEHpy2z/yYb1n9v+kKL7S+Pvul4YXVhlO17+Gin9j3SY/rPr0NRv8cy/53LLnURp7HsWeb3oezt601a9XFTvD5cJ2see9poXtk7R87LVF8r1OGAuUqvKq9j9JlZePXbyYXUe+ddo8e+1RGHfRjt2+grMCuo2ZeVrfvX2H5SyX1v/B8972YVodM/VBVaS/PLqwcOkX39gaf41KeHzNVffU9n/inj30L9w3r/3sF0HTn7svN/yu+8zIOdn1ESgFUIgHZ8/Ohpo+PXhktA0AoLgeWTA/+9r7qb5Dom0AVBcCpQAAAABQGgRKAQBARbKKebFglqzd15St8FepgdIv9Kvx4bBiB0qBzlJuQ96rirGqf4ZDtdvzSEOux4KXfWbOy2lnzzeFKhVg13Nf631mzGS/rWmr17d6Xsb6omlh4DVJlZGLEShNex1IriPfOpOvPTZdw9fPWLvRB0oVLNX+DJu/JDs/rf+i9Sjwb8dB4dTlu/fm/CCgWG7r2d/dPXq+e3zddffSpb9y37zx9+7+2bv9PAKlAArx8Lx52VDTXQNrom0AAMX18Pz6j197B/DaC4BAKQAAqGzJewzlqFh91P3YRdt3uX1nzviR2GJtAFQWAqUAAKDi6IuJAqOnr1714aZkMEtD12vY6GfH3RzuuJBAqQ15f/zSJR+aCtt1ZqC0PcNOvzp9tjt7PT1slhYkA7pKuQVK9VzWcysMTfafs8BXMVYo1NrFJJ/Tac+/2HM/1heFJxWk7DF1ZnZakg15v/HAoZzp+cKfpr2B0va89jwwYKR7bNg4d3vPftk2L0yY7l8vG5tPu3v6DPXTYq+NCpKqb2EwVf9XP3U8dVxtemf45HeGu6/s/ZEPkT606CiBUgAFebg+CDURKAWALhGG+T89aES0DYDqQqAUAIDC6e+9KojQ1t+gy4H9LVt/m46FDwv5e3gliN07aIsV0wmL5nSmW+ljjPq6unG/P68qxhFr0xWeHjXJ31/RfQrtl+4F6Xmh6WG72D0cALkIlAIAgIpjIbHNBw9Hv1ApMKYvXJNWrvGPVZ1QQy2PWbwy20aBpj2nmnO+MKi9qu5NXHFzObHwamcFSiVtu/WZ9Wu6qvppmg3zf6DlnHt48Ohs2+fHT3fNV66kBsmArlJugVJRcFTPf3s90HPn8IUL/jXkiZqPw+N6zqna5kODbj63ks9pe94v3L4rZ5kF23a2eu7H+jJo7iL/fFZIMwxm6nk9PPMc1zT7o5+Cml8dOyXbJhaMTdK2tM2014HYH+EKfe1Rn5N/iLRAari+2Gvj0PrFvkpsMsA75Z11Oa+rnauvHw5fQ+B//vX1BEoBtIlAKQB0PQKlAJIIlAIAULiXJte509eu+r9VJ4silBv7W7b6un7/Af+35nB+7G/ZhUq7V1cKt9IX/S1ey+j4aFSzWJtiupU+xu4DlIPvZPql54DuZdgIdYt27PaPpXfd3GzbQu7dAtWOQCkAAKg4CkFpuOQwIBp6cOAod/DcOR8YVXBUlUpV9e/k5cuubt0mX9X00PnzPuQUfmFQsEvV9vSLNVUTVMhK4S4Fq8LgU+zLkv1qUAGrsUtWZkNp+lKiLyqxYa6tnb4Q7w22q3n60qjtbmg66Ifatu3UZuZpukKlCmTN2fSu3zcdj7QgGdBVyjFQamFMhbH1HNc0ex4duXDBvx7Yc13PQQueJv+QYoFutdHzU8tp6HeFVZN/cFmys8GvX0O8KEiqP4jpeazn8/+fvTsPk+Mqz/7/zw9jwCEGG2MbL3gJNvGCbbAxXoKRkGTLkkeStW+WZUmWrN3arX3fJY+l0b6NpBnJ2m2JBBJeyEZCIAkQAi8JSwjLCyEkEPaw+PzmPqOndbr6VE+P1D3T3fPt6/pcmq46VXXqVHXb033Pc9Rex9IxtQ/1QQHS9wx4ym+rDzX0nqFl9n6lAKxe4/k+QDufQGmh7z0PPa3qrOf6pHb6K1/1Sedqx7D32rqDDf789IFX7Bjjl6/y+wuvSam9/oM17r2NP2zyXwRKAbSIQCkAtD0CpQCSCJQCAFA4/UG/ZvXTZ9Zt+bnr+QgDpfosWoUewvWxz7ILdT4ByVJpbV/suwx9t6lgpH1XUUrnM17lGCi1e0bfYWh2tXCdFeVZt2dvZiY1AqVAywiUAgCAiqKA0+qdu/3/6Of76zwFlxRYsop6jz43xf8ipl+MFGyaW7vJzVi3MecXBlUs1P4VlNIvsqt27nKjFi1rMVCqfs3aUOu3OXDypHtgxLN+uX4p0TFjwn1e3rmn31591jr9O3HFmqxpokXVAxXU0gcDaqfwlsKqmw4cJFCKdleOgVKxKqXD5y7MLOsydpIPPuq1Lvq5W9P7hF5jWh/7ICX2/qBgeGPTazmsXKppevSa1DE37N2XmRJe7xPqi1632rfeixTMTE63Er5f6b1g/PLVbtWOXXk/QDufQKkU+t5zz+CR/lx03vbeo3Ox0Klo7MYsXu7HQ2NkFU6Tx7DzDivEtoW3P7Pch0kJlAJoCYFSAGh7BEoBJBEoBQCgMFYMQYUM+jw/y3/+qpmjwjb2+bA+n43R5+BpnyEnP3u2dvoMXN+T6fPgcBt93q3PzvVZsj4nVjt9vpzcn/q868iRnFn50vqhz6z12bV9zqzPqPV9mT6Xtu/twnPSMf6g1yA/C6GChgps2r4006HGSYVbwv3rM/DwuIV8fm7fJTy7ZHnmewEti33HYAUl9Pl48jN4BUg1lo83XTuFStWXZBvb59A5C/z3nDoHHU/fb4RjLFd2rfGVOsO+67uG8DP9sI82hio6Ee7Hqt/q/lLhHrU34fecse9o1AdNha9+qr19pxnO4pa8rvpXY66xtzYtSZspTXR/6B7QOehctCzf90kAmhEoBQAAqHD6JVi/DKcFyYC20p6BUqBF93d2dx/4HoFSAC0iUAoAbY9AKYAkAqUAABTGz6D3yik/q5/N4KcAnRVPEM2ipdm0NIOUGT53kf9uSQE+zVKVFuRMC5QqLKpAocKjFrK8re9QHxK12cEU2NSMgQrz3TesOcwX7k+BUAUBVQDG+hvrh0KQmlVLx5u2doPv/9Kt230fRi9a5tfru7Kn5i3yoUT9e0PP/j64OGDGCz7QqBCp9iUKwqrdsqZ92HFVKEKBRxWssX4UMsOXhTK1XOeqdRrPMKypdjY2mikxWUFW10eBTTtnjZuuixXNMbZPFZ1RxU2Nn8ZD4xKOl/av4zSHSFdljZdCpcn96V9dPwVZVezi+sf7ZtqoL7pGT0ye4cdU/+p+0zXTmNs4JEOa1gf1TccM+zCxafw17qLAqcZOIVK10b96HlYUbYkFm8Np7UMal3x9BZCLQCkAAEAFUTXE5FT/qgKoX+ZUSTVcDrQ1AqUodzctOUOgFECLCJQCQNsjUAogiUApAACFUchRIUCrwDl97QYfjLyj//CctiGF/MKAoUJ3ySCnpAVKFVYMA3wKRarypkKqt/cbllluU45b5ctwfwr4KdSp0GGn0RP8+lg/LBSqAKXtV9tqRrEwAJkMcYpV2FSwUs8tOKll6ofNhqgqlzqGVXdVxVAFG8NjKgCpMKSW2wxddswwZBou178WJhX9bG2M9dEqbOra6RrqWobt0o7VPGvjKffgiLH+ucLDOreaKc19FDtvBY4VPNaysI96ruOH4VsLuoZjHJvFUZIhTRs/XTtro/FT2NnG3QrmrNi+MxPs1b/NlVXPnU9LdGz1yaqltoRAKdAyAqUAAAAVQr9E6ZdH/TKnX7z1l3r6BVi/tOuXOf31ZGw7oK0QKEW5u2rcBh8ovffoz6LrAUAIlAJA2yNQCiCJQCkAoNq84ZGe7tKew9yVQya5q4dPzeuqYVPcRQ92je4npECevh8KpylXqE5hPAX6wrYhm3pdgUwLJrY2UJpsp4Cg2oV9EQUZw6nkk/uzwKmmi1dVy+T+9d2YQqdhqNGo0EoYJEwGJMX2t2bXbh+QVHhTx1dlUx3XAqQKSGq5zkPt1D52zDsHPOXqT5wLyMaOGS4f+sICX+lUYVKda9jGKMgZTslu51zX0JBVzTTtWAptFhKoTAYpk/uzYKtVabVrGla8LSRQmm/8tE8Vz7nmsSf9uekc5dY+Q7LatYaOTaAUKC4CpQAAABVE03MMmjXP/+J5pOmXPIVL9RefN9U0/+Up0J4IlKLcvWXgtOZA6bFfRNcDgBAoBYC2R6AUQBKBUgBApXtr76fd+zYccV2Of971/+zP/X/bWuO2BVuj+w0pNKqpvjXtvS2zSpQKccamDFeIT+HNZCXRZJDTlhcaKLWgoQKKMdY+uT9RuNOqpSb3b89j+5SWAqWigKTOVwFcTeGuoKOqgGq/WmcBSAudJvsQ7svW2fimHdOWb2085L/PU0A0rCpqLFSpSqA6ti1XBdSwYqrkO1YyUKmApr4/TF6TMEiZ3J+Ff62KqVVttWqsUkigNN/4JXWf8LwPsWqM9N2nCuu8+8nWhUsJlALFR6AUAAAAQFEQKEW5e3OvZ32g9L7jv4yuBwAhUAoAbY9AKYAkBWkIlAIAKtHvPz7YfajxbzPB0JpPfNc9tOvj7o7FO93NU1e5a56ZFa1KGiqkQqnChwohhmHBkCqQ3j98dNY2qjI5Y93G5qnIZ87NWpcWAmxtoFTTl2uGvSRNw64+xwKlCjKu27PX97nbc1Oy9m/HU9hw+NyFOfu1apfaT1rgUsFI9U190JhZxU2FIhWeVGVOBU5V8VTt085RruhS4wO5hQZK1e9RC5f6YKb6H7YRhYEbm9bZdUsKq4PmO1YYqFQV1b3Hjrlthw75MKjGXFSRNgxSxvanwK36o36pCqvCtwri2vpiB0pF46h+ah+6BxQutqBvrH2S+qJgtSrvxtarDzoHC/QSKAVaRqAUAAAAQFEQKEW5e+OjgwmUAmgRgVIAaHsESgEkESgFAFSiK4dMdP0+8xNfkfSetYfcpT3OfxrvligsqvCdwnHJkOUzC5f6dar2GG7Tc9J0HyZdtHlLTlgvLQSo0F0YAE1rZ1PBhwHImOT+zENPj/V9VlhTVT1t/+qnTZ8eBhtj0gKXNnX7vNrNPjjad9ocv1yhSU17P375Kt93C2SGx7zQKe81Hb3GQ8fW+ek8rY2Wa7z2nzjhRi5YknMdFc7dd/yYr6Ya7jN2rDBQqvMJn5tkkDK2v/A6KmybvJ6FBErVXn23SqdhO4VHLdypn9Ve/4brta+Dp065B0ecG6t8FBh++cxpN3bJipx1mTE+ecIHh7UsOQ4AchEoBQAAAFAUBEpR7t7Qtf/ZQOmvousBQAiUAkDbI1AKIIlAKQCg0ryt3xg36PO/dj3/7N99ldJYm2JSeC4WGhQLRGoqdU2prmW39R3qq2XuOHzY3dCzf842FgJU0E4BTFseBlf1PC1QqiDg2t17fHAvGQTsNGZCZnr9tECpjj9x5Ro/9blCjuH+FbZU1UqdcxhuvKXPYNdl7KTMsrTApY2HqoTq2ApNarnCjgo97jl6NCf8OGTOfB++HTx7fmaZ9VHLbRr4tGMml6uvCqjW1u/PXBMFK7VM0+GH52W6jpvsq4WqL3qe71jhvRCr2KljKqwbBilj+9NYqYqrzjE8T1NIoFTPdc2S2+scVSHXQrJpQVBVSU27t2PsnlRY2O4zc9fAET40rAq4FlwlUAq0jEApAAAAgKIgUIpyR6AUQCEIlAJA2yNQCiCJQCkAoJK88UM1rs+nfuj6fvrH7k2de0fbFJPCgQqLJkOdIYXyFNZToE8hQVUltanEk5UwO42e4LdRcFGBTk3nriDl1DXrfZhU27UUKBUFGNVeFS7HLV3p961KngpyTlm9zl38QJfUQKlY6FEhx3D/Wq4gpgKK2p/2q/3rOLVN7a7t3jzlvU0fr/BoOBW+aDp77VfrNB62XPvTcqs4anTsF5uOqb7bmKmN+rC4bmtm+vR8Ic/kclVG1faTV6314Uo/3k1jaxVTkyzwunFfvQ9D5jtWGMC0cdA4a5w09vpZ24ZByrT92X0QqwqrIKgCoXUHG/w9YuHjZEjTrpn6NX75aj9+S7Zs8/ekQrk6/9gYq7qq7qEwDF0Iu/dE9632ZfevQs4fDMKp6quWq5Kv2oWS9w3QUREoBQAAAFAUBEpR7giUAigEgVIAaHsESgEkESgFAFSSu1bu9//tuv7ZedH1xdZt3BQfGExWiQyFU5df0aXGBzQVHoyxcKdCkgr/qbqllis4qNCdQp6FBErlnsEjnQKpCk4qlKiqqINmzfNhUq3PFygVO7fk/i/v3NPN2lDrg4Dqm/6d3fT8yq7nPrtR/9VGxz5w8qR7YMSzmXVWWVPBUlsmqpKp9rFQZ+yYE1esyZqiPV/IM7ncgr063pPTZrvVO3f7sQgrwiYpaKnjqlJsvmMlK3o++twUHwi1fuu6Tl+7Iatyadr+VE1V1z453b3o+ZjFy/1+FIa1CqTJQKno2miqfwVGdRztc+icBZl7QZJjrLYKnt5Ukx1kLcStfYb4be14GhM91/Kwnfqq9THJcQQ6KgKlAAAAAIqCQCnKHYFSAIUgUAoAbY9AKYAkAqUAgErx+ocfc/0/81PX5djnouuBSqMKpwdPncqZ7r6tWfg4FvwUwp9A6RAoBQAAAFAUBEpR7giUAigEgVIAaHsESgEkESgFAFSK68fO9//duuaZWdH1QCVRBdIZ6zZGp7tva6oE22vKzJxp6Q3T0wOlQ6AUAAAAQFEQKEW5I1AKoBAESgGg7REoBZBEoBQAUCnuWXvIDf7ia+71Dz0aXQ9UAk3H/9j4qW7+S3V+ynhNkR9rB6BjIFAKAAAAoCgIlKLcESgFUAgCpQDQ9giUAkgiUAoAqBRdT/yTe+zMV6LrgEpxWacebsPefe7w6Vfd7A21vjporB2AjoFAKQAAAICiIFCKckegFEAhCJQCQNsjUAogiUApAKBS9P7LH7iH93wyug4AgEpEoBQAAABAURAoRbkjUAqgEARKAaDtESgFkESgFABQKfp9+n/c/XWno+sAAKhEBEoBAAAAFAWBUpQ7AqUACkGgFADaHoFSAEkESgEAlWLAP/zSvW/9keg6AAAqEYFSAAAAAEVBoBTljkApgEIQKAWAtkegFEASgVIAQKUY+I+/cu9d/3J0HQAAlYhAKQAAAICiIFCKckegFEAhCJQCQNsjUAogiUApAKBSECgFAFQbAqUAAAAAioJAKcodgVIAhSBQCgBtj0ApgCQCpQCASkGgFABQbQiUAgAAACgKAqUodwRKARSCQCkAtD0CpQCSCJQCACoFgVIAQLUhUAoAAACgKAiUotwRKAVQCAKlAND2CJQCSCJQCgCoFARKAQDVhkApAAAAgKIgUIpyR6AUQCEIlAL5/Z/P/INLPva88sfRtkChCJQCSCJQCgCoFARKAQDVhkApAAAAgKIgUIpyR6AUQCEIlAL5EShFKRAoBZBEoBQAUCkIlAIAqg2BUgAAAABFQaAU5Y5AKYBCECgF8iNQilIgUAogiUApAKBSECgFAFQbAqUAAAAAioJAKcodgVIAhaimQGmX8dPcd37wn2cjf82P//31b9yy3fuj7UOx0GBrHr/93e/c//zs5+4r3/x3d/BP/sz1mDI7epxSGLFopfvJz39+tifFf2jfOkbs2B0BgVKUAoFSAEkESgEAlYJAKQCg2hAoBQAAAFAUBEpR7giUAigEgdJmFxooTT5ee+01963v/8DN2bwjerxiIlBaWgRKUQoESgEkESgFAFQKAqUAgGpDoBQAAABAURAoRbkjUAqgEARKmxU7UGoPBUv/7p+/7B58ZkL0uMVAoLS0CJSiFAiUAkgiUAoAqBQESgEA1YZAKQAAAICiIFCKckegFEAhCJQ2K1Wg1B7f/N73SzYNPoHS0iJQilIgUAogiUApAKBSECgFAFQbAqUAAAAAioJAKcodgVIAhSBQ2iwWGvzUF77og5SFWLm3wX3y7z/nfvzTn53dOvfxT1/9urtj4NPR418IHT8ZKP3pL37hFu3Ym9PP8zFk3lL3zp4DosfuCAiUohQIlAJIIlAKAKgUBEoBANWGQCkAAACAoiBQinJHoBRAIQiUNouFBrUs1jaft3yoh9t85ES0Yqimvz/9V38T3e5CKPSZPF5HrypaTARKUQoESgEkESgFAFQKAqUAgGpDoBQAAABAURAoRbkjUAqgEARKmxUrUGpGLVvr/uO/f3R2T+ceP/rJT92zK9ZHtzlfBEpLi0ApSoFAKYAkAqUAgEpBoBQAUG0IlAIAAAAoCgKlKHcESgEUgkBps2IHSmXJznr3y//937N7O/d4+c8+GW1/vgiUlhaBUpQCgVIASQRKAQCVgkApAKDaECgFAAAAUBQESlHuCJQCKASB0malCJRq+vvP/ctXz+7t3OMr3/x3vy62zfkgUFpaBEpRCgRKASQRKAUAVAoCpQCAakOgFAAAAEBREChFuSNQCqAQBEqblSJQKluOnnK/+91rZ/fY/NBU+L2mzY22Px9tHSjd/5GPud/+7ndnj9T80PO9rxYesrxj4NPuq9/6ztmtzz1+9JOfuudWb8xpn7w+yfPT/rYeO+X+5Zvfcj//5a/ca681j7n+/ekvfuG+8K9fc6v2Nbh39hyQtd9CFDNQav388je+6c/B+qnHr379a/f9//pv96ef/qx7dsX66PYtUb+Sj/A+1pj9zRf+2f34pz/LHPs3v/2tO/rxP8/aTz7q28eb9vnDH/9P1n2g/v/nj37s17W2/7HXbniNdd12v/IR983vfd8fRw/1/7v/+UPXd9aCnP1VAgKlAJIIlAIAKgWBUgBAtSFQCgAAAKAoCJSi3BEoBVAIAqXNShUonbTuJfezX/zy7B6bH8UOe2pf2mf4KPYxQu/uN9wHNJMPBWULDfcpfNqaUGry+mhMNbaq9Nrw0Y/7EGkhD/VxzuYd0WOkKUag9MFnJrg//tSn3S//93/P7iH/Q2HJb3zn/7nJ6zZF95cmLVCqcTrzV3/rw6OxRyH3+ujla93Xv/PdrBBs2kNt1FbbxPaVlC9QOmrZWve9H/7X2aXZj1Le56VGoBRAEoFSAEClIFAKAKg2BEoBAAAAFIUCpfed+N/oOqAcECgFUAgCpc1iocFCQnYtaYuwZ1scI0lVRFVNNPkoZMwUOlWwM/lQSFVh1dg2yeuj67pm/yH3mS/934ICjuFDocqDf/JnPmQZO1ZS7N5oTaB0/OoXo+dbyEN9VRC00L7GAqWf+Ow/+qqh+cYp33XTsVXBNC2Mmu+h66TKorH9htICpbrGsfvMHqW+z0uJQCmAJAKlAIBKQaAUAFBtCJQCAAAAKIobF55y7zv8o+g6oBwQKAVQCAKlzWKhwULCkS2p1kCpqDJocjp/VQ6dsn5ztL2JjXXaVPcmuY2uqyp4tjZMag9VQ9XU/bFjJcX6W2igdPHOfTnXprUPnaNCoYVM1x8LlCanpo890u51hUk1Bf/5jrMehYx12mtXU/Pne7TFfV4qBEoBJBEoBQBUCgKlAIBqQ6AUAAAAQFG8a9Un3V37vh1dB5QDAqUACkGgtFksNJgWsmsNHVt9CB8KyQ1bsDza/ny0V6D0joFPuy99/d/OHvHc44tf+0ZqpdEX6na6X/wqe3p6hVIVTo21N7HrEz40lfxf/OPn3dwtu/x5i37+9Be/lDrNvK6DqofGjhc630BpWhVXhTO/9f0fuI2NR9z9T4/zbRUW1fT2f/W5f8q5X/TQNqoy2lKl0ligNPnQvn7+y1/5e8RoOv7Y/hQEjYVR//NHP3b7Tv+Jf82pnfqv6e3Vx9h465rr2if3b2Kv3dhDVVLDfmsq/GK+ltoSgVIASQRKAQCVgkApAKDaECgFAAAAUBR3bP2yF1sHlAMCpQAKQaC0WakCpa/+5afO7u3c49v/8QP3yLOTo+3PR3sFSkXVSFWVNHwogLjl6KmctmkBVC3TumT7UFqgVOHIz37pK+7BZyZEt5Puk2e5r3zz389ukf3IF3415xMo1fl89VvfOdv63ENhy23HX8kbDO0zY7772rfj267c2xDdxuQLlCqMqUqnncdNjW6bFAv/6tr+yd/8Xd5qqU8tXOG++58/PLvFuce/fPNbqWPdUqD0P/77R27Btj0FT/1fCQiUAkgiUAoAqBQESgEA1YZAKQAAAIAL9roPdHXvP/Ubd/Oyj0bXA+WAQCmAQhAobVaKQGnfWQt8EC75+JsvFDcs0p6BUjny8U/mTImuQGGPKbOz2ilkmqx2WcgU+RK7PjpmoVPBK+D5T1/9+tktzz0U0lQl09g25nwCpfVnPuYrr4aPQgKhRmOn4HHy8W/f/V7e8GxaoFTVWGdv3h7dJiYWiC20SqrEqrP++je/cWv2H4q2TwuU6piq2tpS4LgSESgFkESgFABQKQiUAgCqDYFSAADKxNs+/IR7af8BTz/H2qA0Jq1c4xpeOeX+6JnnoutxYbqMneR2vvyy6z11VnR9W9C11TXWtdbz193f2U1dvc5tOnDA3dJncE77Urj0ke5u0eYtbvXO3VX5Gv/9XuN8UO/to1ZF1wPlgEApgEIQKG1W7ECpQo4KOyaDlvlCdecrFigtxuPL3/hm9HhJaeHD03/1N5k2sXCt2iiMGu4rTez6tBSuTBq1bK3775/85OzW5x4KLMbam9YGStWnb37v+2dbNj90rn/66c9G26fRdPwKgoYPhVS3H3812l5igVLdc5peP9Y+je5RbRc+WjvesaBxWpg6LVCq+6oaw6RCoBRAEoFSAEClIFAKAKg2BEoBACiAhdGOfuRMlkOvvuLW79nr7hk8MrpdaxAobT8ESgt3ycPd3Nrde1z9iePuzgFPRdvc+MQAHyCta2hwV3Xr5XpOmu5fK4NmzYu2bwuxQOm82s1u77Gj7ra+Q3Pal4ICpeua3i82HTjoxyXWppLduOCke//JX7uLH6ns4A2qG4FSAIUgUNqsmIFSTbH++X/5ak6YTo9SBOTaO1AqsenRFYZUKFLrFaZMjkdrAoqx66MqoLG2+Xzkr//27NbnHrpndO/E2ktrA6WxMKbCtArVxtrnE+tvvqnjY4HSfO1jVIH0c033b/hoKcgaM2zB8pwAb9pYx167pQhflxMCpQCSCJQCACoFgVIAQLUhUAoAQAEsjLZi+043cObcjKlr1ruDp056j4waH902JhYe7WiB0mXbdvgw3801A6Pr2xKB0tYZMme+O/KRM27UomXR9TWTZ7iXz5zOhDfLQTJQWmptfbz2dkn3p9z7X/mdu2nxmeh6oFwQKAVQCAKlzS40UPrE8y/46cz//stf8ceMPRS4VPAytv2FKIdAqagiaTI0qvF4fmOdn9o+fLR2LFob6kwzYU2t++kvfnF2D80P9SXfdPCtPbaqcCYfrbmXQrH+/s/Pfu5GLlkdbR8LlLb22ANfWOz+63+yg6AKxPaaNjfaPo2CqV/6+r+d3UPzI22sY69d3dO6t5NtqwWBUgBJBEoBAJWCQCkAoNoQKAUAoAD5wmEKkipQunLHTvemh7rmrI8hUEqgtJK9q/cgf+027N3nq26G61T5c8GmOv+auH/46Kx17YlAaem87gNd3J07vupDepc8PiLaBigXBEoBFIJAabNYaLCYD/Vj9ysfiR77QpVLoFTj/63v/+Ds1s0PVZlMVqlU6DScDr8QxQqUqlLn176dOz3/zpOno+2lNcd+5NnJ7tv/kT0GuvYr9h6Mtm9JrL+qFrrl6Klo+2IESjc0HHG/+e1vz27d/PjHr/xrtG1LkmOX1ncCpQRKARAoBQBUDgKlAIBqQ6AUAIAC5AuHKUS6ZtduH7C7Z9DTPhCq6b6vf7xvpo1CdwrfabmmC09Ona/9hoHSPs/PcjsOH/ZVIBubjjt++Wo/1Xh4XE2zr30ePv2qb6f23Sc87wN91saCktrfqp27mtueOe1W79ztbqoZkGnXd9ocPyX50/MXZ5aFWtu3W/sMyTqepvhWf7VOfUqev8KlCh8qhDh++arMfmxstzY2unc82iezvMfEaX7f6rcta814jFq41F8L/axra8vDQOmAmXP9mExetda9s0d/t6Wh0e/zhp79M21GL1rmz2/skhVZx6l2Fhrdf/KEu3foqKx1FjbduK8+EzZ98vnZ/jrrX2uXdY80rdt15IgfcxtHBY21H90bto3EXosXP9DFT6evfWhf2qf2Hd7jse3CULPd48l70yT7vmTLNn9/aN2+48fc4Nnzfd+t3+G2dm+lhcYv79zTzdpQ6+9/tde/E1esyQrr2hgOnbPAza3d5I+tc6072JB5bbWH1z3Qzb1r1Sd8QO/aaXujbYByQqAUQCEIlDYrZaBUU7+fb6CwELFAqapaLtqx1687X6q6GjtePkt21rtf/u//nu1F/KHQqa5VbPs0xQqUyif//nNn93Du8epffiraVlpzbI1b8lro+mv691j7Qvzppz97dk/nHmn9LUagVPtOPl7+s09G27Yk1p/6Mx/LaUeglEApAAKlAIDKQaAUAFBtCJQCAFCAWBgtFAbTFC7UdN8KPdr6Owc85epPHHfzX6pzV3fr7e4aOMIHwUQ/K1xmYTOFNOuPH/fBymEvLHS1Tcu0vwEzzn15+dDTY33obPfRoz4cae0UpFMoz9qpv9pWbRWA0zT96oPa6blVVFVY7eCpU27EvPyB0kL6dlvfoT7Yp/DlyAVLfLtthw758OF9w0b5fWmcFGpVEO/BEWP9mFzRpcYHTxUgtX5ZODEZ9pyyep0fT42rnrdmPCxwqPOZ/9Jmd3u/YX55eAyrOru4bmsmLNtz0nS/nQKmCg6++8khbnfTeb5Yvz8rHNhR2LT2yWnvFfLVGOsa2LJkoFShXN0fe48d89dLYUyFgbWdQrpqU2igVNdi4tn73O5xhZzVJrw2sddw+LrVPffY+Kl+ezN87iJ/T+le0D2mbez+tnvN+q57Q+eucKvO74mm8dHrZca6je7GJwb4+8heR2GgVP+qnwqIzlr/YtZrNLz/bAz3nzjh1u3Z6487be0Gf07h/tpM07i/ddDMTGXSa6fuircDygyBUgCFIFDarBSBUh37L/7x86775FnRYxZLLMTYXmE8TXP+8aaxTE59bw+FTRU6jW2bTzEDpQo0Jh/5QpetObbuNV338KF7srUB2lAslKlp9Qtt29pAaWzK/mI+Yv0hUEqgFACBUgBA5SBQCgCoNgRKAQAoQL5A6VXderm6hoZMFU2rtKlwmLUZMme+D4hZyDQtWKbn2laBRtvWwqgKy+m5gm+aXl+BNoUhrZ36UVu/31feVIBNyyxAqcCdVX60qp/a3tq1pDV908/Jvik0qyCqgnK2LAzz2TKtD/ul0KLGXcE8G3ur9ir6+ULHw5brOLrOFibVttqHtdFxFm3e4oOxusYKCmqbTqMnZNq0RFODl7tYv2N0r+uet+ugZRrTZVu3+6DwHf2HZ9omA6W9psz01yEMIl/3eF8fPNa4X9apR8GBUlUC1r2pcHB4TRXsVkiz67jJ0e0kdg+GFExVBVr9a8sUktY9pSC0LdP9pWVhGDp2vNjr3t4bFBC1dhaS1XK9BrTMxjAMmYreZxQGD/tTbKpC+sYuA93v95ngrnx2nbt5yR+7ew7+wIfy3vfyT9zlw871HSh3BEoBFIJAabNYaLC1D4UoFYLTVPFbj51ydwx8OnqsYiunQKn0mDLbffc/f3i2J+ceGh+FTRU6jW2XT2tCnS2JhS6/+LVvRNvKhQZKdT/E2haqNfssRqBU+y7lg0BpMwKlAJIIlAIAKgWBUgBAtSFQCgBAAdICpdd2f9It3LwlK3RmgUcLmFrILgw2xoJlsWVilTstWBdWO7U2RlUhVanRgqthUDJspwqf+YJ0SYX2zSqKJvtmYxJuHwvzqcJjYxAC1H4U0lOQ04KLdgy7FsUYD1veb/ocf51U3fSax57MaiMKrCq4quCj2itUGoYY87l+1kEfYiq2ew78R3T5+XrHxNxxTDN97QYfvlXAVs/t2ijga8FKSQZKO4+Z6K/zzPUvZsKoSYUGStMkjxnbLl+g1ILFqqQbBjhjYq+P2PGS7TRGur91zykYa+0keV8nz8colKvjJO/pQrx91Kqma/5azj3Qkvef+o27c/u/uKvHv+Queuix6L6BckWgFEAhCJQ2i4UGP/WFL/pAW0s0nXlbhUdj1IdyCpTKqT//67M9Off4xa9+5WZt2h5t35JSB0rzhT5bc+zW7rsQBEqz21YLAqUAkgiUAgAqBYFSAEC1IVAKAEABLBymQFeSwqSzN9Rmhc40DbhVR1RQTIGxBZvqMuHDWAAttixcbsG6fIE6W2fTkFtQMhk20/K0IF1Ma/sWGycJt4+F+SxEpyn1rQKmzkVBU1U41XqNqYJ++SpPmtaMh66XrpP6adPah23M8LkLfXVNTdmuqc1jbWLe0n+qu3banrL35ppno/2PsWCoqoHquVXbtKqaJhmG1Gtl6pr1/rWj9grwalwv79wzs01rAqUKpY5dusK317UJ77nzCZSqMq2C0smqt+a+YaPc+j17/T0THiu8v2PHS76Oks+tXdjWgtRpgVI9j93ThXhj10Huuul73XUz6vPSdPaqTKpKpG+uGeNe98C5sDBQaQiUAigEgdJmsdBga4N47aXcAqVT1m92P/vFL8/2JPvxpa//23mFb1sT6mxJKQOlO0+ezpnun0Bp9iPWHwKlBEoBECgFAFQOAqUAgGpDoBQAgAJYOGzF9p1u4My5GY+Nn5oTBBMLRqqCo6pjKngWhuxiQbLYsnB5IYFSVVXU9PDtHShNjlM4Xla5Mhbms0qmqtrYbdwUv17VL63ypSqOKmwaVnQs1ngorKfp1lW9Vdvo+GEb46dBb2q768gRd1vfodE2HYWu18Z99Z6q1aoyqa6NVeI1aWFItdMU8gqUKliqoHDPSdP9ukIDpQqn6rhapmCrrom2fWreoqxjxu6T2D2oILEqz6o/A5ruWVtuFGTWfnSffvjZiX7buwaOcHUHG7JeH7HjJV9HyefWTqz6bykDpUBHRKAUQCEIlDaLhQZbG8RrL+UUKFVYVKHRtMfvfveaa/jox6Pb5tOaUGdLShkobU34s1Ar9h5s10Bpoa+hC0GglEApAAKlAIDKQaC0Y+kydpL/Lqz31FnR9QBQDQiUAgBQgFg4LB+byrquocFP154M2cWCZGnhMltuwbpiTfGeDNLl09q+hdVY08TCfKK+qTLk/Jc2Z46n8VRoUEHVNbv2uGVbt2f2X6zxUIhUAVSF99bt2euv2S19Bme1U2XK/SdP+ADk/hMn/LUNp3bviBTiVBBUVWT3HT8WvQ7JMOTV3Xr76q4XP9Al0+buQSP8dVeo97JOPfx9ofujpUDpgyPGNh0/97WZPGbsNRy7BxVoVZg07dou2bLNb6OQsy2LvT5ix0u2s/eJMCBtkvd18nyMnsfuaQBxBEoBFIJAaTMCpcWhsKhCo/keP/7pz9z41S9Gt09TzEDpy3/2ybN7OPfId61bc+xYoPRr3/6Oe3e/4dH2hYiFRP/xK/9acNvW3sfJQKkqrqryaqxtsRAoJVAKgEApAOST9v1KPjbjWCHf34T0Of6gWfN8kQ0V29DMYyqw0O25Ka3aTzVrj0CpCriomM2s9em/S+r7G10zzS6n57pv9D2Dvm/L992Wvu/RdrF77MquNX7WRn1/oX3p+5RNBw7478/CdtVM3yNp7PW6iK0HgGpAoBQAgALEwmEt0S9oCjPql6nkL+ixAFpsWbjcgnUWrkxOx61puhXGC8Or6m8sbKblrfmwodC+KYy5dvceH7pU0M/aSacxE7L6m/aBh1WATP4irHCogoOin215scYjXP7Q0wopnnSL67b6Cphapn9X79ztp95XRUpto2trFTU7qnuHNodstzYe8qFcfSiVbJMMQyokqfFV9VlrY+FKu8esQmfynlOlX72uNP56bq9NVZa1NnqtTV61ttWBUlU31YdiOw4f9oFXaxfSNgrOhmFj3Xe6/8K+xo5nr5ewnd4ndB8Nnt38gY6o/xPP3l9W2Tg5hkbPY/c0gDgCpQAKQaC0WSw02NogXnspl0CpQqIKi4aPX/zqV+4L//q1nGngWzv1fWtCnS355N9/7uwezj1e/ctPRdtKa44duxb/8d8/cr2m5c4GUKjY8f/005+Nti1GoDR2vHzjUwwESgmUAiBQCgD5pH2/ko8+l9dnzNouLJiQj74X0fckChduO3TIjVm83H92rc/QtUw/Eyptn0CpvgNTUZtYwQqxGebC6637RvdA8vuZkH3nZvdKeI/pOxRde31/p/tC32uMW7rSt9P3GbFZ3ypZ7DsdAOgoCJQCAFCAWDisJXf0H+6DZwq/hdPdi/1Cpl/anl2y3HUaPSH1FxNbrl/0bJkFHhViG7VwqQ9Y2rTh4S9s6m8sbKbl4S+CquCpoKampQ/bmdb0TVU+1Tf9daJ+kdQ+9UGFfsFU6M+qUk5fu8H3d2rTsnAqfP3iq1+A1V7hUtuvBRdFP9tyKcZ4JJdranttb0E//avno89On39t9yfdloZGTz/bdh1N+OGCPrzQhxjJNskwZPJ66R5R5U/9ZbPuC9tOP+tDqaVbt2fuI4VWtcxei/ahie4Xrbcp9NUmPGbsNRx+6KbzUFVS9UFBZh0vpNeotlEA1D4807H0l7g6Fx0rfH3Y619/qa17Rx/YxF5H+vfF+v2ZALWdp+61MNCcHEOj57F7GkAcgVIAhSBQ2iwWomttEK+9lEOgNG2q+0989h9d98mzcq7Lb3/3O7f/Ix+L7ismdn3OJ1CqSqGqGBo+VFF1y9FT0fbSmmM/8uxk9+3/+MHZVs0PhWpnb94ebd+S1va3GIHS+jMfO7vluYeu7Vs+1CPavhgIlBIoBUCgFADyaW2gVEU39L2LAob6jsUqVrZk+NyF/jNzFXAIZxyzoKk+x+7oRTekvaa81/cd4Sx9IQVG9d1FWPBG942up77jUPXS5DZi22m/4T2m71D0PY6+j9CU7+E29n1ZvmIdlSj2nQ4AdBQESgEAKEAsjNYS/YKmKdrDCpkhheq0TiExBdnSfjGx5WFoU+4ZPNJt2LvP/8KuX/70i1r3Cc9n/TVoLChpy8NfBDV9hQJtT89fnNXOXGjfVPVRUz+EHziowqMqUGq92mqacy1X/zWlffKvKvXXlGqX7IO50PFILtcxFPTThyv9pr/gzyEZHh0w4wX/S/WMdRuzjtPRWMgy7fURC0OG10vrFL7UBxgWoJTLO/d082o3Z13ToXMW+Hs3PNZNNQN89Vh9EKK2q3buarpmc3z41CqXxl7D4Ydudi+rLzF2j+s6KyC6/8QJv1z91vkrVBtWLlU7/bW2D8A29UuhcjtG8h7Wec4Kgqn6d+KKNf6etzYESoHiIFAKoBAESpsRKL0wCocqJBo+VJmz76wFfv3GxiPu17/Jngr+Rz/5qXtu9cacfcUUK1A6cslq9z8/yx6rn/3il27Supei7aW1x/6bL/zz2VbnHh/567+Ntm3JhDW17qe/+MXZvTQ/1H+dR6x9MQKlCr8qBBs+1Af1Jda+GAiUEigFQKAUAPIJP9uOrU/SZ9j6rPrxidN8qFSfzYefP8fY59lp33FZUQV9Nm4FQ9K+g4n1V8fX5+D2ubj2pc/e7bsWtdU2q3bs8p+f67N/7UffH8UKj6hQg/aVVnmzlNorUGrhT517cp2uRbLgjcZPs82pSEdagRAV+tA+9T1LeM2s6EtyRkaj73eS4dbWfPeh737m1m7y31Xq+yAV69D3SK1tJy3dW6Kf9R2ivnfSfnR/rduzNzMTosZK24bs+6XY9zX6bktjoGNpnfqn77eu7HrucyW7p5c37Vv9sbb6vinZPwBobwRKAQAoEaucmPbLFQAAaFsESgEUgkBpMwKl50+hUIVDw4fCpXtfPRe6VHXLv//yV86uPffQdPiqwhnuL6ZYgdJDH/s/OdPv657RvRNrL6099oaGI+43v/3t2ZbNj+//13+7HlOy/1iqEAqiJh//8s1vpY5ZMQKlsdeRHucbii0EgVICpQAIlAJAPq0JlNosY1boQDO8KWjXUvDSAoSqShlbr/2u2bU7qzhIoYFSq3CqttPWbvAzd2mmMhVnsFniLHynZZoRT/vQrGEKSCq4OOpsO3nHo33c1sZGV0hQthTaK1Cqc1VAOFmgxcLAGhONjS3XGGq5Zo5TgDI5u6KNo+4XzS4YXjMLUarQSriNUV/U1sZffWjN7GwKVSrQqWCl7gndG2FxjkLbFXJviWY3VF90z2hfGpO9x4754jKa2v/qbr3dXQNH+MCq6OdkXyxQGs6Ap9eLjml9q20aAwvu2j2t5bpmOqbuaYVatYzCIQDKCYFSAABKpNu4KU2/lJ/K+YUMAAC0DwKlAApBoLQZgdLzo2CjQqHJh6ZI1zT4Ydvxq190P/7pz862aH4kg6dpYtcndox8VC1VVVOTj5auc+zY+QKlCo4qQBo+FGI9/Vd/E22fJjZemu5++/FXo+2lGIFSiQVZVcl1yvrN0fb5rNzb4P7h//6L6zxuanS9ECglUAqAQCkA5NOaQKkFQ22Kc6ssqkqUybah2KxfScl+FBooVShRQUMF+ayNgoCaiczCkRa+s4CftbPp+xVmtcqoncdMdI1N+wtDpm2pvQKlooBwsjKoXbvkNdZ12H30qHtwRPMMiqr2GhbE0feZui76V9cyeW3DEGVLVBVXgc3wGutYE5v2E4ZZLZwZhkxFIVR9x6q+tqZdIfeWFQQKg6hiFV/tntc6tUm2SwZK7f6bvGpt1nhaaNVee2n3dNdxk32fxy9flVkGAO2NQCkAAEWmX87HLV3p/2JSf30X/pIBAADaD4FSAIUgUNosFho8nyBee2jPQKnCoMmp7jVd+gt1O6Ptj3z8kzkVQv/7Jz9xo5atjbY3seujcOXhP/2Er34a2yakNp/47D+e3fLco5CQZGsDpVJ/5mO+f+Hjl//7vz5cGWuf9OAzE9wXv/aNs1uee3zze9/362LbSLECpUPmLXX/9T8/ObuHc49v/8cPXJ8Z576obMninfsy96bCvArJxtoRKCVQCoBAKYDqcdEDXd3vde3nLu872l01fKq7ugWX9myecjuf1gRKFWYLp4hX4E1BwrQpz02hgdIwQFpIoNSOb+G+sJ0Coba9he+0bdhGVOlS38HdOeAp/1zHLaTqqnnDIz3cvS8ed+/fdKoo9N+sDx36tHvHiGlFdUmXJ6P9D1lAOJwtUeOhgKOCjmHb8DoobKrttL3WWSVbq2qqSpvhPaZ9FhoojVWvNbpmunY2TX8ynGkUDA3vpULaFXpvWYAzGUBWvx8bP9V1Gt38O2ahgVKFWsPXmElWzk27p2/pM9hfi3yvNQBoawRKAQAoMoVJNa3BpgMH3e39Wv7FHwAAtA0CpQAKQaC0WbUFSn/6i1+4RTv2+nXFcP/T43KOqxCowqDhQ2HRP/30Z3PaGoUh/+273zvb+tyjpanvY9dHD4VZ/+Rv/s69s+eA6HaiKqZ/9bl/ygmy6vGpL3yxxUDq+QRKdcyvfus7Z1ufeyhsu/nIieg2pvvkWe4r3/z3s1uce/z6N79xGxuPRLcxxQqUyv6PfCwnLKxHvmCo0fVQ2Fevv/Dxo5/8NBoeJlBKoBQAgVIAleuiB7u5W2ZucA/v+YR74uPfckO++Jp/TyuUql3G9hsKg4Gx9cYqMSooaNU8xSpRhlUtkxS8UwAvX8gt2Q+1DUOAsXYW0lMgL8a2TwvfifqtqpyqzqmgngJ7mvrdpltvyaOvfjk69uer5599M7r8Qr2vgKqnFgRViFLVW+2ax8YjvA52fa16plXntKqmsWur65MMdMakBTHDdRayTAuK6nl4LxXSrtB7K21fSWnnkdw+OVYhrbOAa9o9bcsJlAIoJwRKAQAAAAAdAoFSAIUgUNqs2gKlxX4kA5RpU90rbKip5cO2Sav2NeQEDRVc3HL0VLS9pAVK7fGfP/qxO/gnf+b6z16U2Ub3w77Tf+LXxR6FVEaV8wmUynOrN/oAZfKhYOvXv/Ndt3b/4UxQV6HWkYtXuY83HUuVTJMPbaN1LYVfixkoVSj2n7769bN7yX7oemmdKtGqnW2j8dd1iI25tlFINTyGIVBKoBQAgVIAlem2+Vtcv0//j38PG/APv3Rdj3/e3bOm0b17Tq1753OLohVJk4pZodSm4g5DdaGwqmWSTZWvSpWx9VaFUlOoK8ioZa0JlGra7+FzF7qBM+dm6Td9jrvmsSdTw3dilR8VpPzAU2OypvQvxOv/6DH3vvVH3H21J4pC1/tDDX/jrho6uaje2Kmw/y8Op6q3ax4bj/A6KMyp0KlVqlX7sMpr8h6zEKUqgob7NNqf7TctiClXdKnxBXlKGSht6d5K21dS2nkkt097Peq1tWJ7c9iXQCmASkOgFAAAAADQIRAoBVAIAqXNCJTmfyQDlAp/JqtXthQKNQpFqjJo8qEp1jXVemyb2PVRtc/zfbRm+vnzDZRKOOX7+T4UJv3Ml/5vVnAzTTEDpZI29X5rH1a5Ni0QS6CUQCkAAqUAKstFD3Z1f7Tvz/17V9cTX3A3TFjip7qPtS2GtABbSGE2BUb3nzjhRi5YkhOuU9AtnPI8ycJ0Vvkyud6mWg+rnxYSKLUgatp+TVr4zmiacR1/2toNrZruvhRUVfa9BVQTLRWNo8ZT11sVRmPTr0vyvhkyZ74Pn/Z5flZOJdtkWwsYp4WQFUhV1VhVjw2v8flOea/nrQ2UFnpvPTJqvGts2kYVbsPlOq/rmvp7dbfe/nmhgdILnfLelhMoBVBOCJQCAAAAADoEAqUACkGgtBmB0vyPMECp0KfCn8mHgof5pq0PxabL1+Pvv/yVaOgwdn2OfvzPo9PKt/TQPbH7lY/kHCPNhQRKRdPDq3Lr+Tx+89vftjilf6jYgVJRkPUv/vHzPhR6Pg+dg65VvuqqBEoJlAIgUAqgsjxy4K/d4C++5u5cuju6vtiSYb8YCxku27o9GgDsOm6yDxMqVJhcZ1Tl8ciZ027yqrXu4ge6ZJZf8nA3t7huqzt8+lXXc9L0zHJVrzzykTOu77Q5mWVWCTPsrwKu2q9CiGHfbukz2HUZO8kvSwvfGV+J85VTvg+tme6+FNo7UCoKaCrUmAz5hpL3jd0j2sYqnKa11f5UrVbhzU6jJ2TaybXdn3RbGhrdjsOH3Q09+/tluq90bQbPPnd/6bpOXLnGL7djFRIUbU27Qu4t9Xd7U1+T981dA0e4+uPHM9P+Fxootaqwep2Exxwwc64/V6sWm3ZP23ICpQDKCYFSAAAAAECHQKAUQCEIlDYjUJr/YQFKBRv/8Sv/enbpucfPfvFLN2X95py+5dPw0Y+73/0uO6T469/8xm1sPJLTNi3Uqf4c/tNPRKeJjz0U7Jz+4pac/edzoYFSUaVPTVmfnOo/7aHw5je+8//cjNqt0f2lKUWg1MzZvMN974f/VXCwVO0U+B29fG10fyECpQRKARAoBVA57li0w79nvWf53uj6UlAgTVU5n1m4NKfyqE3rrUCfgnVhuDNk1RPzhTEVHF20eYvfz7ZDh9yYxcs9BQcVHFU4MAzQWdXSvceOuVFNfVO4T9upcmUYTtQU67X1+33YTkFI9Xvc0pW+cmXt/gM+8JcWvjPah6pqKthngb32Ug6BUgs16rqkhYSTIVFReFJjqHtB90S+trf1Heqnk1f4NLxuaqdl4XT4CmC+2HSNtVwVPNVW2+iaK4yse0vtCg2KFtqukHtL7SzsqeqhCr3qft199Kg/P52n2ihEq3CuXmvPLlmeCdIm+6J29jpR6FbHVOVc9Ut9UZ/ULu2etuUESgGUEwKlAAAAAIAOgUApgEJUU6AU1aulUKeqaOr51779HffzX/4qE3rUvz/9xS/cF/71az5YnK9KZltQP7ceO+W+/I1v+sBkGM781a9/7b7/X//tp4Z/dsX66PblQH1TH9VX9dkeOheN/bf/4wfu5Cf/yvWdtSC6PZoRKAWQRKAUQCW4+IM9fJjwsdNfyQpWlpoCaQq0xSjE1vnZiW71zt0+pPau3oOi+xAF/VqaLl6VSQfNmueDdgorKjSnIJ6Oo7Bq8rwffW6Kr3qpvihMOLd2k5uxbmNOOPHyzj3drA21/vhqq39nNz2/smvz7+Fp4bvQlNXrWux/WyiHQKkFbPNd81hIVEFMXUurypmvrej66DrZddM1Xr9nr7tvWO4U+7FrPHHFmqwAc6FB0ULbSUv3lui+7T7h+Uw4Wvf0qp273K19hmTayENPj83czwqNalmsLwrIKtisQLXWaVzm1W7OOmbaPW3LCZQCKCcESgEAAAAAHQKBUgCFIFCKSlCMKqFAOSFQCiCJQCmASnDnkl3+/eqKgefCbB3BTTUDfOVFhUtVibE9ppu36pHtPd29lEOgFACAYiJQCgAAAADoEAiUAigEgVJUAgKlqDYESgEkESgFUAke/+jXO+x7lUKccza+5CtOqopprE0p3TVwhKs/frzdp7sXAqUAgGpDoBQAAAAA0CEQKAVQCAKlqAQESlFtCJQCSCJQCqDcabr7wV98zd296kB0PUpD07mPXLDET1Wuqchv6TM42q4tESgFAFQbAqUAAAAAgA6BQCmAQhAoRSUgUIpqQ6AUQBKBUgDl7ppnZvn3qmtHzY6uR2nUTJ7hp9pXmPSRUeOjbdoagVIAQLUhUAoAAAAA6BAIlAIoBIFSVAICpag2BEoBJBEoBVDu3jVjvX+vessTw6Pr0XEQKAUAVBsCpQAAAACADoFAKYBCEChFJSBQimpDoBRAEoFSAOXuD+du9u9Vl3TpE12PjoNAKQCg2hAoBQAAAAB0CARKARSCQCkqAYFSVBsCpQCSCJQCKHe3L9ru36te//Bj0fXoOAiUAgCqDYFSAAAAAECHQKAUQCEIlKISEChFtSFQCiCJQCmAcnfHoh0ESuERKAUAVBsCpQAAAACADoFAKYBCEChFJSBQimpDoBRAEoFSAOWOQCkMgVIAQLUhUAoAAAAA6BAIlAIoBIFSVAICpag2BEoBJBEoBVDuCJTCECgFAFQbAqUAAAAAgA6BQCmAQhAoBYC2R6AUQBKBUgDljkApDIFSAEC1IVAKAAAAAOgQCJQCKASBUgBoewRKASQRKAVQ7giUwhAoBQBUGwKlAAAAAIAOgUApgEIQKAWAtkegFEASgVIA5Y5AKQyBUgBAtSFQCgAAAADoEAiUAigEgVIAaHsESgEkESgFUO4IlMIQKAUAVBsCpQAAAACADoFAKYBCECgFgLZHoBRAEoFSAOWOQCkMgVIAQLUhUAoAAAAA6BAIlAIoBIFSAGh7BEoBJBEoBVDuCJTCECgFAFQbAqUAAAAAgA6BQCmAQhAoBYC2R6AUQBKBUgDljkApDIFSAEC1IVAKAAAAAOgQCJQCKASBUgBoewRKASQRKAVQ7giUwhAoBQBUGwKlAAAAAIAOgUApgEIQKAWAtkegFEASgVIA5Y5AKQyBUgBAtSFQCgAAAADoEAiUAigEgVIAaHsESgEkESgFUO4IlMIQKAUAVBsCpQAAAACADoFAKYBCECgFgLZHoBRAEoFSAOWOQCkMgVIAQLUhUAoAAAAA6BAIlAIoBIFSAGh7BEoBJBEoBVDuCJTCECgFAFQbAqUAAAAAgA6BQCmAQhAoBYC2R6AUQBKBUgDljkApDIFSAEC1IVAKAAAAAOgQCJQCKASBUgBoewRKASQRKAVQ7giUwhAoBQBUGwKlAAAAAIAOgUApgEIQKAWAtkegFEASgVIA5Y5AKQyBUgBAtSFQCgAAAADoEAiUAigEgVIAaHsESgEkESgFUO4IlMIQKAUAVBsCpQAAAACADoFAKYBCECgFgLZHoBRAEoFSAOWOQCkMgVIAQLUhUAoAAAAA6BAIlAIoBIFSAGh7BEoBJBEoBVDuCJTCECgFAFQbAqUAAAAAgA6BQCmAQhAoBYC2R6AUQBKBUgDljkApDIFSAEC1IVAKAAAu2KWPdHeLNm9xq3fudm/78BPRNgAAtDcCpQAKQaAUANoegVIASQRKAZQ7AqUwBEoBANWGQCkAAGXo8s493awNta7+xHF39CNn3JEzp13dwQbX7bkp7nX3d45u054UKF23Z6/bdOCgu6rbhX3xc3PNQLf32FF/3jFapzaxbQEAyIdAKYBChKGmSx4l1AQAbSHrvZdAKYAmBEoBlDsCpTAESgEA1YZAKQAAZUaBzNr6/e7Qq6+4xXVb3eDZ893IBUvctkOHfLD0uWWrotuVG1UqfWn/Aa81VUstUKpw6sCZc3P0mjLTB1hj25bak8/P9qFW/RtbDwAobwRKARTiHePGZUJNb+7xZLQNAKC4rp86OfPe+8YP94y2AdCxECgFUO4IlMIQKAUAVBsCpQAAlJkhc+b74OjwuQuzll/ycDc/pfy+48fcHf2HZ60rRxcaKF22bUd0fXsiUAoAlY1AKYBCXDVqTCbUdGnv/tE2AIDiumHmtMx778UfJJQBgEApgPJHoBSGQCkAoNoQKAUAoMxMWrnGNbxyyv3RM8/lrBsw4wUftnxs/NTMslv7DHGrdu5yh0+/6oOoqux5z+CRWdupoufEFWvcwVMnfSBS/05buyFT6TMtxKk+qC/qU9hu1Y5dfkp+HVPbJMOjWmZT1BvtY9nW7W7/yRPu3qGjso4z/6U636f7h48uKFCqQK2CtQs21bnX3d85s/z6x/u6nS+/7Nbs2u3e9FBXvyx57tpOVV9tOzve8qbjabnWq93+Eycy7WwcwvPRNtpW+8i6Bk3rdh054gbMnJvVNwBA+yNQCqAQb39qZCbU9LYhT0XbAACK53Uf+LD7g/nN77ty0QNdou0AdCwESgGUOwKlMARKAQDVhkApAABlxlco/cgZN/fFTb4qaayNua3vUB9e3HH4sJ8Wf9gLC/3U+Apt3jesObSpfWjqfIUdFQLVtPEWBtVyrW9toFTB1foTx317HTcZKL26W29318ARru5gg6eftbxm8gz3ctO2oxYtyxzjHY/2cVsbG92Gvft8+LOQQKmCmgqnKjyqEKkt7zpusjv06it+HPTczl3noACtzn1p03bq/+izfbDjqY32N2rhUn9OGlMt0xhoPzc+McA9NW+RD5Pq3xt69ncXP9DF/6u2e48d89sqhKpz0TW0YwAAygOBUgCFeEvfQZlQ07UTJkTbAACK5/e698687944e0a0DYCOh0ApgHJHoBSGQCkAoNoQKAUAoMxYCFLBxd1Hj/rwpcKMyXaqwLlkyzbf5vZ+wzLLFd6sP37cV/3U885jJrrGV1/JhELN2CUrMoHJ1gZKFWJVmNXaJQOlact0HskKotY/C5kWEiiVvtPm+FBsj4nTMsumr93gK4yqgqmeq6KrAqYKeVobje/qnbszYdS0c7Jw6vjlqzLLYlPe95oy04dHdSxbdl3TfhXsra3f7y7r1COzHADQvgiUAijEGzo9ngk23Tx/jnt90/8/xtoBAIrjyqefybzvXjVqTLQNgI6HQCmAckegFIZAKQCg2hAoBQCgDKny5aBZ83xY1KZY18/hNOrv6j3IByEtOGpU5VMVMi3IOWv9i9Fp5q957EnXb/ocv5/WBkqT7QoNlIr6q+qmdw5onj5U+7bp7vXcjmHnHbIArNpZOFXnp+d2vJU7dvqwaloVU1F4taUw7S19BvtwahjEjQVKLRA7s6kfGntbXqg3dR3srpu+1103oz6va6fuclc+u85dPmy+e3PNGPe6DzQHcgEAhSNQCqBQ73x+aibcdPng5j9WAgAU30UPdnE3z52dec99c48+0XYAOh4CpQDKHYFSmAF//3P3vg1HousAAKhEBEoBAChzCk6OX77ahx5VCXP2hlofmLSwZzJ0aSzIqaCktlVwMrZ/actAqSqKatp7TUtv4deN++ozYUw7xqYDB/0U9SEFYBWEVTsFRhdsqvPT5WvafAVSFZwdMqe5GqkdPzY20lKg1Ja3FChVxdOpa9b7afRVMbW26ZjD5y50l3fumWmTz9tHr3L3v/qaDzi1xvtP/cbdse0r7urxL7mLHuQDKwAoBIFSAIV6y5MDM+Gmm+bOcm/4UOv/cAgA0LIrR47KvN/eMHN6tA2AjolAKYByR6AUps9f/af7wJaPRNcBAFCJCJQCAFAhFLhU6NGCkBb2XLF9Z07wUh4bP9UHT8stUKrwp0KgqiT6gafG+BCopt+39WnHiKmZPMMdPHXKVwlV1dFwuns7vqayV8AzOT4WTk07ni1vKVBqFPwduWCJD5QqWKqqqz0nFe/LsNc90M29sesg9/t9JvhKpTcv+WN3z8Ef+GDU+17+ibts6Lkp9wEAcQRKAbTGTXNmZkJO1z8/1V38wUej7QAA5+fyQcMy77NyWf8h0XYAOiYCpQDKHYFSmB5/+m/ug/V/EV0HAEAlIlAKAEAZUcBR1TpX7djlLuvUI2d9GGjUlPGaOl5VOm0a/Jgpq9dlTTFvVFlTIUgFVdsyUCqapl7hz2lrN2RNdy+tCZTatPdTm85xza7dmenutU7/apnWq11yW3OhgdKru/V2N/Ts7y5+oEtm2d2DRrjdR4+62vr90etYNE3X/bLBs9ydO77qVOX0msnb4u0AAB6BUgCtcWmv/llBJ03JfNmAIX565lh7AEBhLnm0xl0zfnzWe+z1U6dE2wLouAiUAih3BEphHjnw1+7xj349ug4AgEpEoBQAgDKiEOSSLdt8iLPT6AlZ6xQanbFuozv06iuu67jJPgi6dvceX+HzwRFjs9p2GjPB3d5vmP9ZlUpVMTOsAiqq2tnYdBxV97yiS42fYj4Z/lQFUE1PX+xAqY6pY6tf4XT30ppAqcx/qc7tP3HC76/vtDlZ61QxVFPR69zD0O0tfQa7LmMn+WUXGijV8ZOhWAuzxs69FFS99F2rP+lDUleMXB5tAwAgUAqg9d7+1MiswJPcPG+2u3rMs+6t/Qe7S7rVuIsf4ctDAEjz+oe7ujd+uId78xN93RVPPe2uf35KzvvqjbNn8F4KIAeBUgDljkApzJ1Ld7vBX3yNewFFo+9NF23e4lbv3N0m3zMCQBKBUgAAysxtfYe6HYcP++Do4rqtbvDs+W7YCwv9Lw0KR2qZqouq7SOjxvswoyqQjlu60k/lroCjtlVlUlXNVFtto/DmrA21vo0qhNr+bV/T125wRz5yxi3duj2zH4U0tex8AqUKVapiqPr37JLlWQHZq7r1cnUNDT6cmQy62jEUcLXp6UM2Vb217zFxmg+9apt39R6UtS8dR1VCde46H22vcdJ4aWr6a7u3bsp7H4RtGjeFRa0fDz091p+jKpKOWrjUH0OhYF0rjWm4z1J63Qe6uPfs+oZ7/8lfu0u6j4i2AYCOrq0DpaoO3nvqLF+RW/8d39rY6P87ov/+ARdK/y+n/2dct3uvm63/x5s1z903bFRW1XQUx+/X9PNhp2QACgBw4RTQv+jsTCMAECJQCqDcESiFueaZWf5euHnqquj6Uru1z5BMsRr7zEjfz2l5rH1HdM/gkW79nr1+bGyMNGblOkYKlK5r6q++K9V3nbE2hUr7HhQA8iFQCgBAGbqya42buHKN/x98BTpFP49duiKrmqfol6ANe/f50KTa7TpyxA2aNS8rTKBtJq5Y44OP+kVJ/+p5uK/LO/d082o3Z/ajgMLQOQuyQpWtCZSKwpaacl7H1F/Shdso8Kp+hJU9xY6hbWL0C7Gm4rf21z/e1x9j2dbt0an/dV4K0obnrtCFxljrWxMoVfhW+9IYHTh50j0w4lm/PLwGOoam81dQ1sK6beWSx0e495/6rbt52Uej6wGgo2urQKn+iEJ/uGD/7VIlbf13cPKqte6peYuy/lACOF/6/7Txy1f7/3/bfuhQ5n7TH7momvobHyScU0ya5v6KYSPczfPmRANRAIDWuW7KZHfJYxf2xSiA6kagFEC5I1AKc9GD3Vy/v/uJ6/7HX42uL6UPPvOcn8nQvvvSZ0ZT16z3z0WFaWLbdSRWnEff+em7yXCMYrNAlit9N6rvSMPvLQtBoBTA+SBQCgAA2pxVL01Od48LpzCpQqUXd+oTXQ8AHVmpA6VveLCrr0aqUN+mAwd8qO+dPfpF2wLFpj/q6TJ2kluxfae/B1VR/bJOPaJtcX7e/MRod/uWz7m79/yDu6PujPvDdXvcrStr3S1LV0TDUgCAF9xNL8xyN8yY5q6bNNFdNXqMu2zgUIKkAApCoBRAuSNQitDdqw74++H6Z+dF15eCFXvRHxffPSh75rq7Bo5w9ceP+yqXHfl7OPs+UmOhMQnX2RhpvdqF68oRgVIAbYlAKQAAaHP2S1pyuntcuDfXPOvDUleN2xhdDwAdWSkDpa//wIfd0q3b3ZEzp13/GS9E2wBtRVXUVWVBlUvfSqi0KK4au9697/CP/XtImndv/Dv3ug+cmyUAAAAA549AKYByR6AUoYs/2MP1+vPvub5/+yP3xg81z5BXaj0mTnMvnzkd/a5NM/ot2FTnK3DeO3RUZnly1kPNVth9wvNZMwAqsKjg4rAXFvop1/V5p9rrj5iveexJ9+hzU/x22l5Tx8+t3eRnC9S2FnLVds8sXOpn9NMfPutzKvVT4VbNuKPnWq71g2fPz5mBUH80XXewwR9btL/7hp07j3DmxD7Pz8r0p7Gp39q/zSAYttPPtr0oRKpZpbROsyHach0nPO9VO3e5m2oGZG2rWRA1a5BNoa/z0Pna7JFpIc5kKFQFCbT9s0uWNx+z6WctC/uta6Z9qZ3RPj44aryfvTF5jWX+S3V+jDVLZGsCpbFZH5MzXlq7zKyXZ6+P7hf1S/0P2wKoTARKAQBAm3lX70Fu5IIl/hc7TVN/S5/B0Xa4AE2/dN934lfupsVn4usBoAMrZaBUH/rpQ7YPPzsxuh5oa/qwWB9q68P+ixIfyqNwF3+ot///qmR4NOmGecei2wMAAOD8ECgFUO4IlCLp8r6j3aAv/MY98fFvuUt7Do22KSaFEhWgTJvWXqHEG58YkAlXPvT0WB8QVEXTUQuX+gBg7f4DPhQ4YObczHbar4UzZ61/0Qc+VcVTyxRMVIBRszSFyy0gaUFIhQx3Hznij2PfCyr8qu31s5bZcoUQFbS04/edNsf3aUtDo++jtVPf7VztOOqjCtiMX74qcz46zoCzf/BvFUobX33F9Z46K3OMNDY9vvaj/an/e48d88e/oWd/3+aqbr1cbf1+32+FVzWFvhUamNg0DgrHtjZQqvPdduiQW1y31V+nMFCq4+nYT0ye4c93xrqNmeta07RM5ztq0bLMMd7xaB+3tbHRKTisIGihgVId88Wm89LnibruOi8FU9U39cvuI/2r5zrfJVu2ZdppOwvEJvcNoPIQKAUAAG1Gv9joFwyFSdN+wcWFu23z591tmz4fXQcAHVmpAqX6gwl98KcP82LrgfbSe+pMf2+q0kRsPfK7bMgcd+fOr+WER0PvP/lrKsMDAACUAIFSAOWOQClirhw6yfX7zE9c/8/+3N2zptFd2mNItF0xKCCYDGOmsWClwqS39xuWWW7hSH1vp5CilinsqJCiAqPWTiHCtbv35Cy38KL2cVmnHpkgZPI4mpJ//4kTOcu7jpvsg4gKhOq5+lPX0OArXupna3db36Fu15EjbuO+eh+StOOEIVO5c8BTrv7EcR90TG6r7ycVstR3lcmKm2L7TB6727gpPpBqoc0hc+Y3h3CDWaqsIqyCm/qsuLWB0jCwKWGgVD/HthVdM127Nbt2Z6bt7zxmYlZ/Cw2U2nmF11fnpZCslmvctMz2r8/Cw8qyw+cuJFAKVBECpQAAAFXm5qV/7N6z+xvRdQDQkZUqUDp97Qb/1+GxDyKB9qQPdTcdOOA/iA8/4EXLrpu+Lyc8mnTnzq+6t/QnrAsAAFAKBEoBlDsCpUijEOmHGv/W3x9S84nvuod2fdzdsXinu3nqKnfNM7Pc1cOn5nXVsCnuogebA4JpWhMotaClKkkm16kSp4KimkJfzxVYjO1Xx9PU7snZB7Xcgo+xIKRYqFEz6YSfUdlyC0lawDSsuGlUNdOmd087zhVdanwgNBme1BT1szfU+s9wFeC0cKmmk7c2FthMHlvT4Su0qX0qtKnwpp6H0+SLgqT9ps9x1zz2ZKsDpckQZuz8YoFS0TXVtdU11nOtV9BW093reSGB0nznlbx3tH+NY7JwkPWPQClQHQiUAgAAVJkbF5xy7238r+g6AOjIShEofcODXf1faD89f3F0PdDe9OGuPphWNYbYemR78xOj3bs3fDonPJp0w9yj7vV/1DO6DwAAAFw4AqUAyh2BUrTkrb2fdu9bf8R1Of55X7HUAqaFum3B1uh+jQKCseBnTFoYMVxnQUq1ie1Xx1MwUQHF5HILPqYFPdNCjclAaVrA0tZZ8DXtOLY8LTx58QNd3P1PjfEVQRVc1XlqKnmts2On0T7TjpvUloFSjYfGRcFgFTxQUNYquWp9IYHSfOdl62wKfe1H+0veB9a/2LUDUHkIlAIAAFSZd77wctGr7wFANShFoPTuQU/7D/3eM3BEdD3Q3t78R481fzA9jQ9zW3LV2PXufYd/nBMeDd2x/V/c5UPnRrcHAABA8ShIQ6AUQDkjUIrWesMjPdylPYe5K4dMilYlDRVSoVTBwlilSKMgoKZE11TqaWFE0fbaT7kHSjXF/IUGSkOahl9T8Ns+7NiqZDpw5twcnUZPSD1uUtr5liJQ+o5H+7itjY1u5Y6d7gNPjfFVXMcuWZFZn9aXUL7zsqqvBEqBjoVAKQAAQJV55wuH3X0nCJQCQFIpAqVD5sz3UySpUmlsPVAOXqzf7+a/tDm6Dp3cxR/q7W5afCYnPJqk/8e66OHHo/sAAABAcSmk9ejpL0fXAUA5IFCK9maVKcPwoNG08gs21WWmiC/WlPexIKGWWxAxLZiYFmq05RaSvNAp7225HefDz05yu48ccaMj+5PwnOzYsfE0GldN268Ap4Kc4TqFLS3Am3a+pQiUisZm3/FjbtraDVnT3UtaX0KtmfJ+/PJV0fuDQClQXQiUAgAAVBkCpQAQV4pA6fNr1rvthw9H1wHlYub6F11dQ0N0XUd32ZA57s6dX8sJj4bu2Ppld9lgPgwHAABoSwppPf4nX4+uA4ByQKAU7c1Ch6qyeXu/YVnr7ho4wtUfP+7W7dnrg44KDKqCZbLtVd16udr6/T5IqDCklrVnoFT90WdYqoipn63dbX2Hul1HjmSmck87ji2347z7ySE+UJrcn1zb/Um3paExEw7Vc33Oq7G4qaZ5LEQh0p6TprtrHnvSPx+5YIk7fPpVV3N2qnxrM2PdRh/qvKP/8ExVz2T/tI3Cu8UOlHYeM9FXmVW/wunupZBAqahwgrYfPHt+ZpnOa2LT8cLz9cd69RV/vlpvbYfPXeiOpFSXBVB5CJQCAABUGQKlABBXikDp3Bc3ufV79kbXAeVClQP2HD0aXddRXfxIjbtu+r6c8GjS9bMb3EUPPhrdBwAAAEpnyD+/5p74+Lei6wCgHBAoRTnQdPWqSClT16z3U7PrXz1XNc8PBqHQh54e65crVDpq4VJfmbR2/wEfFhzQtJ21a89AqfSdNsf3SWFP9VEBzh2HD/u+2/T+acex5eFxNFW+Ko/qOFNWr/NjNGbxcr/P5LnrZy3TOh1X4UoFcbW9ApdqYyFcjdH45av9/pZs2eaDogpfWshy+toNPmC5dOt230YVPhX61LLzCZQqqKrAat3BBt+vd/UelGlvQVztK1lh1cZYAVf1I+mx8VN94FjH0SxHOldVPNU69Vnjsbhuq6+8qv3pXz3XrF06b2un7QiUAtWDQCkAAECVIVAKAHGlCJTqA0F9gBZbB5SLp+Yt8pUDYus6oqvHv+Tu2vftnPBo6Pa6L7q3DjxXaQIAAABta8gXX3P9/u5/ousAoBwQKEW5uLXPEB/sU6BPgUIFHfVcy5Nt7xk80m3Yu8+HBBX+U3Cy+4TnsypNtnegVLqMneSDkwotisKQ9w0blVmfdhxbnjzOe4eMdMu2bvfnpTHS+W8+eNB1e25K1rlL8tj6Odnuyq41bl7t5syYK+g5dM4Cd/EDXTJtLu/c07cJx1ptwvNtTaBUx1cQ1odSm/oVVkgVhWUVug2nuxcbYx0nJjyG+jxrQ63fj9bp34kr1mRVPLV2mXM7e30U/tX4EigFqgOBUgAAgCpDoBQA4koRKF2xfadbsKkuug4oF6paoCoJsXUdyRUjl7rbt34pJzyadP3MA+51Hzj3BQAAAADa3uAv/s4HtS7p2je6HhcmLcCC/BTEUgW4sUuzq7+hYyJQCqBcqMKoKqkmp7tvS72mzKRCKVBFCJQCAABUGQKlABBHoBQdVUcPlKrS6K3r/yYnOJp026bPu7f0fz66DwAAALStwf/UHCi9c+nu6PpSUTU2VdfSlK9hxTOTVmGt0hAoPT+qjld/4rifTjq2Hh0LgVIA5eKugSNc/fHjOdPdl4KmvJ+6ep27re/QzDJVT9Vn5PtPnnD3Dj1XSRZA5SJQCgAAUGUIlAJAHIFSdFQdNVD65pox7uZlH80JjibdvuVL7spn10b3AQAAgPYx+J9+6/p++seu32d+4i7+YI9om1KwQKkqbI1fvjpn/YUESsspxEmgtGW6xrrWuuax9QCBUgDt7V29B7mRC5b46fR3vvyyu6XP4Gi7Yrq2+5Ou7mCD/wOLcUtXuoEz57olW7b5qe8Xbd7iq6XGtgNQWQiUAgAAVBkCpQAQR6AUHVVHC5S+scvApv8fetm9/+RvcsKjods2fc69/RmmqgQAAChHCpR+cP9f+rDWA9s/Gm1TChYoVdhSVbZUkTJcT6C04yBQipYQKAXQ3momz/BBToVJHxk1PtqmFK7sWuNmb6h1B0+d9P8/se/4MV8dVdVLY+0BVB4CpQAAAFWGQCkAxBEoRUfVUQKlFz30mLtm6k733sYf5oRHQ+/e+HfubU8tiu4DAAAA5UGB0rtXN7iHd/8fH9h61/S2qShvgdL1e/b6kMTqnbuzwhFpgdJb+wxxq3bucodPv+qDHZsOHHT3DB7p14UhVaN9PPT0OB8AUVWvcF+z1r/oq37dOeCpzDJNH6uA66SVazLLtP8Ne/c1H7Npn6pO1n3C837aWWuj9jr2qIVL/bH0s/oTC5QqiKJzVn8ufaR7ZnmSQrY6P52nqErZQ0+PzWqjMVOwRAETHefQq6+4ebWbfQDF2thYLm8aS/3OYm33nzjhn9t5WF+Hzlng5tZu8vvS+eq4Nsbm4ge6+Ha2L52PKs0mAy75zkFjpm1Ddr3tWobXQbqMneT3YfvTvsMw8ts+/IR7af8Br8/zs/y10jk0Nu0r1r+krPurabtdR464ATPnZsYo7b5M9tfGctSiZVn3a21Tv27vN8yPp42L1qnNTTUDMvu70O1F56J7TNdR+9K1Cq+3jZX2o35rjPR82AsL/bnr2OH+ekyc5n/fbYtpnpMIlAIAgGpFoBQAAKDKECgFgDgCpeioqj1QevEjvd3Vz9W69+z6Rk54NHTr2r92lw99IboPAAAAlBcLlOrnzkf+3oe27t14rOTT31sAb/KqtZ4CbMPnLsysjwX3bus71Af8FBLUtLMKvm07dChT4VRhwRufGOCemrfIB+j07w09+7s3PNjV/065tbHRvePRPn5fFqbTcftOm5M5xoAZL/gAXtdxk/1zhR8Vltx99KgPi+qYCvUpxKegoW2nQJ72peXa7/yXNvvgXzJQaudQW7/fXdWtV2b7JAud6vx0TDtXLbPKaJrqVlPeKlSo4KCmwp22doMf13D/NpZarrCrzsOm7dUyXQu1s74qaLpuz17/+43tT+ekMVM7BRInNp2vxmn+S3X+uLM21PpzV5jVAostnYP2p74pTKyw44Mjxrqru/X228YCpbpOOsaWhka/LzuHcEzsuiocWX/8uBu/fJVvq2um39V0fW1/SbpXtL+9x475MdL5K0is6zr6bLiytYFS9deujcZKzzW+umdt7Gy52tn0xRe6vd1ndt/auaid3e+Z10DTuKjvCqVOXLHG37e6T9Q+DDwrgK3jKnRty9oKgVIAAFCtCJQCAABUGQKlABBHoBQdVbUGSi99cpK7cf4J995D/50THg29a9Un3FsHzojuAwAAAOUpDJTK/XWnfXBr4Of+1z2480/dTVNWuEt7DsvaphjCAJ6Cj6qSqPCbwmxanwzuKSinwFzYRu4aOMIHBxWqs2XJEKeo2uLBU6d8aFHP7x8+2m+nPizbuj0TgtR+FKa7/vG+/pgrd+zMOab6q8Cm2inAqmUWKFXQ0vYlYV8s5Cf62dokZY7b1O7dTw7JLNexdEzrb+cxE13jq6/4QG54TAVdFRy0SpI2lsnjKjSrUKhCl3pufV1ctzWrkqeChOHYhWFgO67+nbFuYyZwWOg5aJmusfqnflq7ZEBTY17X0ODvkzCIa2O6cV+9Dz9aSDIMmYqq0Koare4hW5bUa8pMfw3D0Ol1TfeBQrC63pd16tHqQGlyjDRtcXK5xmrNrt3+PrP76UK3V9hW42zXTGzs1VbbpI2V9qvPX8LqvTb+yZBpWyFQCgAAqhWBUgAAgCpDoBQA4giUoqOqpkDpRQ8/7q4cs9b94YufzQmOJv3Bso+6S/s2V3ACAABAZUkGSuXKIZPcB/f/pQ9wGQVMe/3599yjr3zJdT3++by6HPucu3LopKx9JiUDeN3GTfHhSAUNFWZMBvfe1XuQfx4GR0XhNoXcwgqaYYjT2tnxVK1SzxUwVcXSKavX+ZCdAqQWsLOwo4UQk8cU7Uf/769pwPVc56H96zhhO+vL0BcWuBfr9/vwo0KwYZukfMftNHqCe2z8VB8ITKsYqSqsOjcL/6WFIG/pM9hXBk2GIMNxEwUsw3NT+9hxbUp0bV/oOeh5IYFSC78mp2GXcBzsGob3g1zRpcaHUZNjELKA7sym/aWFJlsbKE2OpdaremoY4LTl4Rhc6PYxybFJGyuxa2mvFzu/1kx33/2Pv+r6f+anRWHvQ2/4UM/osQAAACoVgVIAAIAqQ6AUAOIIlKKjqoZA6Zt7jXXvnHPY3XPg+znB0dB7G/7T3bjgpPv93uOi+wEAAEBliAVKzRse6eGufup5d9v8Le7eF4+7h3Z/3E+LHwuRhs4nUKoAp6ZLtym5k8E9a6+QXUwYiouF8SxkadOCq0qjwo7ar4KPCixaCDIZorM+hmydBRzVRs+1PGxnfdnaeMhXv7TAbNgmKd9xQ7EgZrjOgrJpIUhbbsdJCzHqeXhu2o+Ne4zaF3oOEjuP5PZpfbN1Fu5NC0na8uQYhHRdpq5Z76eA132oafKHz13oLu98LsSYNpaF9lfrw7EMl4djcKHby33DRrn1e/b6IK5dG7GxSRsr0X0TVjNtrvB70lf2DdvlU5JA6SMESgEAQHUhUAoAAFBlCJQCQByBUhj7gir2RVg1qtRA6es+0MVd8cxyd+u6T+UER5NuXftX7u2jVrnXP9wjui8AAABUlnyB0lKKBQ5v6Nnf7Th82AfZVIExDO5Ze/1uOHDm3Bxhxcu0MJ4CpAqVPjxynJ8evGbyjMzvLKpyqSnPw8qbsT4a9U+VIgsNlKoy6aiFS324TyHFsE1SvuOGYkFMUThX41TKQKnChc80nU/sWqiabKHnILHzSG6f1jdRBdViBEqNpobXlPEKlCpYqnPtOWm6X5c2loX2V+vDsQyXh2NwodsrIK12qlL74Wcn+uWqjFt3sCEzNmljZfSaUAVbvR60n4376ttlunthynsAAFCtCJQCAABUGQKlABBXTYFS+1JIX+S0xL44wjn2BZXGJ/bFX1K+9hrfcLxDqvSjL8+mr93gruxak7VdW6q0QOnv9Rjprpux392191s5wdHQHdv+r7t26i53Sff8U3MCAACg8pRToFRUnVQhvjW79vgwmwX3rHqofi9UYDLcJiktjKd9Kxw4Y91GHyhV8FHL9XuEfg/RvsNwXTGnvFfo0aqwqg8PPT02q13IpqJXoC+57upuvd11j/f1+7rQKe9tuV2DtHHT8/DcdNyWqlUWeg56Xkig9EKnvLfl+QKl6pdCzRc/0CWz7O5BI/y9Ulu/313WqUfqWCb7mzaWafeJlodjcKHbqxKvnts9LsmxSRsrozFvbBrzaU2vD70OWjPdfbERKAUAANWKQCkAAECVIVAKAHHVGCjVlHf1x4+7vceOpRod+WKrUHac5Jdolc6+oIp9ERaTr72+INNyfYmYHHuNndaJvkj8YOLLtbZS7oHSix7q7i4bNMtdP+ugu33zP+UER0M2pf1bBzRX4gEAAEB1KrdAqaYdX1y3NfP/9xbcUzBy7e49/v/3HxyRHcbsNGaCu73fsMzztDCeBUR1XJvKW8sVnNN+JQxAav3KHTt9oDDc/1XdevmAoSqAqpqllqUF/ZJ9UdBS22l77Sdsa+xcw/3Ltd2fdFsaGn2f1LfOYyb6wN/kVWuzQrYDZs71oVwLAKaFIG25XYO0cdPz8NxUDVb7V9A2DF/qfJ6YPMMvK/QctKyQQKn2XdfQ4DYdOJg1brf1Heqrv1r1zLSQpC3PFyjV+SSDsuqj7hXb3xVdanwfkvtXtVv9LtjSWKbdJ1oejsGFbq/zVKBX95u10T2se9n6bmOSPBdjY65rrddGa6a7LzYCpQAAoFoRKAUAAKgyBEoBIK4aA6XJL7eKra2O09bsC6rYF2Ex+drrCzItT/sC8I7+w/0Xe2qjLy3DL87aSjkGSt/ca6x7x8Q6P1X9vcd+nhMcTWJKewAAgI6l3AKlYsG35P//a5p5Bf4UCh23dKWfXl0hQP3R2ZTV6zLhRgtaKgjYb/ocd81jT/rlChyqaqf2qwqjtl9NC6/fIcKKo0aVRHVM9UdT1ms7mwpdwU1rlxb0iwUDrQprMggasnPdduiQP6YoiKllWqc2Cjsu2rzF/wGkKlJqPFRNUv0IA6v6HVO/ayZ/l7Lldg1ifbXl4blZ6FczRSgYqt+DNEX8jsOH/Ti9Z8BTvl0h5yCqEKvxmNp0DRVW1XnF7g8bN+1D+7JjhvtLC0na8nyB0uS11nhqXDW+6qO1088696Vbt2fuwcamvmpZS2OZdp9oefh5wIVuP2TOfN8fjb3GafaGWn9u2qeNTdpYhXSu2qY9p7sXAqUAAKBaESgFAACoMgRKASCOQGnrtdVx2pp9QRX7IiwmX3t9Qabl+b4ADL94jk2FWGrlECh9Y9dBPhD6B8s/5quMJgOjMbdv/RJT2gMAAHRQ5RgoleFzF/pAXPL//+8ZPNIpFKpgodarOuWgWfOyKmUq8DhrQ61vc+DkSffAiGcz63Q8VVtMThOvUKB+HwunCDfJYyrE2H3C81lh0LSgXywYaEFQ7a9myoys9qH7ho3yfzSnQKPoZy0L2+hcVYlU1Sh1HIVrNa3+lV1rMm0sOFqsQKnEjqvw5a19hmRtW8g56I8BfZum/WicNbV82v3RZewkV3ewIXV/aSFJW57v90kJr7XOS+en89T5WpvLO/f0YxzeD0PnLChoLNPuEy3X9vZ5wIVur3tTv5/uP3Eicx4KmSoAbJVL08YqpKCuwrLtOd29ECgFAADVikApAABAlSFQCgBxHTlQqi+ZVu/c7b+w0b/hl07Sc9J0/6WTquV0GzfFf+mjtjHJL470heDUNet9NR6t1342Hzzouj03Jaeqje1XX5Y92rReX3Dpiy59waf19uWU1mu/YbUS+yLwpppz0xKGrL31Q/vV+IxduiKnYol9QaV2yfOJydc+PKdweZK+CC6kXSm0R6D0jV0Hu8uHL/DT09+1999zwqIxd2z9srth7lH3thGL3Ru7xK8zAAAAOob2CpQCKH8Kobb3dPdCoBQAAFQrAqUAAABVhkApAMR19Aqlqo6iL1wULBww44XMck03qOopCjuqmolCoL2mzPRTBa7dvce3V9BU56plmm7Ptr2t71AfCtW2CnzuPXbMV9rRc1VlGb98dU51HK1TNRLtUz+LhSwtUKppHW2/2p/tU8IpEs0Hm8ZD56b11g8dQ6FSLVO/wxBtewRKC21XCqUMlCo4etmQOe6aydvdzcs+6u7Y9pWm19gvc8KiMe/Z9XV3w7xjBEgBAACQg0ApgBj7g9n2nu5eCJQCAIBqRaAUAACgyhAoBYA4przv5EYvWuZDllsaGt213Z/MWqYQp6aXC9vnO45Cli/W7/fbzn1xU+aLHAVI+02f4wOjCnmG0/xZqFI0Xd+DI8a6K7rUZAKiFiiVrY2N7u5B56Y6//CzE321Uh2v77Q5meXatq6hwS+fvnZD1hdKto3ClDWTz03ZSIXS1ruQ4Ki5a9+33U2LXnVvG7GEACkAAADyIlAKIKTf9fX7uKbH1x+Shp8LtBcCpQAAoFoRKAUAAKgyBEoBIK4aA6UKKOajsGO4nYVAbd1dA0e4+uPH/TT1sS9j8gVKNcWcQpyxqiAKlWpcdByFPG25hS9VETVZZVQsULr76FF3e79hWeu0T4231k9ZvS6zXO3mv7TZT3dvIdmQBTlnrX8xs6ytA6Xqo85J7UYtWhZtUyqvu//Dbui8JT5Qekn3p9yba8a6t/Sb6sOhVzy9xF357Fr3jgmb3bXP73bXz270U9TfvPRP3C2r/9y9e+PfnVdwVO7e9x2//Y3zT7i3j15FgBQAAACtQqAUQOiGnv3driNHXOMrp9zYJSvcxQ90ibZrSwRKAQBAtSJQCgAAUGUIlAJA3EUPPpoJu72/SGpP/1W7Bko1rbwCoZriPUbVR5Pbasp6Ve3UdqoSqpBjckp4kxYoVbhz2dbteQOSFg7VMSxw2lL40rZReFMhzuT6QsKbSbFt2ipQqjHtMnaS23bokG8TqwJbam/pP9VNP/MVd+QjH8kKfBZDGBq9esImd9ng2e5Nj2UHgQEAAIDzQaAUQLkjUAoAAKoVgVIAAIAqQ6AUAOIueri7u7v+O9Fg3PmqPfOpipryXhQGnbhyja8uqpBjrBqoSTtOGLBsSRgOLWWg9NY+Q5quxRa37/ixzLmFSh0obYnGWWHecNu20NpA6ftP/Nq97/CP3d37v+fes/sb7o6tXyY0CgAAgDZHoBRAuSNQCgAAqhWBUgAAgCpDoBQA4l7/yBM+MKdQaWz9+WjvKe/PJ1Aqf9h3qA9eKug4Z+NL0TZSSKD0wMmTTevjFVJl1Y5d7rJOPfx2pQqUDpjxgjv06is+SKop8JZs2earrsrWxubqoKUOlOr4sfOvrd/vhr6wIFOlta0pUDpm8yH38pkz7u2jVrrLhy9wbx04w1365AT3ez1HuTc9OtS9odOT7qKHH29q3zlnewAAAKA9ECgFUO4IlAIAgGpFoBQAAKDKECgFgDgCpc1UoXRe7WYfghRNf59WObOQQGkhgUxTikDpHf2H+3Ds4dOvusGz5/vza2mbUgRK086pHGhcXj5zOroOAAAAKEcESgGUOwKlAACgWhEoBQAAqDIESgEgjkBps27jprjGV19x9cePu2Vbt/sw5Lo9e6MVNNOO86aHuro1u3b7bWetfzFrm3xKESjtNWWmr0y66cBBd0WXmqz2QqCUQCkAAAAqD4FSAOWOQCkAAKhWBEoBAACqDIFSAIgjUNrJXdv9SbelodEHMEcvWpbzPNk+33FGNbVXkHL30aPu9n7DstaJKoXe+MSArIqhpQiU2jaxQOklD3dzq3c2B18JlBIoBQAAQOUgUAqg3BEoBQAA1YpAKQAAQJUhUAoAcR09UKpg54x1GzPhy6u69fLL+06b46eLV8XSuwaOyNrGjqNp8e8fPjpr3Q09+7sdhw9n9ndrnyGZdZd37umn1VeIUYFLW16KQKn1Uccav3yVu/iBLn65xmXljp0+LJvchkApAAAAUN4IlAIodwRKAQBAtSJQCgAAUGUIlAJAXDUGSo+cOe2DoHuPHUs198VNfptOoyf4bTTdvaa9t32FVTz1r57bOoVO6xoa/DqFTvefPOF6T52VWf/Q02P9MbTe+rLv+DHfVsu2Nja6W/oMzrQvRaBU0+/rGlhwVP3QOepn9UOhW/2sKfrVVtsQKAUAAADKG4FSAOWOQCkAAKhWBEoBAACqDIFSAIirxkCpgowtUdBRwVBVEdXzRZu3ZIKV5r5ho3xYVKHM4XMXZq1TaFSVSLVOAc2ek6Znrb+ya42bvaHW1Z847vevdgpxjl26wl36SPestqUIlIqqkg6aNc/tOnLEH1+h0rqDDa7bc1N8AFbLdr78sp+CX+0JlAIAAADljUApgHJHoBQAAFQrAqUAAABVhkApAMRVU6AUaA0CpQAAAKg0BEoBlDsCpQAAoFoRKAUAAKgyBEoBII5AKToqAqUAAACoNARKAZQ7AqUAAKBaESgFAAAdnk2vW8iUt5WAQCkAxBEoRUdFoBQAAACVhkApgHJHoBQAAFQrAqUAAKAsdRk7yW0+eNAdPv2qD3sePHXSzd5Q667sWhNtfyEIlBbPsm073N5jR93NNQOj62Omr93gQy41k2dE16e5+IEubtCseW7XkSPuSNP1O9K0j7qDDa7bc1Pc6+7vHN0GQMdGoBQdFYFSAAAAVBoCpQDKHYFSAABQrQiUAgCAsjNg5lwfJN1x+LAbs3i5G9j0fP5Lde7Qq6+47U3LbukzOLrd+SJQWjytDZRe/3hft/Pll/34L9u6veAg6CUPd3OL67b6IOm2Q4f8fTJx5Rp/z2iZfiZUCiCJQCk6KgKlAAAAqDQESgGUOwKlAACgWhEoBQAAZeWqbr1cXUOD29LQ6K7t/mTWum7jprjGV1/xFS3D5ReKQGnxtDZQqqqkDa+ccut273X7jh9zd/QfHm2XNHzuQl+RdPKqtb5SqS23oKkCyT0nTc/aBgAIlKKjIlAKAACASkOgFEC5I1AKAACqFYFSAABQVhREVCBRwcTkOoVN1+3Z68M7CpcqiKjKpWGbe4eOcvtPnvDhHlWoVNhw6JwFPqyo0KiqnC7Zss3dVDMgs00sUKpg4tglK7K2m1e7OWfKfR1DIY39J074dmr/zMKlbtOBg9FzaAuVEih900Nd3codO32AuOu4ye7gqZNu1KJl0baht334CffS/gO+sumNT5y7jkahVF0H7VvH0LJJK9f4++WPnnkuq22sv7f2GeJW7dzlQ6kKrepa3jN4ZGa93S/PLlnu16kiqu4x65OqrlrbSx/p7jbs3ZfaVwBti0ApOioCpQAAAKg0BEoBlDsCpQAAoFoRKAUAAGXFKpQqoPnAiGejbURBvY376nMCfMNeWOgDEz0mTvPPRy9a5kOBS7du91Pnj1++2gcLNTX6DT37+zbJQKlCiIs2b/HbKXyq7aat3eC3q63f7/tox7Pp+VVRVccet3SlDzNqWwKl8TbGgp8Ke2rM1+za7a+prm2svbHQsK5NbL3tK7w3Cg2U3tZ3qNt15Ii/P0YuWOKvqabU1/HuGzbKt7H7Rddd61QR9aGnx/oAcnjvyZ0DnnL1J47nBJ8BtA8CpeioCJQCAACg0hAoBVDuCJQCAIBqRaAUAACUnUdGjffVKhXYU+Cv05gJvmJosp0CfKocquqWem4VLy1IeFmnHj4AqiqSV3Q5V1l0xPzFvqpkrykz/fNkoLTzmIl+an1Np64KpLadhUd1XD1Pm57/roEjXP3x4wRKI+tDqkaq63z/8NH++ZA5813jK6f8+CfbhhQKVThUIdHYekn2o5BAqe4fhVR3Hz3qbu83LNPGrqeFQu1+UZA0vC91HjqfWetfzCzTOemeCUOmANoPgVJ0VARKAQAAUGkIlAIodwRKAQBAtSJQCgAAypKmHVe4T2E8qwaZnKreKlVagO9dvQf5cKBNd28BUy17cMTYzHZJyUCp9qf9av9hu3c82sdtbWx0msJcVTQt2Dhl9bqsdjYleyGB0rcOnO6um1FfVO/Z800fmLrnwPeL4u76/+euGLk82v+kQgOlVmHWxlLL7PpNX7shp32o0EBpGCAtJFBqx09WE1X/1E9dU13b5P2SbKd7RPeK7sFlW7cXPN3920etjF7PLNP3uXdM3Nx0PZa5t/R/3l3cqU90XwDiCJSioyJQCgAAgEpDoBRAuSNQCgAAqhWBUgAAUNZUAbLL2Elu9c7dfhr5vceOufufGuPXJQN8qgKpiqU1k2dktlcoVIE+BQA1vfr8lzb7qcvDyqPJgGC+UKTWWQXUtGBhawKl18884O5/9bUiU5j0P/y/xfKHL3422v+kfGMX8lVgXznlK3jaMl0ThbLCqepjCg2Uhv0oJFBq+9U1jWkpUCqqumpVc3UOOhcLOCfbJt29/3vRsW/Jext/6N616hPu93o+E90vgHMIlKKjIlAKAACASkOgFEC5I1AKAACqFYFSAABQMRQsVeBPVR8toDfshYV+enoFFFVZMlYNUqFUTZuvMKlCpZruXpVLFQ7U+mRAMC0UqWMqOFTMQGkpVMKU96pCakHNJAVewlBwklWmVcXa2HpVpl2za7efut7uhdYESnWNB86cm+Ox8VP9vtOuu9w54ClXf+K4P79YwLkYLn6kl/u9HiPdZUPm+Kqlf1j79+7+V5qDxLeu+5S76OHHo9sBIFCKjotAKQAAACoNgVIA5Y5AKQAAqFYESgEAQFkZOmeBUxVShfGS6yyoaZUitcwCfArzqFJpWA1SQVIFCq2tXPxAFzd1zXofKu01ZaZflgwIFjrlvabRP3gqt1ImgdL8gVJdE4Vya5vGKBnaHD53odt15IgP/Cq8GdvexjcWHpY7+g/3weFwH4UESsN7KV9F0XyBUguz1jU0uEWbt6T2sdje0Lmvu37mfvf+V37n3rPnm+6NXQdF2wEdXSkCpfojh4VNr/fYOqBc6P+vDp9+NboOAAAAKEcESgGUOwKlAACgWhEoBQAAZcWqRCqgo0BouO6ugSNc/fHjPrBnQUH9q+CgQhKSnO5ewdBkQFDTkatypAUCkwFBPx170/rJq9ZmbTdg5lx/jLFLVvjnV3Xr5YODWxoa3bXdn8y0s34SKI230TXSOPadNie6XoFeBUIVDI2tFwVPj5w57a+RQsK2XPfM4rqtfv89J03PLB8w4wUfIg6PqVDwxn31mf7q+drde/w9o7CwtRNVuL293zD/c75AqWgaf1WBUx9aCqcW26V9J7t7j/zE3bX3W+6ih/ggE0gqRaB07oub3Kqdu6LrgHIxZvFyX7k7tg4AAAAoRwRKAZQ7AqVAy/S9i4pvrN65O6v4SzFY8ZGwCE05aem7pLak78D0XVh7fW+J81PId64xyeut7ymnrl7nNh044G7pMzinfbHcM3ik/9587NLm79Hb6jVayveZjoxAKQAAKCv6n9rxy1f7sOCOw4d9AEKVK6c0/Y+u/uf34KmT7pFR47O2UYBPYcFkNUiFTfU/kNqXpkfXfkYtXOoDFWFgMflLXWy7aWs3+KBrbf1+HyS1Y1jIVKFSTb8/bunK5mn1m7btqIFSXaNnmsbZqo6aftPnuBt7DvABYF3Ld/WOV9G0wK8Fd2MUHLVrtO3QIX+fiO4Z3QsTV67JCnJa1VJVv9U9MHLBEr+dgp/hL2O6t9R/VSrVtVS/579U5/uje1Dh1ZY+BLBjtTR1f6lcPmy+D8z9wfKPRdcDHVkpAqV6b9h04GB0HVAunl+z3n9wF1sHAAAAlCMCpQDKHYFSdFRWGEbfk4T0Pcr6PXt9qMzaKui1rmmZPj8Nv1srhnILlFoBHJs9r6XvktpSJQdK1efkvabvAXU++h4xWZyomujcw+8wCxULlM6r3eyX3dZ3aE77Yrlv2Cj//apmCtXztgyUlup9piMjUAoAAMqO/se20+gJ/hdP/QKqXw7sF1H9z2iyvf6aSgG+WDVI/SKhXyi0XvtR+FNT1oe/0MZ+qUtup+Prf7av7FqTaSM63uDZ893+Eyd8O7VXmFL/09pev5i1d6BU4xCjDxgU/vXVW7duz7lW5h2P9nFbGxt99VD9EhBrIwp3Dpo1z0+Rr18eFS7V9dVxVIk0uf9Hn5viQ8fqi67n3NpNbsa6jTm/jOne0D2ifWm/2r+OY5VQW/oQQMddsX1nm013H/OulR9397/ymntTt9L9pSFQiUoRKFXwXO8Xr//Ah6PrgXKgv87Wf/di6wAAAIByRKAUQLkjUIqOygKl+h4kLCqiEJkKdsQKw5RCWwZKCznWnQOe8t83WaGRlr5LakttGSi1+2PSyjXR9a2lPicL2ajAz4v1+/13eJq1sFih0ra8pwqhc09+h1mIYl7v8+2DlGI8i31/IR2BUgAAUPE0Rf3BU6fapRpkjEKEqoLaFr+YxbRnoLS93VQzwFeRVbhUVWXzBVJLRX/9pikd2nq6+9Abuw5y7z/1W3fjglPR9UBHVYpAqf7QQR8MvvvJIdH1QHt7w4Nd/X8XB86aF10PAAAAlCMCpQDKHYFSdFT5Al02C5xV6UyuL6a2DP8VciwVqQkLjRAoLV6gNBZoVIhUYdLGV1/x3xOH685XW95ThTjfMCeBUhQDgVIAAFDRFNhTlcn2qgapadxVDTNcpmCrpjsftWhZ1vK20pEDpaIQ6ZyNL7mJK9Zkqoq2pW7jppRFwPnWdZ9y9x75aXQd0FGVIlB6Wace/oPBEfMXR9cD7e3hkeP8PRqr8g4AAACUKwKlAModgVJ0VPkCXQqRrtm1OxNASwuU3dpniFu1c1fzTHFnTvtZ/8KZBUXf9eh7HgVU9dmW/g0LiYT77vP8LLfj8GFfsbKxqW/jl6/Oqlqp7xK7PTfF1R1s8MeLHdNCeKt27HKzNtT6vilMJzp+KHnu1pfpTf2zZRYo1XeF4blqau7b+w3L2l6zI85uOmZ4rslzsP09u2S577vO1cKq2l6zLNqsjzabon1HZue2vOlcNOuizc6o2Rf1PCyOkpwdUP1W/1XQxdrE+jJq4TJ/DC03uk90v9h25yNfoLHruMn+nHU97BqoP3qu+yC875KzE+p+6T7h+cy5t3SdW9retHQtTJexk3Lux+Tnt4WEOXX85Eya45ev8v9qe2sX25eO56/f2T6oP+qX1uncw7EQ21/avZh8bwivicYgvO/CMQlfy3a9xO5bHdd+Dvtj91fa9pd37ulfy+HrSu8pYTEiO5ehcxb4Gb503XQ+Govke1JHQ6AUAABUJP1S+tj4qW7+S3X+f+70i1WsXSnpf9L1y6GOr35omgX9T7r+h1QBV03FH9uu1Dp6oLS93Dt0lBu3dKWrP3HcT7UR/tLSHq6ZssMH59744f7R9UBHVIpAqeiDOH0IUuq/ugfOh6Yf23/yhHvjg9yfAAAAqBwESgGUOwKl6KhaqhAYBtdiQa/b+g71YUUF8kYuWOKnL9926JD//MoCdVZ9UuE9BcL0/ZuFPG2Kc9u3goP1x4/77+e0r9qmZSr6MmDGC5k+DWjaXt/lKcyp8N2ohUub+ngs67s8C6wpWKfveXQe6t/V3Xq7uwaO8AEz0c/J739UIVOfD4eVMi2opj6rYquOq0Csxk7nfkPP5u9uNOudZt5rDpGu8ue6dOt234/wu89wfxovjcNDT4/NbK/9qn24/cSma6TvMu3c1EbnrPPXuakfFsrTMdRW22j8lmzZdvZ7z9W+Tfi9V6wvH2zah87pickz/DVRMSAVAgpDsecjFoQ04b1o94POW8sUgrXwoMZJ46vZJXXudp+o/7o3tK9817mQ7aWQa6F2KlSkbbc0NPp92bXQMVTl1/aX79yNjh/uy76n1PXR9tYuuS+91vSa0/XTdqKfta+ek6b7c1fb1Tt3+yDogyPG+jHStmn3YlqgVOevfahvNnYKbY4+W5jJ2oXvE2L3rfqu8Gna/RXbXv/qntXrftb6F/210Hf56rO9h6idnYtev/b+YK/TZH86GgKlAACgIqkinP0lmP5qL/xroraU/Es9/Y+pfskK/1KvrREobR/6RUi/FOkv7ZJ/Xdoe3tJvqg/OvaX/1Oh6oCMqVaBUH6boQ4f+wYeUQDnQX1Hr3tQHdbH1AAAAQLkiUAqg3BEoRUeVL1CqQF1dQ4Pb2tjo3vFon5ygl/4gX9+hKZgXfo+i8J5CoQp86bmCmZrKPHkMTSuvY6sPtu9kCO/OAU/5QJ2Oo+dv/dDj/g+uFYwLw2E226B9bmbhNX3fp9CrtZPkeYTrFBJcsKnObdxXH618qGqVFiQUBdbCWQ5VPEfHrZlybtY77Uf7s3HUMttfGIaTIXPm++9KwwCt9Un7fVfvQannZhU+FWTV8+sf7+vPcf5L2X3WuKud2ut5Wl8k3/1xPvKFKnXu+m5W1zDtftA9p0Bv8p6z8Gc4A2bsOrdm+0Kuhb1G9F2ifrZ2FrQO76OWAqXXdn/SbT982IdJ9bMtv3vQCB+QzBco1TVXMDMcK9vOXjux7STt+qcFSsOwuOi8df4aO91zsXEXu2/D84jdX7Ht7Vro9WbtdC0U7NVym2Uy7VwUQtVslPrux5Z1NARKAQAAqgyBUogqkyo497YRi6LrgY6oVIHS1zVZVNf8F/P24RHQ3n7/g93dnqNH/QfPyQ92AQAAgHJHoBRAuSNQikrxhkd6ukt7DnNXDpnkrh4+Na+rhk1xF7Uwy01aYFCBtoWbt/jCG1ZZMxn0UqBOATELjhoF6FRExtopzKUQmmaGC9td89iTrt/0OX4/sRCZXNGlxofVwhBaTPI8YuE1k3Ys0efBvurn2YCosaCa/g2Xq73CiS31Lxnki+1PYcc1u3Zngnm2XDRGGiuNWdq5qTqrKke2FP5MHjvWF9MWgVIV+1EVzbDKbNo1soBx8p4TBVEV7u0xcZp/HttHodsXei0sxJu8XyR538fOPaQwqEKhybGOXe/kvixwqYqryen4Q7E+pF3/tEBp8pqIAq1qq23S2sXOI3Z/JbfPdy2S1zPtXBQKtv6FyzsSAqUAAABVhkAp5OJHevng3FXjNkbXAx1RqQKlog899RfE+oCjHKoUo2PT9EOa0oeQMwAAACoVgVIA5Y5AKcrVW3s/7d634Yjrcvzzrv9nf+7v09a4fcG56oQxFuhSCCtJYVLNKmh/3JwMeuXbVqxdS0E6Se47uTwModlsg5pWXH0Mj2nBtFh4zaQdSxTMU7Dxjv7Ds5anBdVi+7q1zxBfFTI5NuEYxPaXr1+htHOz5WE4T59zj126wi9X9c+wP3bstHOTWOAvybY34Xkmqc9h25CCuZpqXe3SxiJff2ydhTtj+yh0+7TjJ+UbOy0LA64tvQ7S9hW73sl9Xd65p1um6fibtlfAVQFM7SdZmCDWh7TjJscq35iE+0hrFzuP2PVIbp+2v7CtAuy619PORc91HB0vXN6RECgFAACoMgRKIRc99JgPzl0zZXt0PdARlTJQKlc/2tt/GKEPfcYtXelu6T042g4oFU2BpWl89FfWmp4o+UE2AAAAUCkIlAIodwRKUW5+//HB7kONf5sJhtZ84rvuoV0fd3cs3ulunrrKXfPMrGhV0lBrKpRqGvmBM+dmaOr2tPCWBbvStg33oeqCLQXpJLnv5HILoWma6+lrN/jQ3Ix1G93dg572+31i8oys6o6x8JpJO5ZVQtSU5uEU8ZIWVEvuSxUTFUjdduiQn4Zb/RBN0R+OQWx/af1KSjs3W25joDChpnfXNdI095qGXW2emrco69hp5yaxwF+SKnaG173XlJmZad6T1GdNY//MwqWZ9qr2+Z6mcQsra6aNRb7+WIXP8w2UhtunHT8p39ipKmZbBUpF96yKY6iisGa5UthaId37nxqTd7u04ybHKt+YhPtIaxc7j9j1SG6ftj+xCsYESltGoBQAAKDKECiFXPSgBUpzP/wAOqpSB0rlzX/0mP8ARpUh9UGEplXRVDX9Z7zguoyd5N439Bn/ISFwoe4Z/LTrNHqC6/P8LDd51Vr/QZjuOdGH2JpmLHaPAgAAAJWAQCmAckegFOXkyiETXb/P/MRXJL1n7SF3aY8h0XbFkC9gl5QMdukzLf0hdCyAGZqyep1vp/bhcgUeNRuPgmBpoTFbbiG0zBTzW7dnHTN5HrHwmkk71v3DR/t+Kggatpe0oJpN+2/HCaf+Dtslg3yx/el8FM5VGFB/aG7LRWOkc9eYpZ2bLbcxeHDEWHfwVO61TR471hfTmvujEMlxSJN2jeyeK/WU94Vei1JMea97KFweu97hvtTX6x7v64WvCX1/oWunarmx7WxZ2vVPXvu0ayJqY/d9WrvYecTur+T2xZjyXs+tf+HyjoRAKQAAQJUhUAohUArkaotAqdEHF/pr6bm1m9zuI0f8hxJAqagaqT7oGzF/sXtnj37RexIAAACoJARKAZQ7AqUoF2/rN8YN+vyvXc8/+3dfpTTWpphaExhMBr0UrFu7e48PzSm8GLbtNGaCr5aon1WpVH+wryqZYZvhcxf6AF3nMRNz9m1tbLmF0CyUprBfGJ7TLD8KA9p5xMJrJu1Yqnxa19DgrurWK6u9WFBtcd3WrGnEdVydm6bK13MdX+ekcKC10f70x+NhkC8t+DZywRK/vzDUqvNUNdZ9x5un4k87N1tuY2DXVoFea6N96Y/Zw2On9UVac38UIhZojEm7RgoXquqqQsV2f4nGuLZ+vw8dKuyZto/WbF/ItdB2umd0fcP7RtVgdx054jbuq/evEy1r6dzT9qWgs6q6htc73Jf2rwqdYd9FQVgFYtO2s2Vp1z957W081ZfY/W2vHY2RXp86jgLX1i52HrH7K3bd9PrStdDrzdrpOBObtguvUdq56LmOo+OFyzsSAqUAAABVhkAphEApkKstA6VJ+uBJH468+8kh/q9ggQule+max570VXFj9xwAAABQyQiUAih3BEpRDt74oRrX51M/dH0//WP3ps69o22KrTWBwVjQS8EyhcRUJXDc0pX+j/JVLVBVGxVk1DTmCmAqiKng16wNtb6NqjeqjQU0Y/sOj2khNAsEajrv5U3LRsxb7KeT13MFyew8LFwZhteM7UP9fnbJcj9rkIX5FCpNthcLqukc1u3Z64Ntdg5bGhozswspHNvYtEzH1nhoDPSztg2DfGnBN/VDwUZdE82cpbHSH54rLKvwnEJ0aedmy20M7JzUR10TBSRrm8bySNNxw2On9UUUmlR4su5ggz/nMCB4PmKBxpi0+0EeelqVV0/6UOiohUt9ZVGdl67NgKbxsnax69ya7Qu5FmrXd9ocv63uA+1L47zj8GF/jDB4Wci56/jhvnQPafx1f4fXO7mvZB90rXSfajsLO4vub7Wb2nRfKuitMUq7/mmBUu1T56ZKqjqWjql7SgFx21bH1DIFXdWXqWvW+22S5xG7v2LXXv++2HQtdC/rdWfvMzqXMOSddi56TqA0shAAAACVi0AphEApkKs9A6UAAAAAgMIRKAVQ7giUohzctXK/vw+vf3ZedH0pXGigVO4ZPNIHxxTuUohMlRkHzZrnw6TWRlUUJ65Y40NlCnzpXz236o1p+7blYQjt8s493bzazc3HO3PaV0d8fOI0H0yzyqVpoUujUKEqOqovizZv8RUOFYpVFcVYewXSdG7PLFzqVu3clTm2wqw31ZyrCimPPjcls+/m4N1qH+QLK5emBd/kyq41/vwUnlMbndfQOQsy45l2brY8vJbqmwVu1Wf1vd/0Ob4vVrk0X180lmMWL/fttY+wWuf5SAYh06TdDyZ5zynA2X3C85mQp0le59Zu39K1MJpeXqFIjZHdk/cNa57q3hRy7jq+gpWawcqOp3tO+wuvd2xfWX1o2lavQwVUw3O6pc9gvy+t1/lf1qlH6vVPC5QqfKvQrPqm7dRX9Tk8jgKeuu913+Q7D22TvL/Srr1e9wqkp72HSNq56DmB0shCAAAAVC4CpRACpUAuAqUAAAAAUBkIlAIodwRK0d507/X/zE9dl2Ofi65H6SjUtmBTnQ/YheE0AKgWBEoBAACqDIFSCIFSIBeBUgAAAACoDARKAZQ7AqVob9ePne/vwWuemRVdDwDA+SJQCgAAUGUIlEIIlAK5CJQCAAAAQGUgUAqg3BEoRXu7Z+0hN/iLr7nXP/RodD0AAOeLQCkAAECVIVAKIVAK5CJQCgAAAACVgUApgHJHoBTtreuJf3KPnflKdB0AABeCQCkAAECVIVAKIVAK5CJQCgAAAACVgUApgHJHoBTtrfdf/sA9vOeT0XUAAFwIAqUAAABVhkAphEApkItAKQAAAABUBgKlAModgVK0t36f/h93f93p6DoAAC4EgVIAAIAqQ6AUQqAUyEWgFAAAAAAqA4FSAOWOQCna24B/+KV73/oj0XUAAFwIAqUAAABVhkAphEApkItAKQAAAABUBgKlAModgVK0t4H/+Cv33vUvR9cBAHAhCJQCAABUGQKlEAKlQC4CpQAAAABQGQiUAih3BErR3giUAgBKhUApAABAlSFQCiFQCuQiUAoAAAAAlYFAKYByd/uCbT5QevEfdY+uB0qNQCkAoFQIlAIAAFQZAqUQAqVALgKlAAAAAFAZCJQCKHe3zNzgA6W/17VfdD1QagRKAQClQqAUAACgyhAohRAoBXIRKAUAAACAykCgFEC5u2HCEh8ofWvvp6PrgVIjUAoAKBUCpQAAAFWGQCmEQCmQi0ApAAAAAFQGAqUAyt3b+o3xgdLrx86PrgdKjUApAKBUCJQCAABUGQKlEAKlQC4CpQAAAABQGQiUAih3r7u/sxv0hd+4e9Y0RtcDpUagFABQKgRKAQAAqgyBUgiBUiAXgVIAAAAAqAwESgFUgsdO/19X84nvuv/v/s7R9eWgy9hJbufLL7veU2dF17eFP3rmOdfwyik3aeUa/1xh3Kmr17lNBw64W/oMzmlfCu1xzFIjUAoAKBUCpQAAAFWGQCmEQCmQi0ApAAAAAFQGAqUAKsHti7b7ae+vHT0nur7YLnm4m1u7e4+rP3Hc3TngqWibG58Y4AOkdQ0N7qpuvVzPSdPdoVdfcYNmzYu2bwuxQOm82s1u77Gj7ra+Q3Pal0J7HLPUCJQCAEqFQCkAAECVIVAKIVAK5CJQCgAAAACVgUApgEpw8Qd7+FDfY6e/4gOLsTbFNmTOfHfkI2fcqEXLoutrJs9wL585nQlvloNkoLTU2vp47YVAKQCgVAiUAgAAVBkCpRACpUAuAqUAAAAAUBkIlAKoFLfOrvVVSu9cuie6vtje1XuQr7K5Ye8+d+kj3bPWKdS6YFOdO3jqpLt/+Oisde2JQGlpECgFAJQKgVIAAIAqQ6AUQqAUyEWgFAAAAAAqA4FSAJXkkQN/7QZ/8TV355Jd0fXFZKHR/SdPuHuHjspaZ2HTjfvqM2HTJ5+f7Y5+5Iz/19rd2meIW7Vzlzt8+lVf7XTXkSNuwMy5mSqrb/vwE+6l/Qc8/Wzb3Vwz0O9/2bZznzlf/EAXP52+9qF9aZ/a9001AzJtYgFP7UP70j7teOpnTLLvS7Zs89P4a92+48fc4Nnzfd+tf+G2Oq6Onzym7e/yzj3drA21PoSr9vp34oo1WWFdG8Ohcxa4ubWb/LF1rnUHG9w9g0dm2rU1AqUAgFIhUAoAAFBlCJRCCJQCuQiUAgAAAEBlIFAKoJJc9GBX9/CeT/hKpV2Pf97dMH6Ru+iBrtG2xWDT2ienve87bY4POg57YWFmWTJQekPP/m7H4cNu77FjbtTCpT6MqWqn2m702f0VGihViHPiyjW+Lwp5Dpw5141fvtqHOF+s35/ZtqVA6Zse6uoeGz/Vb2+Gz13kdh896gOeDz091m9zW9+hPriq5WHfFWLVuSvcqvN7oml8GpuON2PdRnfjEwPcJQ93yzmmnqt/6qcCorPWv+iPO/+lOr+/xXVbM9vZGO4/ccKt27PXH3fa2g3+nJJj1JYIlAIASoVAKQAAQJUhUAohUArkIlAKAAAAAJWBQCmASnTb/C2u36f/xwdLB/zDL3249L1rD7t3z6l17xy30F09fGqLLu05LLrv0Dse7eO2Njb6MKVV0lS4c9nW7b5i5x39h2faJgOlvabM9OHRATNeyLS57vG+btuhQ662fr+7rFOPggOl1zdtpzbzX9qcqW4qY5es8CHNruMm++ctBUptWUjB1CNnTvt/bdnIBUvczpdfdg+OaA6YigKjWrZm124fTNWytCnvk8ccMme+D48qIGptLCSr5QruapmNYRgyFYVQD546ldWffBQ8vnXWi+4P59UVhe6z+7eciR4LAIALQaAUAACgyhAohRAoBXIRKAUAAACAykCgFECluujBbu6WmRt8xdInPv4tN+SLr/ngX6FUdTK236Tpazf46p33Dx/tn9t09yt37MwEKyUZKO08ZqJrfPUVN3P9i1nTuocKDZSmSR6ztYHSR0aN9+e2eufurABnTKyvhQRKNUYKoSqMqmBs2O7OAU+5+hPHfbVSPU+ej1EoV8fR8cLlae7ffDp6zc9Xzz/7d/fYma9EjwUAwIUgUAoAAFBlCJRCCJQCuQiUAgAAAEBlIFAKoFq87gNd3O917ecuf3KUuypSkTSpkAqlYsFQVQPVc6u2aVU1TTIMqYDm1DXrffVPta/df8ANn7vQXd65Z2ab1gRKFUodu3SFX67KpzqWOZ9A6VXderlNBw76ae1v75c7FvcNG+XW79nrK6CGx2ptoDTtHMXWWQXYtECpnrcmUFqKCqUfoEIpAKAECJQCAABUGQKlEAKlQC4CpQAAAABQGQiUAkB+Cjpu3FfvXdGlxlcmVbVNTQEftksLQ6qdppBXoFTBUlUE7Tlpul+XFrZMBkoVTtVxFapUsPW2vkN9m6fmLco6ZqGBUk03P2PdRt+fATPnZpYbTaGv/Sjo+eFnJ/pt7xo4wtUdbMjq64UGSjWeCrUWO1BabKpm+971L0fXAQBwIQiUAgAAVBkCpRACpUAuAqUAAAAAUBkIlAJAyxTiVBC077Q5bt/xY5kp2kPJMOTV3Xq7G3r2dxc/0CXT5u5BI3xF0Nr6/e6yTj0KDpQ+OGJs0/Fzg5vJYxYaKFWgVWHSRZu3ZE3bb5Zs2ea30fT+tizW10ICpcWY8l7PCZQCAKoRgVIAAIAqc8Pco+7eoz+PrkPHQaAUyHVxp97+dXHXvm9H1wMAAAAAygOBUgBo2b1DR7n9J0+4rY2HXOMrp/w0+Mk2yTCkQpIKod4/fHSmjYUrLZSpSqErtu/MCW9qG21rgVILbk5ZvS7TRttOXrW21YFSVTfddeSI23H4sA+8WruQtlFw9pY+gzPLNC2+wrCtDZTq+ZA5832AdfDs+Zk26v/Epu20vGbyDL8sOYZGzwmUAgCqEYFSAACAKvMHyz7m7jnw/eg6dBwESoFcl3R/yr8u3rPnm9H1AAAAAIDyQKAUAFqmIKimnFfYsa6hwV3VrVdOm2QY8qGnVVX0pA9hjlq41A2cOddX/jxy5rSbvnZDZjuFLY80badp3xW4nLpmvd9O7SxQquPpuIdefcUHVW0KfW3XmkCpzkNVSbXvWetf9H0KdRo9IatP2w4d8seavaHW90nHCgOld/Qf7oOnmgpffbdQbDJQqvYv1u/3/bfj6jwUJl1ct9VP6a92yTE0ek6gFABQjQiUAgAAVJl3r/9bd+eOr0bXoeMgUArkurTPxOZA6a6vRdcDAAAAAMoDgVIAKIyFLJPVOE0sDHnP4JE+KKrgpNYpfKnp8y1AKfp5/PLVvvKptXlm4VK36cDBTKBUbqoZ4Fbv3O3DoNrfqp27XL/pc/x2Vrm0pUCpgp0KhOo4MXY8VQ9VQHT/iRN+ufqk81eoNqxcqnZjFi/3fVC/rNJoMlAql3fu6WYFwVT9O3HFGnfpI90zbQiUAgA6GgKlAAAAVeauvd9yt6z+8+g6dBwESoFcbx+1wr8u7tjypeh6AAAAAEB5IFAKAEB+BEoBAKVCoBQAAFQU/eWo/oI0/IvUqavXuU0HDmT++rQju/iRGh+WesekLdH16DiqKVBqf6EeTlvUntL+Ih3l76aFr/rXxR/W/n10PQAAAACgPBAoBQAgPwKlAIBSIVAKAACK6k0PdfXTi2hakPuHj462uRCxQOm82s1+2W19h+a0D8WmVak2V0/Y7MNSl/aZEF2PjqM9A6X2WlPoMlR/4ribvaHWXdm1JrpdmkoPlN7Rf7ifcmnjvvqsqZLQtl73gS7uvQ0/PBso/Wy0DQAAAACgPBAoBQAgPwKlAIBSIVAKAACK6t6ho9z+kyd82KoUwc1koDQmLXxW9YHS+zv7sNQ9B77vg1PRNugwyiFQumL7Tjdw5lxv8Oz5bnnT6/bImdOutn6/u6pbr+i2MRcSKC3F6761gdJRi5b59uqH+hNrUw1aOy5t7crRq/1rgkApAAAAAJQ/AqUAAORHoBQAUCoESgEAQFGNXbLC7T9xwtXuP+DqGhpaFRorBIHSdNdO3eWDUleN2xBdj46lHAKlsdea3iOOfOSM6zttTs66NJUcKFVFUlUm3dLQ6HYfPeqmr63e12c5B0pf/3AP997G/2pChVIAAAAAqAQESgEAyI9AKQCgVAiUAgCAolF4VCFSTXnf5/lZ7tCrr7geE6dltUkLhMZCX5rOXlUNFVBVSElTRo9fvsr/G26vn7VP7Vs/q23I9tmaYFmXsZNc3cEGX01RNh046O4bNiqn3U01A9zqnbt9m8OnX3Wrdu7y1Qh1HB0v2b5Ufr/3OPf+U791t9d9sWncPhxtg46lXAOlsXUKXU5cscYdPHUy81rXa1/vAVqfFii9tc8Qt2TLNv9ek9zO3mvC94Lk61Lb6zWr1669zu8ZPDKzXi7v3NPNq92c1UZ91/4KCU52HjPRNTYdd+gLC9yCTXVu58svu+sf75vTTn3uPuF5t+PwYR+41fHW7dnrbu83LKvdlV1rfH/Cc35m4VJ38QPnqhJf8nA3H9zVOrVRW22jba1N2pgm36PtuarLamxtn3pftrG2a6rlxt6TtY+scW5at+vIETdg5tzM9S29zu6W1X/R9Hp4zb1t+AICpQAAAABQAQiUAgCQH4FSAECpECgFAABFY8EpVR58x6N93NbGRh+gCkNDybCSiYXMFDhSAEmV/Ya9sNCNW7rS1Z847sNKaYHSq7v1dncNHOHDoKKfLSyVL+QWUv/D445csMSHvBR2e2TU+Ew7BWg1dbfCWrPWv+jDVQrTattkcK2U3jZikbvvxK/8VPcXP3IuMIaOrVwDpTWTZ7iXz5z2wWs9V/hxcd1W337a2g1+evylW7f78Obos21i4cfb+g71wURV/Ry1cKl//W3Yu8+//vQaVsDyhp793RNNx9P70ox1G92NTwzwxwu312tbr3G91rcdOuT2nzyRCY9b39QXBVfVt/kv1flj6H2okECpKpLq/eldvQe5ruMm+/PUGCTb2fudzkHnonPae+yY76P6qjb2nqN9jF++OmusJjaNtd5r3/RQV7do85asPmtctY22tarRrQ2UanuFYdUve0/UMl1rjZPG9ql5i/y46F+NvV0DtdW5hNdJwVK7vqX0+j/q6f6w9h/8a0FVnC96uDuBUgAAAACoAARKAQDIj0ApAKBUCJQCAICiUdBKgSMFi/RcQSpVs7uj//BMm0IDpdd2f9JtP3zYhzr1s7W7e9AIXxkvLVCq52lBqUICpVZlVVUILXglFj7T1NWqpqhlQ+bM96Gt4XMXZtop0KXgmgWtbHmxvfHDA9yVz65zt2/9kg9H3bHtK+7iTn2ibdExlVugVOHCDzYtVwBUrPLmgBkv+FC2gobWVgFFVf61ap6x17RCjVr/4Iixme303qNla3bt9sFKLYv1ResUtgz7IQqg1x8/7t/L9Pyx8VN9yFOv6TAYP2L+4oICpeq7+mPBent/UfDc+ie2PPmedf/w0T7Ibn3Xe476ozGzNtqv9q/3QIVWfbC/aTwnr1qb1WcLrKpyqZ6nvU+mBUrDYKsoHKvrpqrRtiw25X2vKTN9eDTs83VN46LwrgKul3XqkVleLK97oKu7tO8kd+OCU+7eoz9z73/ld+7KMc1jSKAUAAAAACoDgVIAAPIjUAoAKBUCpQAAoCgsyGVBLFGQ6+CpUz4EZcsKDZSqEqiqCibDn7HtixkotZCUVU8MqQqpqhfeO7S5eqGOq8DsLX0GZ7VTmKrQQOmbug52103f666bUZ/XO2c3upuXfdT94Yufce879CMfiJL3Nv6Xu2byNh+giu0fHdfrH+7h75H3n/qtu23z54vihrlHo8dKsteawoVJep946OnmEKgCj8u2bvfLktPA6zVor6O013RSrF3sda/gpd4zwvcrUVhc1TNt+ymr10Vfy7HgZIwqker9pMfEaZll6kcyaJ/2vqPQqUKtnUZP8D8rKBsbK51Pv+lz3DWPPZnzPmWsarTOT+eZNqZpgdLke7be93Qe4bjGxsUCrjOb+mVh/NZ4+zMrou+JSTfOP+Gntb9j65ea7vnfnH2PfM29e/3fut/vPS6zPwKlAAAAAFAZCJQCAJDfgL//uXvfhiPRdQAAXAgCpQAAoCgUGlUAVOEhW6bwkCp6WoBJywoNlKYFtkodKE07rq3TVN0Kh6Udw9rFQmgxbx+9yoeeLCCa5v0nf+PuafhPd+fOr7pb1/21u276PnfZkBfc6z5AkBRxF3+wxt2979vR++l83Xf8l9FjJdlrbcX2nX7KdVmwaYt//YSVKu11pNdcjL2O0l5vmpp+/Z69PowZbhe2i73ubVm4Tci2T763mHzvE0YBUFUiVeXRsNqxVR0Nw6OF7C9tDJLS+mzrLJCatr/ke2zsPTdc3lKgVNVmp65Z76s5q0JqbdPxVNX58s49M23yuavAe/jeIz9tavsdd9vmL7ibFp92V43b4C557Fxo1xAoBQAAAIDKQKAUAID8+vzVf7oPbPlIdB0AABeCQCkAALhgFpyyMFaSwlMKUaltWjgpGfpKC1jFttfPWqZ1ep4WlIoFy5LSjisKwhU7UAqUSrlNeW9VjMPXjL2ONJ26QoYWPjVWdTP2elNVTx1DgfUPPzvRv/41ZX3dweyp42N9sWVh4DWkqqB6X0u+t5h87xNGFUhVwdPeB5MUtregfSH7i41BTFqfVQ1W59vWgVKj6z9ywRIfKFWwVP9d6Dlpek67UiNQCgAAAACVgUApAAD59fjTf3MfrP+L6DoAAC4EgVIAAHDBrOKeAkfJYNYzC5f6ddPXbvBt08JJydCXTXk/fvmqrHax7fWzlmmdnqcFpWLBsqTWTHm/ZMs2f1xNNx22U5hKx9HxwuVAWyq3QKmMXbIiq0ppOI27Aodh21DsNR17/cXaxfpy54CnXP2J427BpjoftLTlSZry/uCpU+7BEc1T9JtCAqA6V031PmHF6pz3xdkbav06q+hs73fDXliYtQ/17brH+7qru/XOBEI1bb2mrw/bKZiq8VM10Laa8t6WtxQoVd9v6NnfXfxAl8yyuweNcLuPHnW19fvdZZ16ZJa3BQKlAAAAAFAZCJQCAJDfIwf+2j3+0a9H1wEAcCEIlAIAgAum4FRagNICYzbt8xVdatymAwdzQkw1k2f4oJmFk9RW26htbLroUgVK0457W9+hvopiWFVQ0/xrGmdVVrR2Cn3NWLeRQCnaXTkGSq1KaTgNvKpW6nWk95Ew3HlLn8Guy9hJflnsNa3XvSqAqp1tc3u/YT6oGLaL9UWv4bW79/jgZTIs2mnMBL8f/azAp4Kfk1etzeqb+povUKr9670i+R5kLNBqQftruz/pth8+nPX+Iqq4Wn/8XDuNlap76v3S2th7jsZCVVHT+jxg5ly/rfpu2ymgqvfOMJSbfI+90EDp/Jfq/P6sSrXYfxfSxqeUCJQCAAAAQGUgUAoAQH53Lt3tBn/xNff6hx+LrgcA4HwRKAUAABfEApj5gkGquqewqIWgFI468pEzbunW7b5anwJHqs6nZWE4yQJQWxoa/T7GLV3pQ1MKn+ULlNoU/AoxPbtkues0eoJfbsGytGmurV3faXOyjqsQ147Dh/3+VEnQjqtzV4U9VTRVVcDBs+f742pbAqVob+UYKBUFGvVaVyBbz+11pNeN3gv0WtRrXYFLTY+usKXeW/QeE77P+EB30362HTrkX6Oq+qnXqAKNYTubel5T4es1auFJvZbVXsfR8XRcHV+vZ1UmVUVNVfxcXLfVv+eoImrYRsdOC5RaqDN2/hKrzGrvd6ogqn6OWrjUh2MVZFegXW1srDS245c3Vz5Vv/T+OrHpWAqJat+LNm/J6vO0pvdcbaNtLcgrNoZ2zKlr1vsxCd9jWxMotfPWufWbPsdd89iT7qGnx/p96lx0TtZnHcOCsm2JQCkAAAAAVAYCpQAA5HfNM7PckH927uap2TP9lQt9Xj119Tq36cCBrMIQ1aKSzi/2HQvy0zVdt2ev/y5D3yNp5rVYO6BaESgFAAAXpNu4KXmDU5KcXvryzj3dvNrNPjylMJPCmkPnLMgJJ6mtQk77T5zwITGFwjSFvqqH5guUikJMCmtpO4WrtMxCbloWE+5TlREVQNMvCqJj3jcsewppualmgFu9c7dvo/NZtXOXny6fQCnaW7kGSq1KqQLbCotqmd4TZgWBUP2rgOiVXWv8+tiHHbH3BwUkFerWz/YBjtqNWby8ObTe9DoNq3veM3ikD1Pae5HCm4Nmzcuanj3r/erse4GC5jq/WKBUxyukSnEyaK/tuk943r8fqi/2fnJrnyFZ22lM1B+FWu289f4Z9llBWAV3tU5t1Fbb2HiG7RRM1djYvpLvsa0JlPop95uum/p+4ORJ98CIZ/3ycJztOOqf2tu2bYVAKQAAAABUBgKlAADkd9GD3Vy/v/uJ6/7HX42uL4WWvmcz+jxZn3nrc2l9jmxFE9qSvufbfPBg5nPp5PcOF6q9z681CJS2jmaSU5hU97oKdvSaMjNrdjmgIyBQCgAAUGTjl69qMUwGlFp7BkqBckWgFAAAAAAqA4FSAABadveqA75K6fXPzouuLzbNSqXZqTQTlajggAoaJGcGtBkB24vNCKYCDio4oT7Z7GPbm5ZVY8XUfC4kUJqvgMj5qIRw671DR7n9J0/42Slj64GOgEApAADAedLU0apEGE4hrb9Q27iv3ldgvP7xvlntgbZEoBTIRaAUAAAAACoDgVIAAFp28Qd7uF5//j3X929/5N74oeJU3myNYocNi0Hf2dU1NGTNkmZs1sXpazdkLa92BEpbpxzva6CtESgFAAA4T5rGec/Ro/4vHEcuWOKn39YUCJoWW9NIx7YB2gqBUiAXgVIAAAAAqAwESgEAKMzlfUe7QV/4jXvi499yl/Zs26nXWwreaep7TQl/c81A//zJ52f76edHLVrmVu3c5auI6ju12v0H3O39hvnv3TYdOOiXaZ3a3FQzIGufKuwyccUaP4W99rXv+DH//ZymoNd6HUvH1LHD7URhU32Pp4qqb/3Q437ZxQ90cUPnLPD7sanx9R3fJQ938+stAKl+6Twbm87XwpDJ8ytkf7E2qpy6ZMu2nHM9X48+N8UXvrHjK0CrkG0yxHlrnyH+uDp+cixtHLXc6FrrmmtbtenWdJy6gw3+eonGSNfQ9p+k8Qr3JxrTfGOssRo0a57bdeSIO9LUPnZf2H2lMZ1bu8mfj9qqb8n+6Jwz915TG+1XFW3t/knro21/37BRee9RtdU4jVq41F+DcMyASkKgFAAA4AKEv2zFfvEA2guBUiAXgVIAAAAAqAwESgEAKNyVQye5fp/5iev/2Z+7e9Y0ukt7DIm2K7bzDZQqiKfv1mwqej3ff+KEn2Zcz8Plavemh7r67RXKXFy31R9z2toNvt3Srdt9uG/0omW+jVUo1f4eGPFspi8x+i5vYlPf9R2fHXfWhlp/3Hm1m/16CzvqGDquAoQKtCrYmjy/Qvanduqr9qe+q40Cp9q3Ctjc0LN/Vh9b65FR432IdPfRoz7UqII42q/GPQyU3tZ3qP9O09opSLph7z7f177T5vggp/ryxOQZPuCpGRtvfGJAJhir70J1ngroalvtY++xYz5EeUufwVl9Mld36+3uGjjCBz1FP6s/aWP8lg897sfz5abldr/YWL1Yvz9zLnZf6Zpbf3R/qF14zjofjYX6GZ6zvt+1+0d9DM9Z19a2t7FVAHrYCwsz5xxeN70WLPiqY89/abMPS2sdUEkIlAIAAABViEApkItAKQAAAABUBgKlAAC0jkKkH2r8Wzfkn51X84nvuod2fdzdsXinu3nqKnfNM7Pc1cOn5nXVsCnuogebw5uFON9A6eRVazPhSv07e0NtznKFSNfs2u0DjwoyatmAGS/4EKOCgHouCjiu3rnbBxmvf7yvX2bBP4X61IdOYyZkVQg11v9kfxQkVLj13qGjMmFH7U/7DbdPnl8h+7usUw9XW7/fV7m8oktNZl8j5i/2QcReU2ZmlrWWQq5rd+9x9ceP+7CmLb+uaVy2HTqUFa5U0FRj9uCIsZl2Gmct07hbiDd2jVXdVVVeNe62P6mZPMOHPxW2tGVJNp5hX9LGWNdTyxXKtPGUsUtW+Pug67jJ/rndVwobh9d51voXm/Z5KnOOGluNse4ja2Njo2uia6NlsXO2Puq6KbRsy7uNm+Iam/qiqrt6boFSBWHDPgOVhkApAAAAUIUIlAK5CJQCAAAAQGUgUAoAwPl5a++n3fvWH3Fdjn/eVyy1gGmhbl+wLbrfmPMNlOrfsJ22V0XIZGBTy217hfOWbd2eFRw1CvOpH+qPLbMZBhUq1TH1r54npye3oKctkx4Tp/lgpPoZC0Ca5PkVsj8FNVfu2Om3C8OcxaDKoJq2XuMUhhnznUMo1q6laxwqpG3sGLFl+STvo+Rzo+BoeF90HjPRhz9nrn/Rh2/DtqHYedgyC44a3Yu6J3Uv6Lm2CY8JVCoCpQAAAEAVIlAK5CJQCgAAAACVgUApAADF8YZHerhLew5zVw6ZFK1KGmqrCqXJ4F9aCE/LbXsLHWr7mLQQnypWdhk7yVfT1JTqmqL8/qfG+HXqX2xfRv3MF3ZMnl8h+1M7BU4VQtQyBUBVgfO+YaNSK1rGzj025mnXI+0cdMz1e/b6ap/hvsN2afvUlPiDZs3z071rXMPtY30zsb6k9U8U/By7dIUfZ1X+DI9j45l2X+l5eF/oXpi6Zr3vrwLGmrp++NyF7vLOPbO2i52zHSMNgVJUGwKlAAAAQBUiUArkIlAKAAAAAJWBQCkAAOUvLWxoShEo3XXkiA8BDpw5N0u/6XPcNY89mbV9koKlOo5V8FT/NM36MwuX5uxP3tV7UN6wYyxQ2tL+bFuFGzUVv8KkCpUqLKnKpcljiKqaPjZ+ata+klVQJe16xM5B08Wr7Ya9+9yHn53oz0HT5NcdbMhqF9unxm762g0+iKrp/O8e9LTf/onJM3yl2bT7QWJ9iS0TjZHGRMfXNPe39R3qj/PUvEVZ91HafaXn2jZ5X2lqf035r0CpgqW6Zj0nTc+sj52zHWP2htqs62A6jZ7g22mb2DGBSkOgFAAAAKhCBEqBXARKAQAAAKAyECgFAKD8pQUYTTEDpQpVrtm121f2VCAwbBcaOmdB0zbH/DTzyXXJ4OKs9S/6MOH9w0fntDVpYUdJnl8h+1NIUv0P96Vqn75yZtPY9JoyM6t9a9iU95raP1weOwe1Ud/DkGusXewaq/+7jx7NmVq/pftBYseILZMHR4xtGs/c/SXvo7T7Ss/D++rqbr3dDT37+/G2NncPGuHPpbZ+v7usUw+/LHYeCuAqQKtgqy2L0TaxexmoNARKAQAAgCpEoBTIRaAUAAAAACoDgVIAAMpfSwHCYgZK9VxVJTVduUJ9YZBRQUpVH9Uy65PCjgpvWhtRBc7648d9MNWqfqpC5fyX6rJChld16+WrbWpZWthRkudXyP5UWXT/yRNuwaa6rHOwwGJybFpD56SKngpI3t5vWGa5AqAK4obnoL4rfKqxs3baRtuG7WLXWOer816xfWfWOQyePd+93HR90u4HiY1nbJnYsaesXpdZpuNNXrX2vAKlui7JwK8FlVs652u7P+m2Hz7sx/GmmnOBZvVH1U2tOm7avQxUGgKlAAAAQBUiUArkIlAKAAAAAJWBQCkAAOUvFrwLFTtQqmCmKklaaFNTjY9butLVnzjupy9X6E8Bv/HLV/vg6Y7Dh92Yxct9O4UStS8FCh8ZNd7vT4HTxXVbM9PNKxCp0Kq2U7DyPQOeSg07SvL8CtmfAoyLNm/x/VOVUPVt1MKlfr0Cnnf0H551jNbSuekctT/tV8dXEFLHC89hyJz5vp/bDh3ybTSVu7bT9QnbqT/ql6bC1/mooqkFV7XP5U1jMGLeYrd6527/XNun3Q9i2+pYzy5Z7qeKTxtjXe+6hgYftNX1Vj91ndXv8wmUPvS0Kp6eGxuNva6B+q0p/G27tPt6QFN73Xu6nuqLxkPnov5pPNUm7V4GKg2BUgAAAKAKESgFchEoBQAAAIDKQKAUAIDy19aBUrm8c083Kwg/6l+FIa/sWpNpo1Cpgorr9+z1YT+10796ft+wUZl2ohCoKp4qNGntFDK8tc8Qvz4t7CjJ85OW9hdro5Dihr373D2DR2baXIhHn5viK2na+Chgu2rHrqxz0BgpELn/xAnfTn1RKFIBSf1slUvVTqHcxqbro+BlzeQZfrmuw7zazb7vWr7pwEH3+MRpfttk5dIkBTutfwrX5htjVQO1sKqOtWrnLtdv+hzfH6tcWmigVDTGGmvty85b10LXxNrku69VCVfhWvVH9HO3pvG28yVQimpBoBQAAACoQgRKgVwESgEAAACgMhAoBQAAAID2QaAUAAAAqEIESoFcBEoBAAAAoDIQKAUAAACA9kGgFAAAAKhCBEqBXARKAQAAAKAyECgFAAAAgPZBoBQAAACoQgRKgVwESgEAAACgMhAoBQAAAID2QaAUAAAAqEIESoFcBEoBAAAAoDIQKAUAAACA9kGgFAAAAKhCBEqBXARKAQAAAKAyECgFAAAAgPZBoBQAAACoQgRKgVwESgEAAACgMhAoBQAAAID2QaAUAAAAqEIESoFcBEoBAAAAoDIQKAUAAACA9kGgFAAAAKhCBEqBXARKAQAAAKAyECgFAAAAgPZBoBQAAACoQgRKgVwESgEAAACgMhAoBQAAAID2QaAUAAAAqEIESoFcBEoBAAAAoDIQKAUAAACA9kGgFAAAAKhCBEqBXARKAQAAAKAyECgFAAAAgPZBoBQAAACoQgRKgVwESgEAAACgMhAoBQAAAID2QaAUAAAAqEIESoFcBEoBAAAAoDIQKAUAAACA9kGgFAAAAKhCBEr/f/b+PNyu8sDvfP9yTJWdlKvscnl2bFdsHGwXJoVNYVwmDJIZBUIgJoEMAgsLMYMQIAkkBolBIEBikISQkIQQEojBfVPpupmnrkzVT4bufvI8PSV9q5NObvp2pe5NUp1a9/xe/B6vs8/aR0egc4629uePzyOdtdZea+211t5+fPTlfWE8QSkAAMBgEJQCAADMDEEpAAAchQSlMJ6gFAAAYDAISgEAAGaGoBQAAI5CglIYT1AKAAAwGASlAAAAM0NQCgAARyFBKYwnKAUAABgMglIAAICZISgFAICjkKAUxhOUAgAADAZBKQAAwMwQlAIAwFFIUArjCUoBAAAGg6AUAABgZghKAQDgKCQohfEEpQAAAINBUAoAADAzBKUAAHAUEpTCeIJSAACAwSAoBQAAmBmCUgAAOAoJSmE8QSkAAMBgEJQCAADMDEEpAAAchQSlMJ6gFAAAYDAISgEAAGaGoBQAAI5CH/7+uSWc++xNmzrXwzASlAIAAAwGQSkAAMDMEJQCAMBR6BdnXVbCuU9d/2jnehhGglIAAIDBICgFAACYGYJSAAA4Cv2ZOT8u4dwnFqzoXA/DSFAKAAAwGASlAAAAM0NQCgAAR6FP/mh1Cef+9LnXdK6HYSQoBQAAGAyCUgAAgJkhKAWAD+Ci2+5uXv/pu+XPrvUcHjc98miz660DzW9f+5PO9XwwZy6+qdn82mvNhbcu61w/HXJvc49zr/Pzh046vbl13ePNM6+80nxt7uXjtp8KHzv17Ob+Zzc16zZvbX71jPM7txkkX151oDlx3x91roNhJSgFAAAYDIJSAACAmSEoBeCo9eDzL5bYs23viG37Xm8Wr364+ej3Z3e+7lAISqeHoHTy8lw/tvWlZvsb+5tvzb+6c5svnz+/BKQbd+1qPj37gua8m+5oXn37reayZfd1bj8duoLS+zY8Wz6vx827ctz2UyFB6eMvbWueeWVnuS5d2wyKD508q/nNPf+++fMb/kHnehhWglIAAIDBICgFAACYGYJSAI5aCUp3HnizuXbVmubSu+4tFtyzqnlq+44Slj6w8blDikq74tGjOSjN9UvM9+tzLu1cP50EpYfmiuUryjO+6P4HO9fPufnO5rV33xmNN48EvUHpVJt2gSrfAAD/9ElEQVTu4023T/34sRLN5c+u9TCsBKUAAACDQVAKAAAwMwSlABy1+gWRiUgTk+5++63m9OuXjlk3EUHpzBGUHpqvXnhZuXfrt71cRt1sr8vInyuf2Vhi65Ouum7MupkkKD18PvyDOc1f2P3vRvzb5kO/dWbnNjCsBKUAAACDQVAKAAAwMwSlABy1JgoiZ91wc5niOzHZvNuXd47meO7S28sojque3VTCs4SjVd1vDUqvXL6yuXfDM2Wf2dfGnbuaEy6/Zsz+ErJmqv2X9+8rr8m2mdL7U7PmjG6TfWbfD42c++V3rxjddscbb5SfEwNmu1894/wy0upzu3c3nz/7ovLz0zteKebetqx5cc+ech67R857yUPrxo3EeuzcK5q1m7c0e955u9k78h4zxXc931yT+j6rXMs6TfrqTc+P2deyJ54aN736iVcuana8+caYWC/7T+BYjjmyz5zj2TfeNvqeItvnWi9ataYcq0akdXk7KJ1/173lGt689rESTa7bvLUc8zsLFo1uk6ncc7wVT29sjjl5eMK6Go3meuRetNfV2PTJl7ePxqZdYfSYZ2Rk3Za9e8s1r/erPqt5NuproivUzLXPdPrZR/aVfWbfX5kzf8LXtT/D9RnvfTar3nPPc5rnI+vyOaqfn3re7dfWZ6v9Ocrf6/4+cfp5zbL1G0qEm+3z59KHHx0T6x7Kd8GUGnmPxz72t5qT3v6T5lfm39G9DQwxQSkAAMBgEJQCAADMDEEpAEetiYLSdrxWQ8ne0RwTSibI+97CxWWbq++7vwRj+fNL511SIrkakSX4fPylbSVau/2x9WXf7SjtI6fMau5/dlOJNxO6Zfr9ut2G7TuaT8++oGxXY7cszzklrLxm5eoSX9boLdtlvznfxGrtoDQB6fb9+5slD60t0/tvGFmWKHb+nfeMvq/j5l1Zwr7sM/vOds+/+upojJl95TwSaCbEy/v/zOwLS4z38AubS8T62R/OHT2PHDfhXMLceowcL0Fdwt38fMqPFpcIb+vrr5f3VM8tYWEixfq63I8aHGa/K55+tvnGxQvGBaWnLlpS9peRZmssm3PPe3jwuRfKsnpuuY5fm3v56DGGRZ3WvjeUrgF17kFd1huU5vnO87Ft375yv/Jc53nL66772f7qs3qwoDTPzdKRv+dc6rOfyDnbJIrOfep6XbQ/w/kMnbXk1vL66qp77y/PVJ6FPGN5TX2+67NWzz3PVN57Prd5f+ePXJ98Xu58/Mny+W4/M1HPK3/mPPM85zshx02gnP21n7/JfhdMpWNOndMc9+zvl1jucze/0LkNDDtBKQAAwGAQlAIAAMwMQSkAR62JgtIrlq8YjerqaI7tUTYTeG7ctauEaBON4liXtcOySHi288CBEmPm50ytnyn2M5pmHeExElMmTMvIpfm5RnoJ4hLG1e3qiKoJReuythrCJaxLbFmX5/3kfdVRRRPl5e+J7RJq1u2Ov3RhCVETytVlXdcvcWL7fWXK9Lwu0VxCzvresp+EnF88Z1455iMvbh53zFzjxLTZLkFfltWgNAFi+zq1g9Iak7ZD3CqhYq5TRia96t5V5dom7GtvMywS/Sb+bT/Duaa5TwmFv3nJVaPb9j7bF9xyV7kP7RD5CyP3MuFxrvvHTzt30kFpnoE8m4mD2/c0z3w7Oj5YUFqXteV+J9LOn3VZIuk8U/UZjRqNP7pla3kes6zreF1Bab4rep+jvI88o1mecDfLJvtdcLh96ORZzcfm3dx8edWB5sTX/0Pz3QP/d/Op63/+noCxBKUAAACDQVAKAAAwMwSlABy1umK0jE6Y2DAjL7ZHrqzT29dRG2tsVkPP6I3u+i2LhHg1gMzPdbTT3unHe6O/fpFezjMRYDt+a+sK4eKTZ84p09nX/dXpztvhaOTYOYf267uuX70u9TolMM3537Lu8dGAtJ5LDUxr1Np7zMh+ct1z/fNzOxxtb1eXX3zH8nKcjG76ubMuGrNNJDDN+926d28JXTPKajvum8gvz7+9+cId2454f+aCyYeJdzy2vsS3CX/zc73/CXxrWBm9z3ENoO8aeW7bo/a2TTYo7af3mF2v63oGqxoWT+Yed30+uo7Xu12uUSLU+mzX7aL3ue59P1Xvd8FEfm3RI80X7tx+UF9e+WbztXV/vfnmc/+8+e6BPy5xXKa4P/aJv9P80gU3dO4beI+gFAAAYDAISgEAAGaGoBSAo1ZitAReXTJaZp0iOxKLtUcwfG8kzp+HeNEVjPWLyPJzOyKbKIzLuhqs9Yv06vJ+kV5XMNdeXvdXI7r2tWhrv77rnGsAm1FOa2yXoC77TVyX0SZraNcb53ade11Xp2XPNl3xXZZnNMtcp5xn70ivbbNvuKXEkHUK/65tunxx2c6fhXmH1wmv/O+dy9+vzy4dH+b2U8PQGkbX0TbrqJpV73OcQPPWR58oo39m+wS8GfH1E6efN/qafs9q1/1OlLp4zcNl+4x82n7m6jG7Xtf1DMZoONwz6m2V+/7ES9vKM9M+Vvv57jpe7+eo9+e6XXvbGoP3XsMqP3c9012+veMPOu95rxP3/mFz/Mv/sjnu2f+2+cr97zSfvmF989Gzxl8HYDxBKQAAwGAQlAIAAMwMQSkAR63EaIlCr121prn0rnuLjHD5G/OvLiOV9m6fUUQzCmhGEU0k9uTL28eMztgVjE02IusXxiWKfPiFzdMelOaY9Zq0nbXk1tGRK/udcwLSRKXfv+aGEvQlTqzHyTXMdOnt0Vi7wr0qo0zuHlk3maA01znTrWc01Lwm4Wh7m2re7ctLBJl7357+f1ISqR7pus67jzy/eY4jo9VmZNI8a5kCvr1dv+c422UK+QSl9ZpmhN+s6/es9t7vxKk5bpYlbD1u3pXltVffd/+YY3Y9J13PYD4zdz7+ZDmf+SPPbF1eJWrOfvIZPuPHS8trj790YbNx564xn4+u4/V+jnp/rttFHf33cAalwNQTlAIAAAwGQSkAAMDMEJQCcNTqF0T2kxAtozne/tj6MsJme7r76ArGJhuRfdAp7+vydvzW1i98q8vr/urooSuf2dh3hM+q3/VLsJmwMFFfgtJMo57lmV49x8q+2+dxuKa8T0SaQDTX6fGXtpUw8mtzLx+z3ZfOu6R5cc+e5vlXX2227N3bPLV9x7gQcNjkOc79yn1LMN11H3qf48/MvrBcy3Z4/e3LFpb7naj346ed2/dZ7Q01v7dw8cjxxwfFvcfsCjy7nsEErYlJ739205hp+6uMnpvX1Ocyuj4fXcfr3e5wTHmfn7ueaWBmCEoBAAAGg6AUgMPhhMuvaTbu2lVmUetaDwCMJygF4KjVL4jsJ9No5/9UJlZL/Nme7j66grHJRmR16vHeqdozwmKOV+PVfpFeXd4b5VVdwVx7ed1fYszHtr5U3l9Cv7pdnHb9jWOmD+93/WpIl/eX2K5GfQlys99IQFu3z/qMUNk7PXmud+LE9oiZeX9d8V3v8lN+lEjxzeaBjc+VETCzrI5cmeuc0Uvn33lPubbX/Wz002GViDn35Lndr5YoN89i7za9z3EiyVzf9megxpX1GasjdPY+cxmxNpFwfVZruJmRZes2uVf5LBxqUJrRTRMKJxpO8Fq3a8trEs62Y+M8d3n+2ufadbz6eWlvd8XyFeU5uvzuFaPb5fyXjrwuy/N+s6z3Glb5ueuZBmaGoBQAAGAwCEoBjkxnLr6peXbnzvL78fxOPP+WcPf6Dc2nZs3p3H6mfWfBovJvWrc++kTn+kNR/10h77tL77+XAMCgEpQCcNTqF0ROJKNs5v/09U53HzUKTVSXqfM/d9ZFk47IEuNlRMW9775TRlDM9PIZCTXbJKpMXJntJhuU5twe3fJSmYo8r+0K4aIub+8vo3zm/+Dn/0DfsOaRci4JCF8deW+J/uqolLkW+YXArSPL2lPh59gZUTXvOyOM1v1mBMfEoe0RR6sagCbqW7RqTXldnUa9PW15bzg60fIlD60bE/qddt2NZZv7Njxbgr+Epus2b222799fpjyvrxs2NejN/UowXZ+1tt7nuPd+5RnJc5vnN89FfV3+vnfkdWuee2H0OUq0mmX1Wa2hdp6vrK9T6Geb9jHrL2LagWf7M9z+DCVYzvHacv/zmgSg2XdGqc2x8ousvJccq/35+OYlV5XwNFPh5xnKiKZdn6P8mZFuc/71uHkfefbaQXPvNazyc9czDcwMQSkAAMBgEJQCHHnqICkZ9OH6Bx4a/X15fn/+wsiy3lnljlT9fp9/MPXfMR5+YfOYf5+o2v+WNp26/m0DAD4IQSkAR633E5QmtkwQ1zvdfSQcW7Z+Q/k/y6+8+WZz8sIfH1JEltdnv4nY8pr8H+zEj+3/anOyQWmN3DJd/ufPvqjv/1msy3v3lyk+EoXmvSS+y6iPly27b8wU5/k//hmBMuuzbaY5r+tyHhn1sncK//ziIOfZnm686j1mfuFw9o23jRmxtSsc7be8XoOcx1lLbithbm88Wv/PfcLSGv4NoxpZtmPNtq7nuH2/si7PbZ7f9nX8xOnnlWe4fU+vXL5yzLMaX5kzv9yDxKDZdu3mLSXKzmetjlxa71X7de3PcH2Wcy5d6jOe5ymB6I433ijLc955/4lq2yOXZrv8wqsEsCPnlZFG+32O8j7z2a9hav5c+vCjY6LzQ/kuAGaOoBQAAGAwCEoBjix18IhNu977d6n2uswalwFZ2gNSHMn6/T7/YLr+HeNI0O/fNgDg/RKUAkBLwrMEir3T3QMAg09QCgAAMBgEpQBHln4DokRi08df2lZG7vyVv3hOWZYBTDIARR1kJQM1ZOa53sE/MiV9Gdzk3XeKzCqWWdTq+n7xZ+9AJPXnzLqW2fTqunYEWv+e/VV5Tydc9qMSY+Z1mY2vHqPO2JflXz5//qSC0jrbYe/ANRmgJf/+mJnQ6rL2e6+DcWSAjrq+vqe5ty0r68rgHiPbZgCPul3uR/v9RPv8uq7vmYtvGl0PAF0EpQDwM/k/sfk/YV3T3QMAg09QCgAAMBgEpQBHljpCaWYHywx+XdtUmSFs6SOPlpn6MrNdpoOvMwBm1rM6c11mDUxo+vyrrzYL7llV5O9ZlnXZ5lCC0syklmMkDl3x9LPNNy5eMCYCzb8DJgy9+r77yz7z55fOu6TErwlAX3v3nebcpbePHuNb869utr+xv7yH/DyZoLSOFpoQtf1vjXlvuR6zbri5/Fzf+4aRbbMuIey2ffvKTHA5p2yT4+Scst3qTc+X65hzyXvMz5le/zOzLywz9yUUjfy9jlKamDQRa+/1zevPu+mO0XMDgF6CUgCGXv4PXf6PaKbEzv+Zm3f78s7tAIDBJigFAAAYDIJSgCNPjSATJGZkzNOuv3HciKNRw8ub1z42Go/mzzsff7IEjhmtMzFk/l1u6969zdcvumL0tQk+MyLog8+9UF5zqEFpQtZ6zOiKQLv2mZkL897aI4hmVsO81xqZTiYojayv7zM/1/eaIDdhbo1OM3Jofq6vm33DLWV000X3Pzi6n973lH09umVrs/X118u1yrJ+U94veWhts3vkfGucG9++bGGJghOk1mUA0EtQCsDQy3/pt2Xk/7Dm/1Tlv0DMf4nYtR0AMNgEpQAAAINBUApw6BId/vKcq5svL13TfHvtK81JG99pfrDjbzZn7v/9ZtZBnPzCX+rcZ69j515RYsSEloky62iZvVO1t4PKKmFmRtxMyNk7+mfbadfd2Jy15NYSTx5KUNr+uZpsUFqnt39u9+7msz+cW65lotY63X17X3ltr0yd/+tzLi3b1Tg1I4Lm569eeFlZf8dj68fsp4ajVabbLzHt8y+Wn/u9p1vWPT7meP2C0hrEZvRT//YJwKEQlAIAADAUBKUAAACDQVAKMHm/cOq5zXErn2vm/q1/21zxT5v3/JM/aeb9vf+zOfd3/sfOgLTXZIPSKiOTnrn4pmbd5q3N3nffKdO1n3T19WVdgsiu6LJKyNkVenaZrqA0EnjWaelr3LnymY2jo4PWfT38wuYy/XzbBbfcNTrFff588uXtZSTRRLFzbr6z2XngQHP69UvL+nr8fg4WlGb5ZILST5x+XoliM8pp3lfOJ8fuGlUWANoEpQAAAAwFQSkAAMBgEJQCTM7H517bzP3b74Wk5//u/9p8a/WW5jNX3dp8+PtndW4/FRKWJnys09QniMwIndeuWjMuvIyM2NkVenbpF3/2xpb94suu4/TbZx01NSOJZjTVRJiJQev6yZ5zZEbEl/fva755yVVlFNY63X3W1ePfvX5D5/XJCK3Zrt97yvLJBKWR+/GNixc0Sx5aV0ZfTfyb6fJr/AsAXQSlAAAADAVBKQAAwGAQlAIc3GcX3tFc+o/+Y3PR3/l/l4i0a5vD5crlK8sopAkte9f1Bo3LnniqBKWZ+r132+prcy8vwWW27V33mdkXNl84Z16JIaczKM1oohnFM/Hn/c9uGjPdfRxKUJr3nmn/f7TigRJy1unuIyOgJlZNdNp+Ta9+7ynLDxaU5trlGtbrWF9b49/Vm54fXQYAvQSlAAAADAVBKQAAwGAQlAJM7GPnXtFc8vf/Q3PuX/6fmo+cMbdzm8OpxpQZhbR3yvTjL13YbN+/f3SK97OW3NrseeftMjLnMSefObpdRug8/+Y7y7JMC//Y1pfGRZufP/uiZtOu3c0jL24u+zp10ZJmd0/EmUAyU9G3Y8t+8WVXBNovKI0rlq9oXnv3nXL+7enuo2tf/eT9rd/2chnxNNrnlff4wp495b1/Zc7P33uOdd5NdzSfO+ui8nO/95TlBwtK6/F7r+9nfzi3BK51Wn0A6CIoBQAAYCgISgEAAAaDoBRgYme98983l/3+Hze/fP5VnesPt8SOmTY9U6a/uGdPc/0DD5Xp2W9Z93iJGzMiaeLPbJvg9IGNzzV7f/puCUMvv3tFc83K1eV1mW79N+ZfXbbL9nnd86++2iy4Z1WRmLS9r0SoGTE0YWWOn22e2r6jBJ/t2PJQgtLTr1/a7H77rRLAXnzH8tGAMzJFfUZOTVTanu4+6r4efmHzuGnq44Jb7iohZ91+0f0PlnC1HXpW80e2z3vINcm1yTXKtcrIpYlas81kg9KEt3ltrtuPVz80OmX+vNuXl2Pkmua65RiPv7St3MN6DADoIigFAABgKAhKAQAABoOgFKC/Ty+4pbninzbN8Q9v71w/VRKVJlZ84qVtJXxMLJk/8/N3Fiwas22i0kzpnjizbpdp1o+de8WY7fK6Z17ZWSLHyN9793XC5dc0G3fuKoFqAsm1m7eUWPP9BqU5t2XrN5R9vfLmm83JC388ui7vMcFo78ieUfeV99OlHXnGiVcuKtPet4/dlunny/v62XvP32f/5JbRUVEnG5TGKT9aXM4555Hp+uvyMccYWbdl794Ss9ZjAEAXQSkAAABDQVAKAAAwGASlAP391qZ3y3T3x/z2z0fD5PCoI6L2TncPAMNEUAoAAMBQEJQCAAAMBkEpQH8X/o1/03zvxd/pXMcHM/uGW5qdBw6Mm+4eAIaJoBQAAIChICgFAAAYDIJSgG4fO/eKMt391+5a37me9yfT09+w5pFm+xv7m6e272h+9YzzO7cDgGEgKAUAAGAoCEoBAAAGg6AUoNvnr1tegtLPXHVr53ren8Ske999p3nmlZ3NNy5e0LkNAAwLQSkAAABDQVAKAAAwGASlAN2+csvDJSj9lQsWdq4HAPigBKUAAAAMBUEpAADAYBCUAnT7+j3PlKD0F0+/oHM9AMAHJSgFAABgKAhKAQAABoOgFKDbN+5/oQSlH/7+WZ3rAQA+KEEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpAAAAQ0FQCgAAMBgEpQDdBKUAwFQTlAIAADAUBKUAAACDQVAK0E1QCgBMNUEpADCtzlx8U7P5tdeaC29d1rkeAKaKoBQAAGAwCEoBuglKAYCpJigFgAGXQPPZnTubPe+83bz+03ebnQfebO5ev6H51Kw5ndvPtPNuuqN59e23msuW3de5/lDc9Mij5T33k/VdrwNgOAlKAQAABoOgFKCboBQAmGqCUgAYYPPvureEpC/u2dNc/8BDzaUjP694emMJNl8YWfa1uZd3vu5Ic9Ftd5cANH92re8nwWjea/7Me+914pWLOl831X71jPObp3e8UuTvXdsAMP0EpQAAAINBUArQTVAKAEw1QSkADKhPz76g2bhrV7Np1+7m82dfNGbd7BtuaXa//VZzx2Prxyw/Un2QoHTXWwea3772J53rZ4qgFODIJCgFAAAYDIJSgG6CUgBgqglKAWBA/fqcS5tt+15vHnz+xXHrEps+/tK25uEXNje/8hfPKcuOOfnM5srlK5uX9+8r8Wamxl/y0Lrmo9+fPea131mwqHnmlZ3N3nffKTbu3NWc8qPFo+v7RZw5j5xPzqu93aJVa5rNr702+pp2PJqfszw/V9nHRbffXZZntNX2MTLi6I4332hWPrOx+dBJp08qKD39+qUlrl28+uExy+u+lj3x1Oiy9nvPyK9rN29pvjJn/uj6ery5ty0r67JNtl23eevodrkO7fcTeV3W9d6DjK66etPzY44BwNQRlAIAAAwGQSlAN0EpADDVBKUAMKDqCKU73nijOXnhjzu3qRJfLn3kvenhE2lmOvhl6zeUIPK+Dc+W9dnu1EVLSmj6/KuvNgvuWVXk71mWddnmUILSvT99txwjI3WuePrZ5hsXLxgTlCZm/fL585ur77u/LMufXzrvkuYTp5/XPPny9hKifvGceaPHyPm89u47zblLbx89xsGC0jpa6PptLzcfO/Xs0eXZV67HrBtuLj/X975hZNusSwi7bd++5sU9e8o5ZZscL8fPdglBcx1zPfMe8/NHTpnVfGb2hc3xly4sIW7k73WU0uvuf7AEqGuee6G8NkFvzr99DACmjqAUAABgMAhKAboJSgGAqSYoBYABViPIBI0JOk+7/sZxI45GHQn05rWPjcaj+fPOx58so3RmtM7EkI+8uLnZundv8/WLrhh9bYLPhJ0PPvfChKOC9gtKE7LWY0Y7KJ1oWUYUbQef9fzakelkgtLIdvV95ue6rwS5CXNrdJrRSfNzfd3sG24po5suuv/B0f30vqfs69EtW5utr79erlWWdU15//HTzm02bN9RjvHJM+eUZbFwxQNlnxfcctfoMgCmhqAUAABgMAhKAboJSgGAqSYoBYABd+zcK8romIlKE2XW0TJ7p2pvB5VVRvrMiJsJOb81/+pm+xv7x00zH6ddd2Nz1pJbSzzZL+LsN+V973aTDUp7p6T/6oWXlf3X6e6zLMfI67rkfOq+TrrquhLeZuTR9r7ueGx9+bkGtzUcrRKulpj2Z/vq955uWff4mPfeFZTWiDXbfW/h4jGvn6xv7/iDEkIdzIl7/7A5/uV/2Rz37H/bfOX+d5pPL17ffPSsBZ37BBgmglIAAIDBICgF6CYoBQCmmqAUAI4SGZn0zMU3Nes2by3Tqme69pOuvr6sSxDZFV1WCTlrVJlosnffbdMVlGZ6+kxT/9zu3c1nfzi3xK8ZsXTOzXeObpNjZFn+zBTybYlg2/vKFPoZSTRhZ/ax88CB5vTrl5b19fj9HCwozfKDBaWRSDaBavb58v59zYqnn22+s2DRmBFcJ/Jri9Y2X7hz+0F9eeWbzdfW/fXmm8//d8133/zjn4Wmf9Ic+8TfaX7pghs69w0wDASlAAAAg0FQCtBNUAoATDVBKQAchRKWJnys09QniMwIndeuWjMuvIyM2HmkBaWREUUz5XzCz4ycmhizTisf/Y7RJVPoJ+L85iVXlX3V6e6zrh7/7vUbOq9PjVP7HS/LJxOURsLf066/scSkOZ9Md5+RS3u3O1w+dPLs5mMX3dR8ZdVbzYn7/qj57oE/LmFq17YARztBKQAAwGAQlAJ0E5QCAFNNUAoAA+rK5SvLKKQZubN3XW/QmGnjE5Rm6vfebauvzb28BI51ivm2z8y+sPnCOfNKnDqdQWmdhj/T3Gek0vZ093EoQWnee6bQ/9GKB8q+6nT3MeuGm8tIp4lO26/p9UGC0oSkiWHb4egxJ5/Z3ProEyUqveCWu0aXT5UP/+D85rin/1GJqT538wud2wAczQSlAAAAg0FQCtBNUAoATDVBKQAMqESNiRszCmlixfa64y9d2Gzfv390ivezltza7Hnn7TIyZyLGul1G6Dz/5jvLskwL/9jWl8aNAvr5sy9qNu3aXUbRzL7m33lPCSDn3b58dJs6pfzhDkpzvBw35x7t6e7jUILSOoV+AtVovybv8YU9e8p7/8qcn7/3xKvn3XRH87mzLio/f5CgNNPdJ2jtjWJrzNr73qfMyLGPfexvNSe99SfNr1x6R/c2AEcpQSkAAMBgEJQCdBumoLTfvx0BAFNLUAoAAypR4pKH1jV7332neXHPnub6Bx4q07Pfsu7xEjdmRNJTFy0p2yY4fWDjc6PTq19+94rmmpWry+u2vv568xvzry7bZfu87vlXXy3TzUdi0va+MmV8RjLN6KiLVq0p+8n2r42cx/sNSjOlfaa2TwB78R3LRwPOuGL5inLevaFr5BiJMfNn7zT1Uaeqrxbd/2A5djv0rOaPbJ9oNdck7ynXKNcq+885ZJvJBqU1hM11+/Hqh8p5ZNn9z24q92v1pufL+eX65frXqfjb+5xKH/7BnOY39/z75oRd/0cJTLu2ATgaCUoBAAAGg6AUjhz5fXh+L57fg3et79L+HflEM6d1OXbuFeV36PndfH6fn33cvX5D86lZczq3HzYzHZSeufim5tmdO8u/p0z1/RGUAsDMEJQCwABLVJpY8YmXto3+ciV/5ufvLFg0ZttEpZnSPfFi3S6/lMkvZ9rb5XXPvLKzhI+Rv/fu64c/uaUEnnU/9254prnz8Sffd1Cac1u2fkP5BcQrb77ZnLzwx6Pr6lT8vSN7Ro6RffWTafjb29dRQvv94iu/CNm4c9foe8/fZ4+813rcfu8py9vvPU750eLRa5SQNMt670Heb0ZNPeHya0ZfN11+7bq1Jar65DUPdq4HOBoJSgEAAAaDoBSOHO8nKK2/i8/vwQ/ldXXQi90jx8vv1Rfe90Dz0PMvln+H2LJ3b3PcvCs7XzdMZjIobQ/MUQc5ycx4uT+ZBS7/ntP1uvdLUAoAM0NQCgAc0TJ66c4DB8ZNd88HdNLpzbd3/L+ar6//e93rAY5CglIAAIDBICiFI8f7CUozsMKON95oNux4pdm4a1fz6dkXdG7X9vmzLyozpiUcPf7ShWPW1dD08Ze2NR879ewx64bNTAWluYe5l7lHuVftdbNvuKXMQnfHY+vHLP+gBKUAMDMEpQDAESsjg2bk067p7vngvrzyQHPivj8y7T0wNASlAAAAg0FQCkeOQw1Ka3iYKe/n3rasjF557tLbO7dtm3f78mbvT98tMWrX+sSKiRYzCEV+zoxhmTmsd6ayfufbnp0to2yu3byl+cqcn/+7Q7bP6xatWlP+TaL8/f4Hy58ZhbO9rzoCa9fMalNtpoLSftc7cs8T+z78wuYSl07mmh1z8pnNlctXjptVr31P+gWlvbPNdc20F+3Z9hIkL3loXbP2xS3N0zteaX71jPPHbQ8AvEdQCgAccT5yyqzmrCW3jk6Vkv+T37UdH8yvXftwCav+zJwfd64HONoISgEAAAaDoBSOHIcalCb4zJT1CUQ/+8O5zXO7d08qvMy/B2S2su8tXNy5ftYNN5d/L0jkmZ8PJSitI5xmxNQF96wq0ei2ffvK1O1fOu+Ssk22T9Ca2DTB4Yqnn21OvPLa5smXt5co8YvnzBvdX/bx2rvvTCqUPdxmeoTSjDx78sL+/6aQEWQnc82uG7mPiUHXPPdCmTo//w6U+9a+J11BaZ6r3KOMlJp9XrNydXlN7m/uc92u3vOtr79e7ne2y7T8OaagFAAmJigFAI44Hz/t3Gb9tpfLLwXuXr9h6KewmSq/dMENJaz65I/WdK4HONoISgEAAAaDoBSOHIcalCYMbc86lpFFMwrlNy+5aty2bQlDE4gmFO1a33sekw1KEw4mIMwolu2p9+s07TVQrUHp0pE/2/FrRkxNyJqgNT9nQIyMvtobTE6XmQpKo0aa+bebXPfTrr+x+ej3Z4/b7mDXLP8GtGH7jnJPPnnmnNHXLVzxQLkHF9xyV/m5NyitUWvvvTxu3pXNlr17S8iaf0+qx9u+f39z/KULR7fLVP0JUQWlADAxQSkAwJD6yA+vLGHVpxc/0bke4GgjKAUAABgMglI4NL9w6nnNp6+6tTluxabmxCf3Nads+d3mjL3/sJm1//cndMbr/6j59IJbOvdZHUpQmog00WB7uvO8PiOPXrF8xZhte002KK0B6WSD0vpzDUerhI051/r6bJ/tsn17uzpV+7Innio/f/XCy8pxD2W6+0v+/n8o32uHQ2LSKPdv5B4fTn/q5Fmd59927NwrytT0iUoTe+bP3qnqD3bNavCZZf1GpI3eoLR3lNq2HCvHzLHrs5Ep+HvvUe63oBQAJiYoBQAYUh/+wfklrPrsTZs61wMcbQSlAAAAgyHRlKAUDu5TVyxtfrDjbzZX/JM/GQ0NL/3H/6m54K/9QfPDt//ZuIC01+EOShONZrr7THtfl9Up0DMr2USzkU02KK3nMdmgtEaJ/RwsKM0559wzdX+m8M+U7Yka59x855jtJnLJP/ijcWHo+1Xv8w/f+ufN7Df/yWH14VN+2Hn+XTIy6ZmLb2rWbd5appHftm9fc9LV15d1k7lmCT8T9OYeZATbFU8/23xnwaIxAWhvUNr7c1uW1Sn1J3pmBaUAcHCCUgCAIfWnTjmrhFWfu+WFzvUARxtBKQAAwGBINCUohf4+9FtnNCc+tb+EhZf9/n9uvvfi7zRfufmh5mPnLejc/oOYbFBaR52soWavTJV+0lXXdb42MqppRjLtN2JlHZ1yyUNry8+HGpTevX5Dc+ld945z2nU3lu36BaWx4J5VZXr8hLK9U/pPt5mc8r6fhKW5dg8+98JoEDqZa5YoNdPmJyZNVJrp7vMM1dizNyDt/blt/p33CEoB4DARlAIADClBKTBsBKUAAACDQVAK/SUmPe3V/6ZEhb+5/vUy3X3XdofLZIPSxKKJRhPs9Uab165aU9bd8dj6ztfGvNuXl6Bw8eqHO9fntTVQzM+TDUpriNpvv9VEQem35l/dbH9jf5myPaNuHsp094fbTAWlVy5fWUYhTbDZuy5xZiLNdqg50TVLSJq4tB11HnPymc2tjz5RnoELbrmrLOsNSCc75X32vfX1kWejFbhWglIAODhBKQDAkBKUAsNGUAoAADAYBKXQ33efOVCCwq8te7Jz/eE22aA0wWa/IDOjlz66ZWuzcdeu5tOzLxi3Pj5/9kXNpl27my179zbHX7pwzLpTFy0pQerjL20bnTb/k2fOaZ55Zee4ODDTqmekynq+2e8Le/aUETK/MufnI2QmNDzvpjuaz511Ufl5oqC0jr665523i0OZ7v5wm6mgtD4HiTQThLbX5X5t37+/3ONcqyyb6Jol+kz82Rvm1mC034ikeXbyDOW+t5+j4+ZdWZ6bJ1/eXp6PeuycU/tZqs+YoBQAJiYoBQAYUoJSYNgISgEAAAaDoBS6ff7aZSUmPGXzf925firUkPDhFzaPG3k0MmV8Df0mCvUyBXpCz4lizB+MHCuh4e6R493/7Kbymgc2PlciwwSDCQfb22fU0oxouea5F8q5ZGr1vDbL2gHs/JF1iRpf3LOnuWbl6ubyu1eU4DD7vWL5irLNREFpZLvsdyanu4+ZCkoTfi55aF2zd+Qe5jpe/8BD5Zrfsu7xMlJsgt+Ev+3X9LtmCT5zf7Ov1ZueL/tZtGpNGVU0U99/85Kryna9QWlkJNvcy4SheT5yP3M+vcevEXL2mX1nu4TFOaagFAAmJigFABhSglJg2AhKAQAABoOgFLrN2v/7zSV//z80v3DquZ3rp0INShP2dckU4rNvuKVMRz/RKKbtKdB7pyFvO3buFSUyTOyZ/SceTJT46JaXRkcTrT5x+nnNfRueHd0mYeF7U7O/Pu5czlx8U7Nx564SFEb+Pvsnt4yey8GC0q/NvbzEjjM53X3MVFAaed8JiJ94advo/cmf+fk7CxaN236ia5ZRTjOqbdbX+7x+28vNCZdfM7pNV1AavfcyI5Z2Hf+HI/c3MWv2kbg0QezaF7cISgHgIASlAABDSlAKDBtBKQAAwGAQlMJ4Hz1zbgkJv7V6S+f6o1VCxKvvu7+Ei4kDf+vq6zu3m2qnX7+02XngwIxOdx8zGZQeqiPlmlUfP+3cZsP2HYJSADgIQSkAwJASlALDRlAKAAAwGASlMN6fu21dCQl/6ezLOtcf7TKFeUbCPP7ShZ3rp1Ki1jsff3LGp7uPQQlKZ/qa5Xm57v4Hx4yMetJV15WRSlc8vXHMtgDAWIJSAIAhJSgFho2gFAAAYDAISmG8U7b8bnPx3/v/dK5janzklFnNWUtuLQFiRkjNlOld202nIz0oPVKuWR3V9pEXNzeX372iWbRqTbP19ddLUHrKjxZ3vgYAeI+gFABgSAlKgWEjKAUAABgMglIY7+yf/g/Nmfv+cec6pkamSF+/7eVmzztvN3ev39B87NSzO7ebTkd6UHqkXLOMTDr7J7c0G3fuava++045n5zXCZdf07k9APBzglIAgCElKAWGjaAUAABgMAhKYbwL/tofNN9/6a90rmN4DMqU9wDA4BKUAgAMKUEpMGwEpQAAAINBUArjXfJ7f9ic+NT+znUMD0EpADDVBKUAAENKUAoMG0EpAADAYBCUwniX/f5/bk547NXOdQwPQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADCkBKXAsBGUAgAADAZBKYwnKCUEpQDAVBOUAgAMKUEpMGwEpQAAAINBUArjCUoJQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADCkBKXAsBGUAgAADAZBKYwnKCUEpQDAVBOUAgAMKUEpMGwEpQAAAINBUArjCUoJQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADCkBKXAsBGUAgAADAZBKYwnKCUEpQDAVBOUAgAMKUEpMGwEpQAAAINBUArjCUoJQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADCkBKXAsBGUAgAADAZBKYwnKCUEpQDAVBOUAgAMKUEpMGwEpQAAAINBUArjCUoJQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADCkBKXAsBGUAgAADAZBKYwnKCUEpQDAVBOUAgAMKUEpMGwEpQAAAINBUArjCUoJQSkAMNUEpQAAQ0pQCgwbQSkAAMBgEJTCeIJSQlAKAEw1QSkAwJASlALDRlAKAAAwGASlMJ6glBCUAgBTTVAKADBJv3rG+c3TO14p8veubQaJoBQYNoJSAACAwSAohfEEpYSgFACYaoJSAGCgfeL085pl6zc029/Y37z+03ebve++02zcuauZ/ZNbmg+ddHrna94vQenh89vX/qTZ9daB5qZHHu1c3+Wbl1zVvLx/X/Pky9ubj516duc2/Rw794pm9abnm1fffqs8JzsPvNncPfLcfGrWnM7tgaOToBQAAGAwCEphPEEpISgFAKaaoBQAGFifnn1Bs2H7jhIJPrDxuebyu1c016xc3Tz/6qslLP3Jg2s7X/d+CUoPn/cTlC66/8ESg+Z1eX3XNl1OXbSkBKS7R153/7ObmoX3PdA89PyL5bnZsndvc9y8KztfBxx9BKUAAACDQVAK4wlKCUEpADDVBKUAwMC6YvmKEo5ede+qMcs/+v3ZzbrNW8tolhnVsr3ugxCUHj6HGpRmRNKMTLpp1+5m6+uvN3c8tr5zu16fP/ui8pqEo8dfunDMuhqaPv7StkMe8RQYTIJSAACAwSAohfEEpYSgFACYaoJSAGBgJUbsN1rl/Dvvabbte705+8bbmgefe6HZ8eYbzYlXLhqzzYqnN5ag8KSrris/Z1r0tZu3NHveebvZ+9N3S4Q4/657R6fO7xeUnnD5Nc36bS+Pvu7FPXvKcXun3P/KnPkldE0Em21zrIy6eagjbh4ugxSUnn790jLC6JX3rGxWPrOx2fzaa80Xz5nXuW3bvNuXl3uyePXDnesTpu5++62y//z863MuLc/Ng8+/OGa7rvM95uQzmyuXryzhcp1Gf8lD60rQnPX1eXnmlZ3ldTn//LzgnlXlnHLv677i3KW3N6+NPBv9zhX44ASlAAAAg0FQCuMJSglBKQAw1QSlAMDAKiOU/vTd5t6nnhmN+LrMufnOEuq1A77P/nBu89zu3SUEzeiUXzrvkhKCbtu3r1m0ak2ZPj/rsv/rfva6rqD0lB8tLiFhRs3M6xILbhhZn2A0MWo9Xnt6/mVPPFX2/8iLm8t2gtLubdoSfib0/OqFlzWzbri5vDb3tWvbtvei4QPN9xYu7lyffeWe1GdjskFpYuGlI3/Pa3OMS0fu9bL1G8r9vG/Ds2V9fV4SEOe1CYiXPvxo842LF5Qgtj579Rh5LrrCZ+DwEZQCAAAMBkEpjCcoJQSlAMBUE5QCAAMrEekDG58ro0OWoPP+B5svnz9/3HZZloDv0S1bm4+cMqssKyNetkLCC265q8SjGdm0vu4L58xrnn/11RKCfvy0c8cFpdlXotAcO5FgfV2NR3PMej5d0/MnOrzz8ScnHZR++PvndC5/v6YiKM0+u5b3OpSgNCOR5lpmZNJcs1zfjbt2lWtf72c/CUMTiCYU7Vrfex6TDUrrzzevfWx0JNp6P2sUWp+XBMeZXr/uK9vlvWx/Y3/zrflXl2X1PfVGpsDhJSgFAAAYDIJSGE9QSghKAYCpJigFAAZaph2/bNl9JepMWFrj0vZU9ZFRJNsBX8LA9nT3NTC964mn+gZ9vUFp9pV9Zt+922ak0oyKmmnM83MCxUyN/rW5l4/Z7qLb7p5UUHrMX5zbfPfAHx92397x/ypx1eH0p773w8730HYoQWlGIs1IoPVaRl6X6/nNS64as22vyQalNSCdbFCaP7tGE63T1ue+9j4vXdvlOcnPdf+Tme7+1xatHbnOfzLuuvf67pt/3Jyw6/9ovrX5XzTHPv63my/cub35+JX3NB/6rYkjXDiaCUoBAAAGw/x/8EfNb67f27kOhpWglPj6Pc+UoPQXT5vTuR4A4IMSlAIAR42MBrrkoXUlCMxoo3ev3zA6gmU74EswmpEgn3x5+2g8mtFOb330iTKKaKYtz7T1GU30E6efN7r/3kBwoiiyrssIqBOFhZMNSiNB4BeX7Txs/uw9r70XH771X5oTdv7rw+Ibm/5J57n3mujatdVRYDN6Z0bxrMsTAicIriPM9jPZoLSex2SD0qyvAXOXgwWlddTVOmpu3kc7cJ7IL866rPnCHdvK8zCRP3v37ubXH/xLzZ9/6veav7Dr346Gpn9h979rPnfz85MKf+FoIygFAAAYDBf+jX/TnPz8/6NzHQwrQSnx67c8UoLSj513Zed6AIAPSlAKABx1Eokm+GuHmp/94dzmud27S5z4W1dfX0aX7BoNMlHqNStXl6A0YWkiv/NuuqOs6w0EJ4oiM8X57pF1hzMoPdymYsr7yZro2rVlBNKMRNoVbUY7Cu6S0WN3HjjQfG/h4s71s264uYx+uuShteXnQwlK82xcu2pNc+ld947z1Qsvm/C+x7InnirvLaOc9gbOU+EXz5jffOrHjzfffO6fl/v+rRf+h+YXTruoc1s4WglKAQAABsO5f+l/bH6w4292roNhJSglvnD9vSUo/dVLun/nDQDwQQlKAYCB9LmzLioB3toXtzQfP+3ccesTaiY4zJ91WQ34bn9s/bjRID8z+8LmS+ddUqbQr8u+fdnCMn3+hu07yjF6A8FDmfJ+9abnS6iY0LC9naB04qA00e/ut99qbnx43bhoMyPQZt3p1y/tfG3Mu315Ga2231Tyd4w8C+19TDYozbN0sBFFDxaUJmbNsfM85jmazHT3h8uvXr2q+c4b/7E54ZX/vTnmtLmd28DRSFAKAAAwGH6w42805//u/9q5DoaVoJT45fMXlKD01299b5AEAIDDTVAKAAykTBOeSDOR32nX3Thm3YdOOr258/Eny8iTifbq8kSDGTU0I4/2jgb53kiWYwPBHCNTktcgsDcQrNOxJzr9xsULRl+XqdkToWZK84x4mmVXLF9RptPPNPp1u3qegtLubXJ/cp/6BZk16E0U2ruu+vzZFzWbdu1utuzd2xx/6cIx6zKKbO754y9tG30WPnnmnOaZV3aOO+acm+8sgXA937OW3Fqeozw37Qg59/78kW2zrPd5qdu0t81U/tlPRsydzHT3h9MvXfiT5rsH/u/mm8/9s5Fn8YzObeBoIygFAAAYDMet2FSCqY/Omte5fpDkP1zOf8Cc/5C5a/106vqP8BkcglKqeX/v/2y+9+LvdK47WuR38e1/Oznh8mvK79MXr5m+gRkAYFgJSgGAgXXcvCubF/fsKeHoAxufay6/e0UZGXTd5q0l3syyj35/9uj2NeDLL017R4M85UeLS1yYOHTRz6YxT7Ca/dRgsSsQ7H1djl+ny58/so+6/xqZ5lwzumXONTFqthvmoPThFzaPG3k0EgmXAHjkevWLTmvw2w53u/xg5FgJNhMT3//spnKP8mzkXiQ0zXPU3j73O6OarnnuhXIuiUbz2iyr55LnKvvIstzH3M9rVq4uz2Oehd+Yf/VBg9LIsSYzdf9U+fxtW8oz8OnF/aNcOJoISgEAAAbDr122pASl31qztXP9VKm/s8rva9ryHzVntpxPzZrT+bqJDHJQ+s1LriozPs3U764YS1BKdeorf6uZ/w/+qDnmB+Nnb5su9ff3K5/ZWAbP6Nrmg+gNSr+zYFH5Lr710SfGbdvrSPreBYBBJCgFAAZafom79JFHyy8HEvdF/p7/SrXrl5y3rHu871Tl+S9c1297uUSe+cVqflma8LRGqf0CwfbrcvxEhWffeNu4X6J8Zc780dg1267dvKVZdP+DQx2Utn8x35Zf+Exm9NbEoRk5NCOIdq2vjp17RQmEE5Fm//VePbrlpeZzZ100ZttPnH5ec9+GZ8fczyuXryzPVTtuzXOR5yPPSfaZfecYOVbWTyYozSipiVWnc7r7MUae0eNf/lfNt3f8wcjPh/+XfnCkEZQCAAAMjvP+8v/cXPx7/9e0BlP1d1bt/wg6/yHxQ8+/WH6nl/9gPP/heNdr+3m/YVM9l37/sfX7cahBaX53me1n6veX0+VQr8tMEZRSfXHxihLd/8ZDL3eunw4ZiCGfm3y/ffXCyzq3+SB6g9Iu/b4nBaUA8MEISgGAoVGnqD+S/ov6JQ+tnbFfyM5kUDrTEvtefd/9JQLNCKe/dfX1ndtNtSuWr5iR6e7bPnXduvIc/NLcJZ3r4WgiKAUAABgcX1pyfwmmTn7hL3WunwoTRZz5D4LzHx/Pu335uHUTGdSgNL8/ze9RN+3aXWbkqbM4HY0EpQyck05vzvmv/sXIM/HHza9csLB7mymUGcPye/V8R+T32/k9d9d2H4SgFABmjqAUABgax1+6sNm+f/+MjAaZkQsy4mZ7BIP6S9n84uWL58wbs/10GOagtMoIoU+8tK08G13rp1JGOM2ItTMdOH/4t8977zm4eXifA4aHoBQAAGCwfH/r/7NEpV+947HO9YfbRBFn17r8Tmfpw4+WGZESJGYmm4xo2p65qCts6p1Np/26bJfts7zqjary+sx+VGbYefed5plXdpZZlOr6GDMLz8+2yblnf5MJJ8t01iPHvfKelWVK636/w8w5Z7amzPKT4DbHe/ylbc03Ll4wZrvMNJXzab/na1etaY45+cwx2525+KZm485d5ZzreWeq67q+36xA9brlerd/zuiyubZ1lqEdb7wxeq3rPa3XOdr3qvf+5s9l6zeUa1uPO50EpbT9yoU/aub/w/9vc+7v/I/NR86Y27nNVElAmu+Hc5beXn6/nRncen/H3S/U7gpFu2Z3ywio7e3a38EH+56cbFDaNQtZvqfyfdXeLt8X+d7I90e2q99f+X6q3zkAcDQRlAIAR71Mt3LNytXll5r5xefX5l7eud1Uyi90X3r99XIOOZf88iG/WM0vSJY8tK7zNVNNUDoz8ou1/BIto+XmF1SHOqrFVPj29n/VfH393+1cB0cTQSkAAMDgOX3vPyhR6YlP7pvy6e/bwVLvujk339m89u47ZRr4/JwQ6YGNz5Xtb39sfZkef81zL5Tf9133s22iN2w6bt6VzZa9e8uon4tWrSm/J0yMlYgqvydKYPml8y5pzh85XoKt/EfqGQ0wx2u/vv6eccE9q5rnX321jBJYw8t6bjmXhKs5t8RZOUZiqMkEpRmRNOed363OuuHm8j5zDXq3mz+y7+w37yHvJe9p27595Rxzrtkm/5H9hu07yj7yu9D2tVo6cq1rgJv3n31lVNS8r/o73YSc+Q/Ds82hBqU5Zn4nnPOq+8uy3Otcp1zbzGSU65I/c+1zD3JONchNRJpzzp/5Ob/XnYn/QFxQSq/PXXNXc+k/+o/NRX/73zWfufq2zm0OtzoTXP0M5rOaz2jvLFyTDUrr90N+V77siafK90j2n0C9vV37+/lg35OTCUrzPu5/dtOY78l8l+cYOZ/24CD1e65+N92w5pESlea1glIAjkaCUgDgqJdfdOb/2OcXh/UXjzOhPfJAfhmSX6rmFxH1F6bTTVA6M/KLrtz7/KIr//Vz7ygMM+HrT/zd5viX/1XnOjiaCEoBAAAG00kb3ylR6aX/+D8139v8l5uv3PJw87Hzxo6AeTh0BaX53c0PRpYnAI068ub8O+8pv+dL/FS3TcyUUfbao3n2hk2JGrP+ewsXj76uTh/96JatJXLKsq5zybr8frF9HlFnZko0mp/PWnJriZ8SWbV/97hwxQOTCkpz7jmfjEya1yes2rhrV4m86vlFXd4bdyYsS2BWzz2jGeZ8cs3qNtlv9p9rk2i17isj/rVDrhrQ1ll+DjUobYetkTg2923JQ2tHl3WFb7knuc4Pv7B59Brmz7vXbxh5bwfG3L/pIiilyycuWtTM/Vv/tnxHnv+7/2vzrdVbms9cdWvz4e+f1bn9B3XilYtKwF5ngvvmJVeVuDIRenu7yQal+U7Mv99cde+q0W3qZ629Xdd3YteymExQWkZhHvkuuHntY6Of8ajxaH1/9bspMennz75odLv6vSsoBeBoJCgFABhSglKqP/fQX27+wu5/17kOjiaCUgAAgMH1qStuan6w42+UaKpKYHrBX/uD5odv/bNm1v7fn9CZ+/5x86krb+rcd1XjpERQvRJYnvKj9yLCxEcPPvfCmHC0ygim7QhqMmFTVyTZFUolvMy+ajhaJbTMCKH19bese3zMOVT9Aq9e+Q/0E12eu/T20WU5j0Rjicfqshpn1lFbq0SniVpPu+7G8veEsl3XKu/n4juWN58766K++4qMWpiALSHboQalvbFXZq/K+2hf167rUiOyyEABdfmh+PVb1jYX/zf/V3PJ7/3hYZFn/rQ9v9d5LIZbRm8+bsWm0bC0+Cd/0sz7e/9nmRK/6zux18kv/KXOffdKaFk/j/m5fh/ms9KOwft93+SzV7+f8tpE2/ms5vug33b5ues7sWtZTOZ7t/290l7+2R/ObZ7bvbt8p+a7tR4j36vt7ep3kaAUgKORoBQAYEgJSqm+dN/+5jv7/3+d6+BoIigFAAAYfL9w6rllaufEUyc+tb85ZevvlmnxuwKptkMJShM4ZfrjWPnMpjLVfXt0zRoS1di0VzuC6gqbMjX9Ey9tKwFl+3XtSHKieKr9mrb6+n4xVb/Aqy0BaEYi7Y3D6qij7eBzMvur16r93rpMtK8syz1I4Npvf5MNSuvy9nXtd+yzb7ytBGd1tqmMwPj1iyYfl05FUHq6oJQJJND85fOvar68dE1z/CM7yujOP9jxN5szO74Te00mKK2hde9oxV0Rer/PVTsUnej7ob1dfp7oO7G9LPp9B7ZNtE3W1Qi+3/uo5977HQMARwNBKQDAkBKUUv3Z5Xua7775nzvXwdFEUAoAAMBEuuKkOh19O3iqIVEiw0zTXOPTqo66mW17o6WMxJljZPS7M368tCzP1Mkbd46dOn6ieKodvLZlVNBEXv1CqX5hVFudvjrbdalTz2fbyeyvXqv2e+sy0b4S885EUBp5r4nlsp8EtZmaOyMbtmO66WLKe2ZanSa+fh/0WvnMxtHp4/t9rvLZy/dYvs8m+n5ob5efJ/pObC+Lft+Bbf22qaOmCkoBGGaCUgCAISUopRKUMiwEpQAAAEykX5yUKZ7bo5S2p3FPcNretldvtLR60/Pl5/b0zl1RVde5fGv+1c32N/aPiba6ZGrmnQcONN9b+N4U/VW/MKot7zXB2I0PrxsXrN69fkNZl6gs2566aEmze+QcF9yzasw+cm5fOGde85nZF47GWZlCOlNJt7dLrJnr99Hvz562Ke/r8vZ17bouObdsW+PZuiz767q200FQykzKZznfPTveeKO5ZuXqcd8P+ZwnRk+Unu37fd/ks1dD0fr9sPX118d9l7a3y89d34n9vrN7v3e7THbK+3zW85nvPYagFICjmaAUAGBICUqpBKUMC0EpAAAAE+kXJ9VRStvTwCeoymiVCTDbcefX5l7enLn4ptFlvWFTfk50le3qa75x8YISVB0sKE3c9NjWl0oE1Rs0nnb9jWU/+XsdRfDmtY+NObec60RBafafEUh7Y82qBq2Z+j0/f/7si5oX9uwZM2ppZMTV7ft/vl2u1Z533i4jfdZtcl53Pv7kaIBWp9J+5pWdY6baP27elWUk2HqMGqDlmraj3Dolf427PmhQmtFQExHnmtVlkXi2HblNJ0EpM6l+Dz743AtjvleqROH53rli+Yrycw3O25+1vC5RavszlO3z/XD53e+9rt92Xd+JXcui93u3S7/vyfl33VvOp37263fTpl27y3de3a5+z/V+xwDA0UBQCgAwpASlVIJShoWgFAAAgIn0i5MicdHen747GkslMtqwfUcJj1Y8vbGM0HfDmkdKcLlhxyuj4VFv2JTXZz/Pv/pqCS0z6mdCyASN7ZCzTj2fqfATWtV4MpFWts9xcrwcN8fP6J4ZmfSYk88sI34+sPG5ErxmRNT2Njl2v6C0BlZd7z+6Rmat8VVG88t5Llq1psSxiUATg2abeq1ybZc89N7IpzmvBJtLR45VY655ty8v+0q4lXAz1+fFPXvK+837rudRr2E95q2PPlG2yft9P0Fpfd95bxffsbz53FkXlfvw1Mg555plJMOc85KH1pbjtMPi6SQoZSaVz93IZyyf0671dWTPGn/XELN+7vOZzmcqn/Esq6Fo/X6on7V8ph95cfO47bq+n/t9T+Zzn8/qtSPfR/nsttXPeL7P7n9205jvydsfW1+OkfNpf8br91z9bsp3b47b/s4BgKOJoBQAYEgJSqkEpQwLQSkAAAATmSgoraPztUep+8Tp5zXLWkFo/kwg+qlZc0Zf1xuUJp5M+JRpo/OaREkJtRJQ5e915NJsd/0DD5UR/hIttUf3POHya0pMmcApYWXizcuW3Vdi0rpNzu2+Dc++t83I6zPyZx1dsysozfEyYmg74OqSfSQEreeT1519420l/My55HhrN29pjp17xZjX5ZrkfBKN1fd95fKVY845Mrpr4rCccz3v7ywYOyV1gtkEark2dV8Jx7Lt+wlKs7/cx5z7K2++2Zy88Mdlee/9zbknPPvKnLFTc08XQSkzJZ+RdZu3ls9Oe2TgXglC83nJiMH5Od9V5fPc+m5YdP+D475n8pnK/vOZ77dd1/dzv+/JfO7zme3S3mfeV/5jgXyHZF0+4/mean+H1+P0fm/3fucAwNFEUAoAMKQEpVSCUoaFoBQAAAAYVIJSOHLkPzLIaMyCUgCORoJSAIAhJSilEpQyLASlAAAAwKASlMLMyDT5vVP9ZzTUjNackVTbywHgaCAoBQAYUoJSKkEpw0JQCgAAAAwqQSlMv0x3f8dj68t0+Cue3thcete9zZKH1pap/Te/9lrztbmXd74OAAaZoBQAYEgJSqkEpQwLQSkAAAAwqASlMDOOOfnM5rJl9zVb9u5t9v703RKXrt70fPOVOfM7tweAQScoBQAYUoJSKkEpw0JQCgAAAAwqQSkAANNBUAoAMKQEpVSCUoaFoBQAAAAYVIJSAACmg6AUAGBICUqpBKUMC0EpAAAAMKgEpQAATAdBKQDAkBKUUglKGRaCUgAAAGBQCUoBAJgOglIAgCElKKUSlDIsBKUAAADAoBKUAgAwHQSlAABDSlBKJShlWAhKAQAAgEElKAUAYDoISgEAhpSglEpQyrAQlAIAAACDSlAKAMB0EJQCAAwpQSmVoJRhISgFAAAABpWgFACA6SAoBQAYUoJSKkEpw0JQCgAAAAwqQSkAANNBUAoAMKQEpVSCUoaFoBQAAAAYVIJSAACmg6AUAGBICUqpBKUMC0EpAAAAMKgEpQAATAdBKQDAkBKUUglKGRaCUgAAAGBQCUoBAJgOglIAgCElKKUSlDIsBKUAAADAoBKUAgAwHQSlAABDSlBKJShlWAhKAQAAgEElKAUAYDoISgEAhpSglEpQyrAQlAIAAACDSlAKAMB0EJQCAAwpQSmVoJRhISgFAAAABpWgFACA6SAoBQAYUoJSKkEpw0JQCgAAAAwqQSkAANNBUAoAMKQEpVSCUoaFoBQAAAAYVIJSAACmg6AUAGBICUqpBKUMC0EpAAAAMKgEpQAATAdBKQDAkBKUUglKGRaCUgAAAGBQCUoBAJgOglIAgCE1GpTeLCgddoJShoWgFAAAABhUglIAAKaDoBQAYEj9wmkXlbDq0zc82bme4SEoZVgISgEAAIBBJSgFAGA6CEoBAIbUnz7nRyWs+uSPVneuZ3gIShkWglIAAABgUAlKAQCYDoJSAIAh9fErlpew6lcuu6tzPcNDUMqwEJQCAAAAg0pQCgDAdBCUAgAMqS/cub2EVcecNrdzPcNDUMqwEJQCAAAAg0pQCgDAdBCUAgBHrZseebTZ9daB5rev/Un5+YTLr2k27trVLF7z8Lhth9HXn/xvmm9v/9861zFcBKUMC0EpAAAAMKgEpQAATAdBKQAwY06/fmmz++23mpXPbGw+dNLpndt8EL1B6XcWLGq2v7G/ufXRJ8Zt2+vB519stu17vfn1OZd2rh90v3jGJc133vhPzZ+9e3fneoaLoJRhISgFAAAABpWgFACA6SAoBQBmzIqnNzav//TdEm5+9cLLOrf5IHqD0i4X3XZ3OYf82V5+tAelf+7h321OeutPml88Y37neoaLoJRhISgFAAAABpWgFACA6SAoBQBmxJfPn99sfu215smXtzc73nyjuWL5is7tPghBabePX3FPCaq+uu6vdq5n+AhKGRaCUgAAAGBQCUoBAJgOglIAYEYkIN391oHmnKW3l6h0/baXm4+devaYbfoFoV2x51fmzG/Wbd7a7H33nWbPO283azdvKSOgtl+fP/Nz9lv/npi0au9zskHpJ04/r1m2fkOz88CbZR/5c+nDj457L5nS//K7VzQ73nijbPfy/n3NtavWNM+8srMcq73tVPrYRUubE/f+YXPCzn/T/MLpF3duw/ARlDIsBKUAAADAoBKUAgAwHQSlAMC0+8gps5pHXtzcPL3jleZXzzi/WXDPqhJinnTVdWO2m2xQ+unZFzQbtu9oXn37rWbZE0+VcDP73/vTd/sGpR/9/uwySurV991fAs/8+aXzLmmOOfnMzmN0ybk/1TrupXfdWyLWBK0PbHyuHKNuO39kXZZv2rW7vN8b1jxSotIEsNMRlP7C6fOaL971SvPdt/5Lc/y2/6X5xVmXdW7HcBKUMiwEpQAAAMCgEpQCADAdBKUAwLQ78cpFZZr7xasfLj9/85KrSlx5x2Prx2w32aD0mpWrS5h51b2rRrfJiKB3r9/QNyit232QKe8zymoi0QSsdVmOu3Rk/1k+5+Y7y7IErxt37Sox6efPvmh02+MvXdhs37//MAelpzfHnHpB86fPvab5+BXLmy/cub3580//w+akt/6kRFTHPvF3mg//9rkdr2OYCUoZFoJSAAAAYFAJSgEAmA6CUgBg2iUkTVCasDQ/J8J88LkXSnSZ+LJuN5mgNK99+IXN5eevXjh21M3e1x/OoDSjrD66ZWuz+bXXmi+eM2/Mum/Nv7rZ/sb+Mlppfq7HvWXd42O2ywinGaV1skHpt3f8QQmhDtVf2P1vmz/38O82f/q8azv3C4JShoWgFAAAABhUl/zeHzYnPrW/cx0AABwuglIAYFrV0TozJX2izLo8o3lm6vhzl94+umwyQWmNMuv0+e3tpjIonei4dd36bS83Hzv17L7HqNtNNij9tUVry4ijE7rj5eazS59tPnnNg80vX3Jbc8xpczv3BW2CUoaFoBQAAAAYVBf8tT9ovvfi73SuAwCAw0VQCgBMq9OvX9rsfvutElh2WfnMxjLqaLYd1KD0k2fOaZ55ZedhD0phqghKGRaCUgAAAGBQnfNf/Yvm1J1/u3MdAAAcLoJSAGDaJBRNMLrjjTeaa1aubi69694xMnX9y/v3Nd+85Kqy/WSC0jrl/dbXX2++fP78MdtNZVB6KFPef2/h4mbngbHHDUEpRwpBKcNCUAoAAAAMqu+/9FeaOX/lf+tcBwAAh4ugFACYNgk+E2A++NwLo6OQts264eYyeukVy1eUn+ffeU+z96fvNvNuXz66TUb8fPLl7WNiz2y/5523m8vvfu91UePVqQpKo99xl47sP8szjX+W1Wn+N+3a3Xz+7ItGtz3+0oXN9v37BaXMOEEpw0JQCgAAAAyqry9/urninzbNL51zeef6I0F+f3//s5uadZu3jpvZazr1/n7/hMuvKb+jX7zm4XHbTpWZOCbvX56VPDP+vWZq9fs3OQCOLIJSAGDaJMDc++47YwLRts/+cG7z3O7dJRjNL54yUmlGLN22b1+zaNWaMqrp86++2rw2so/2L4MSbG7YvqN59e23mmVPPFUCz0de3FyizoMFpXUK/ow2evEdy5vPnfVe8JlfGuw88GZz7chxe0dSrdvlF2JPtY6bdRmVNMd9YONzzUe/P3v0OPNH1mV5otIF96xqbljzSHlvuR5+QcFME5QyLASlAAAAwKD61YuvL0Hpbzy0rXP9VDhrya2jv//uWh+LVz9cBobI7//ze/3HX9rWPPPKzvJ7+67tp0NvUPqdBYvKrGK3PvrEuG2nykwc82iV+5kIsS3PXO5xnr/2v8W8X4LS6SEoBRgMglIAYFrk/9Dnv0rO/yH/6oWXdW4T+cVUQs6Trrqu/PzDn9xSRjXN/8HML67u3fBMc+fjT475ZVB8Zc78sv8Emgk3127e0iy6/8GDBqU5r2XrN5TXvPLmm83JC39clnf9gqJq7/MTp59XXp9zzrr8ufThR8svzuoxIiOXJnTNdP/ZLjFpYtX8Ys0vKJhpglKGhaAUAAAAGFgnnd6c95f/5+aS3/vD5hdOPa97m8Oszr6V39F/8Zx549a3ZxSb6Pf+0603KJ1q0328YZPr2zsASAbuyIAfCUt7B/g4mK6o8WgOSo+k51NQCjAYBKUAADPky+fPb7a+7r94ZeYJShkWglIAAABgkP3ZG1aWUUq/t/kvd66fChmgIbOGnbv09nHrMjBEQr+Vz2wsgyr0rp8p0x3QHUnB3tGo3/VNRJqYNLPQZTa69rqJCEpnjqAUYDAISgEApkGmye+d6n/OzXeWX8RlJNX2cphuglKGhaAUAAAAGHSnbP6vS1T6tbvWd64/3Go0uuLpjePW1dg0v+vOz796xvnN0zteKfL3LDvm5DObK5evLLN2JSTLTGSrNz1fZh2r++kXmWX/7RnD4ti5V5TXZz95Tfab2cHaQWtvQNc7e1k9Xpfec79s2X3Nlr17y0iYdXa0eu7ZX+/ra5DYNWNanLn4pmbjzl1ltrXILGaZHr+ub1/Dubcta17cs6cce/fIvpY8tO6wTO8+aCYKImfdcHN5FnKd828wuVa9/+aSGDrP6apnN5V70r5fdb81KH1o5Fh5nurzmlnnep+v3INMtd9+pu/b8GzzqVlzRreZ7P5yvzPS6nO7dzefP/uisqw+97n/ed7y3OVZySx97c9N5PPQ3ibP0wmXXzO6n/o+q1zLDHaSUYfzOWrvKzMIbn9jf/Ot+VePLjvxykXNjjffGPMcZ//rt7383jFH9pln9Owbbxtzjep7WLRqTTlW/p7PRNdn/dRFS8p3TM7n10auYd5njtn+XJx30x3lePkeyueyLgdgaghKAQCmWP5P9B2PrS+/VMj/2c10LEseWlv+D3L+j/TX5l7e+TqYLoJShoWgFAAAABh0f+rkWc3pe36vRKUnPrV/yqe/r9Pa9057X8PHhHCf/eHcMcvaUeZ19z9YQrc1z73ws9+NrytxWSK0L513SdlmskHpcfOuLHFnZv5KqJYwr4Zt7QEdDhaUZnr+OnV6de9Tz5RzuP/ZTc1HTplVfq+/dGT7hIgJ3drnngAw7y9yjARwCQa/t3Bx85nZF3YeM3KOOddNu3aXKduvWbm6XIf8W0GiumxTr2EC0u3795d/S8i2G0aW5Vzm33nP6P6GxURBafs611Ayz0Se27pNQskEirk/2ebq++4v9zp/5hlMoFgD0Owr+8jzVe9PltVnMM9GnpE80/W5uP2x9WWbDSPPxadnX1C2m+z+cr9zvomM20Fp7nWNLHOM/NtSnp38nHPIdvXzkH1m33lOnn/11dEYs9/zmWf74Rc2d352E4i2P0t53vJvWwl38/MpP1pczqt+BuuzmXObP3Ke9XV5DzXCzn5XPP1s842LF4z7rNf30L52Ofe8hwdHvjMS79Zz8+9pANNHUAoAMA16/0vm/B/w/B//3v+aFGaCoJRhISgFAAAAjgYf+q0zmt/a9G6JSi/7/f/cfO/F32m+csvDzcfOu7Jz+w8q0VgCt/a09zXky2AKdVkNvyJ///hp55ZQLKMmfvLMn4/euHDFA+X35Bfcclf5ebJBaaK5RGUJ4+o2NSJ8dMvW0dDuYEFpr0Rq2Uc7WEs8W0O49siLGZmyHdhFV/DYe8zEcht37SrXooZzUYO6RLuJIOs1bEemkVEjM3pk/l2hLhsWEwWlVyxfUZ6lPKO5Tyuf2ThmlM163duRadfzVgPQ3Ivck7q8joCasDc/Z2r9TLF/89rHxjwXiSkTT+b5yM+T3V+XGmMmaK7HyLOdZzwhZ575/JxnIT8n1KyvPf7ShSVEbo8o3HX9MorrzgMHRj9LGYk4r8szm5CzHjf7yecin4cc85EXN487Zq5xPufZLueWZV3vIdrXvj77vdcoEm/nOmVk0qvuXVWubQLy9jYATB1BKQAADDlBKcNCUAoAAAAcTX7tshubH+z4m80V/+RPSlwal/7j/9Rc8Nf+oPnhW/+smbX/9yd0xuv/qPn0gls69932zUuuKiMcJtarcViCsYR1Cezqdr1BaQ3QErO1I9BeXYFf5BjtoLRL7zGz7FCC0oyA+MDG58p7mX3Dwa9F17lOJiitIWHvdOxRR9DM9OJd7ycS5CZGzbHar53Iqa/8reb01/7+ES+j7nadf9V1fTOIR2LDbfv2jQmB6/T2CUzzc70PNfSMrntYA9De65v95tmv97F9r9rbZaTPjPhZw9XJ7q9Lv+f+lnWPj16HjLKbv7fD0cixcw4TfR6iXpd6nfJc5vxzjBqQ1mexBqY1au49ZvRG5/3eQ732V96zsoz0m5g0EWx7m0ikmud968j6hK4ZZTWf1d7tAJgaglIAABhyglKGhaAUAAAAOBpl2vtPX3Vrc9yKTc2JT+5rTtnyu80Ze/9hZ0TaNtmgtIahdQTCOupjHVWzbtcVQya8y+sSkSWky4ifmdK636iFdVl0RWl57RMvbStxZl5TTRTQ1XiuK+LLqIcJ4XpHnIy8t8VrHi77ymiL7eO1z3WiYK8es997rOtqjNd1DaMu7w0UJ3LG3n/QzD7wT494H/7+WZ3nX+U9t699W0bLzDTsdduEkHne6oi1743E+WYZgbNu03Uv+gWgdXm9j133usq6GmNOdn9dup77urweuz5fvdejmujzEDWArVPo53olFM1+E40mgK4BaW+c23XudV0Npvu9h3rtn9v9avlM1Wnt29tUCbwTetcp/Lu2AWBqCEoBAGDICUoZFoJSAAAAgPdnzs13logzf9Zpv9ujPka/GDLB2GnX31hi0kSlCckSqNZtugK/6I3SErnl54zAeMaPl5ZALqMbbty5a8KArl8IV6cHz3Td7WnoI+ecc8zr8j4zJXf2d/V99487165gr/eY/d5jzL/znikJSo8Wec+JQq9dtaa59K57i4vvWN78xvyry0ilvdtnFNE8Z4mZ86z0hs9d9yL3Lvew9/rW5fU+dt3rSIz88AvvRdfTGZTmmPWatJ215NYSiuZ1/c45AWmi0u9fc0MJc/PZrs9ZruEFt9w1ZjTWfp+jOHXRkmb3yLrJBqUZmXTRyP3Md0qmtG9vU827fXmZ6j73Pvvv2gaAqSEoBQCAIScoZVgISgEAAADen4xMmlgu097f8dj6zmm/e2PIRJl5XTuMTAB466NPlKg0wVqW9Yste6O0jKaYMC7Tfddteo+ZZb0BXVcIl8Dw8Ze2lVitPcJllSn6dx4YH891nWtXsNd7zA865X1d3hsoDoN+QWQ/udYJnm8feU4zwmZv+Nx1D7PvHKP3+tbl9T5+0Cnve/fXJeu6Yswsz2uzjzp6aD6PvSPr9up3/RJs5vm/8/EnS1BaP1f5fOdZy77bz+HhnPI+EXXO+74Nz3Z+Br903iXNi3v2NM+/+mqJTzM9fvvzAMDUEpQCAMCQE5QyLD508qwSlH79KUEpAAAAwKFKSJaYLqM/ZvTOOgJi1RtDJrrL9r3RW40ra9BXRzdsR3bZPq9rR2kJ43Lsr829fHS7b1y8oMRw7fCtN6DrjTuz76Ujf9/77jvNdR2BZ9TX3LLu8dFleV2mxn8/QWlGQN24a1fzzCs7x4yGmpFPE8zVUTR7r2Hdri7vDRSHQb8gsp96rTO6ZZ6/9nT38UGC0jo6b56D9jM9/657y/FqvDrZ/XXJuq4YM8vrdciz8tjWl8r7S/zc3i6jAedzUX/ud/1qIJpjZcr7+nnO5zP7jQS0dfusz+c+n7f2/nO9M8pvgvME5FnW7z30Xvt8lvO69ijBua6JXHOdM+194tNc236fVQAOP0EpAAAMOUEpw+IXTp9XgtJjn/g7nesBAAAA6K/GdBld9IrlK8at740hE6Dd/+ymEm5mdNFMxZ1prhOkJQz95iVXldfVADAB2pKH1pXRDjMiYSKydpSWY+bYGbXwmpWrm7vXbyijGyZQaweYvQFdb9yZ0RDzuhf27GmuXL5yzFThmUr9c2ddNHpOCV8T0uZ4G0aOkeP3xogZ0THneuu6x0enGu89ZtQpvDft2l3eY/aZURhzLnVK795rWF9bl/cGisOgXxA5kdyT3Kfe6e6jPseJKOv9rqHnwQLQrmc6I6HmXrejyMnuL+f26JaXyrNVX9svxszy9nXIM5NnJ1HoDWseKeeSZzXPbELojAac7bqezyzPsTOiaq5Tnsd6nEzZn8izPeJoVT87+Qzns5zX5dyz/0S1dbt+76Er5q2fixrpnnbdjeW1Gb00P2ek43Wbtzbb9+9vjr904Zj9ATA1BKUAADDkBKUMiz99zjUlKP3aur/auR4AAACA/mpkmaitPe181RVDJgbLqI0JSBOSJRxLxHbC5deMeW1+3rhzVwk2s83azVvK9PDtKC1x2eV3r2h2vPFG2Vf2mcg0oya2Ry49WFBao7Yu7eN9Zc78ErIlHqznlAAxo6m2Ry7NcTPyaM497+3jp5077pjVmYtveu99juwz8rrvLPj59OmC0vHeT1BaR73tne4+8kwuW7+h3NNX3nyzOXnhjycdgNbXt5/pBJyJHz81a8641x1sf7mviaczXf7nz76oLOsXY2Z573XI5ybPXN5Lnr+MdnvZsvtGY9Loej7ruuwzI5H2TuGfMDXH6vqc9x4zUfTZN942ZsTWfu+hKyitkW72l89zwtzeeLR+nvJ5zPWvywGYGoJSAAAYcoJShsUnrry3BKV/7qHf6VwPAAAAAAy+hIld090DAAcnKAUAgCEnKGVYfOGO7SUo/dI9r3WuBwAAAAAGW50ivWu6ewDg4ASlAAAw5ASlDItj1//dEpR+/rYtnesBAAAAgMGUeDRTqT/y4uYyDf2825d3bgcATExQCgAAQ24mg9IHn3+x2bbv9ebX51zauR4Ol2NOm9t8543/JCgFAAAAgKPQl867pNmyd2+z+60DzeLVDzfHnHxm53YAwMQEpQAAMOQEpQyDP/fg75SYVFAKAAAAAAAA3QSlAAAw5ASlHO0+fsU9JSQ99vG/LSgFAAAAAACAPgSlAAAw5AYhKP3o92eXaYpe3r+vef2n7zavvv1Wc9+GZ5tPzZozZrsPnXR6c/ndK5odb7xRtsv2165a0zzzys5yrPa2Zy6+qdm4c1ez9913imzznQWLRtf/6hnnN0/veKWYe9uy5sU9e5q9I/vMlElLHlpXzqm9v17Hzr2iWbt5S7PnnbfL6zLd0vy77i3nmPV5z3nvvef129f+pNk1coybHnl0zHZrX9xSjrvzwJuj7y3n9ckz5zT3bnimXJMcJ+f5w5/cMrq/i267u2y/6P4Hf34+I+93w8j7+sbFC5oTLr+mvPcsy7ps85U580dfH3kvqzc9X45Rj53rXN9LvVbZT8471yg/L7hnVTmnHLu9v3OX3t68NnK83NP28qnwyxff0py49w+bE3b+6+Yjs68QlAIAAAAAAEAfglIAABhyR3pQ+pFTZjX3P7upBI+JGi+9697m9sfWl+hyw/YdzadnXzC6bYLNRJGbdu0uMeMNax4p8WNe2w43592+fMx216xcXULMxJqnLlpStqmRZOLI7fv3N0seWlu2TYiZGHL+nfeM7q/Xl867pOxv2759zaJVa0p8uX7byyWuvO5nceWhBqV5Dxt37Rp9X9vf2N/sfvutcm45p/byrXv3Nl+/6Iry+hqU5v3W67fi6Y3l54S3O958o/zcXp7tct3z+uPmXVli2K2vvz7mvWS7XMdsU69VzjHnnih16cOPlmB182uvle0/durZZdtY9sRT5bgnXvnzgPdwO+a0uc0X7txRAtLjt/0vzUdmX9586ORZglIAAAAAAADoQ1AKAABD7kgPSk+/fmkJJ29e+9joiJhR49E6ymXC0gSXiUQ/f/ZFo9sdf+nCEl3WcLNul9E02zFqDSeffHl7iR9rJNmOTONb868u0Waiy7qs1wW33FXi0XZ0+oVz5jXPv/pqiWA/ftq5hxyU9p7vWUtuLSOG9i5PWJpj5xzycw1K29cvf969fsO45YlIH92ytcSjXz7/vVFKE9smCv3ewsXl58i6LMu2eU2/a5X9rnxmY7leuW5ZVq9/b2T6QRxz6gXNnz732ubjl99dItI/v+EflHA0jn3i7zQf/u3zynaCUgAAAAAAAOhPUAoAAEPum8//d81f2P1vRwO8w+GLy3Z1HqvXZILSfqNZfvaHc5vndu8eDRNriHnLusfHbFdjxxpuzrrh5hJi9k7DHu1j1ddF/l63yRTziTh7Q9C2GsHeNbK/ftHkoQal/bbrfb91eULS/FyD0vpzVaembwegdfnB7knvtel3raJOb5/QNT/X85vMdPfHv/yvOp+vg/nNPf+++erav9L8mTnXj9mfoBQAAAAAAAD6E5QCAMCQ++rav1oiu+Oe/f3DIqND/pnzx4Z8/UwmKJ1om6zLSJlfPGde33Cyxo41yOy3XV2X+DERZL9Isnd/XT76/dnNrY8+UaaAzyiqmZL+qntXNZ84/b2RMuNwBaV1u97l9f31e795XbbL9r3Le6/3dxYsap54aVsJcbOvql6bftcqcm/ao5km5M1Ipidddd2Y7br82rUPNV+4c/tBfXbpxuaT1zzU/PIltzW/cPrFnfsKQSkAAAAAAAD0JygFAIAhd6RPed9vm0yn/vALmw9rUJop6g9HUFplavhMGZ+gNGFpQsrzbrqjrBuUoDQjuma7jAR7xo+XluXHX7qw2bhz1+i16Xetqoz8+vL+fWXk1+znyZe3H7bp7g+FoBQAAAAAAAD6E5QCAMCQO9KD0slOef+9hYubnQfGB5a9AegHnfK+d39dPjP7wuZL513SHHPymaPLvn3Zwmbr6683G7bvaD5+2rkDE5Su3vR8+fmrF142uk3vtel3rapc890j1/z2x9Y329/YP6np7qeCoBQAAAAAAAD6E5QCAMCQO9KD0tOvX1pixJvXPlZGJa3L5991bxn1s8aJn559QbNx165m067dzefPvmh0u4ymuX3//tEgs273zCs7y9/rdsfNu7LZsnfv6OiZ/SLJunyioHTF0xvHTeue6d4z7Xvd3yfPnFPOoXf/c26+s4ySeqQEpTluRhf92tzLR7f5xsULShxbz71ek973UtVrnvuVYHcy091PBUEpAAAAAAAA9CcoBQCAITfTQWnCy2tXrWkuveveMS6+Y3nzubMuKiHm/c9uava++04ZLTPrMtJlYsiM9tmOQmtkmqh0wT2rmhvWPFJiyLy2HWTOu335mO0yLf2Le/aUczl10ZKyTb9Isi6fKCg95UcZLfXNEl0u+tl7y7nnPO4YOfe6Xf6+96fvNmuee6FskxB198j7yrIjJSi9YvmKcj7Pv/pquU53r99Q3lv2Wa9Nv2vVlvea18zUdPchKAUAAAAAAID+BKUAADDkZjooTWTYpR07fvT7s8tIpIlDsy5T1t+34dnmU7PmjNlfRjC9/O4VzY433ijbZfvEqhkJtDfIPHPxTc3GnbtK5BnZ5jsLfj6tfr9Isi6fKCiNEy6/psl0/AlX67nkPeS91G0+cfp55X1km0SbiVqvXL6yBJ1HSlDadU0TmT7y4uby94xcOpmgNKFuYtmZmu4+BKUAAAAAAADQn6AUAACG3EwGpdPhy+fPLyOFHiwAZWolQp3J6e5DUAoAAAAAAAD9CUoBAGDIHU1BaabJz3T27WVzbr6zee3dd5pF9z84ZjnTJ6Oyrtu8dUanuw9BKQAAAAAAAPQnKAUAgCF3tASlmZr9jsfWl+nwVzy9sbn0rnubJQ+tbXYeeLPZ/NprZWr2rtcxdRKPZqr9TI+f+9Ib+043QSkAAAAAAAD0JygFAIAhdzSNUHrMyWc2ly27r9myd2+z96fvlohx9abnm6/Mmd+5PVPrS+ddUu7F7rcONItXP1zuT9d200VQCgAAAAAAAP0JSgEAYMgdTUEpTERQCgAAAAAAAP0JSgEAYMgJShkWglIAAAAAAADoT1AKAABDTlDKsBCUAgAAAAAAQH+CUgAAGHKCUoaFoBQAAAAAAAD6E5QCAMCQE5QyLASlAAAAAAAA0J+gFAAAhpyglGEhKAUAAAAAAID+BKUAADDkBKUMC0EpAAAAAAAA9CcoBQCAIScoZVgISgEAAAAAAKA/QSkAAAw5QSnDQlAKAAAAAAAA/QlKAWBAfOzUs5v7n93UrNu8tfnVM84vyx58/sVm277Xm1+fc+m47Q9V9vn0jleKun9gOAhKGRaCUgAAAAAAAOhPUArAQaPEmx55tNn11oHmt6/9Sed6+ss1y7V7/afvdsq173pdlwSlj7+0rXnmlZ3Np2dfUJZNd1Ca4+R4h3LewJFPUMqwEJQCAAAAAABAf4JSAGY8KL3otrtLXJk/u9YPshqUPvzC5ubSu+4d57Trbux83WQJSoHDQVDKsBCUAgAAAAAAQH+CUgAEpVOoBqW5hl3rPyhBKXA4CEoZFoJSAAAAAAAA6E9QCsD7CkqPOfnM5srlK5uX9+8rMejOA282Sx5a13z0+7NHt/nQSac3s39yS7Nx565m77vvFJmu/YTLrynra2xZp3+Peh41XFz74pay3+w/63O8ubctaz555pzm3g3PNK++/Vazd2T5i3v2ND8cOVY9dnxq1pzm7vUbRl/bdY55bzvefKOZf+c95dxyjnveebu5b8OzzSdOP2/M/t6PyQalB7tW0RV8dt27ydybyPXa/Npro9vc8dj6ZuOuXYctKM31W9Zz/Zc+/GiZur93u1zvXPf6vhfcs6pct97I+MzFN427Rt9ZsGh0ffsa5TnJc5HnY/fIvnqvQc4j59M+v5zv4bjvMGgEpQwLQSkAAAAAAAD0JygF4JCD0sSPS0eWJeZc8fTGMnV7QrwaYmZ9tps/sjzbPP7Stubyu1c0i1atGTnOvhIxfm3u5SXu+/L585ur77u/BH3580vnXVKCyBouJhpM5JjA8IY1jzTb39jf7B7Z5/b9+5sNO14Zs3zr3r3N1y+6ohz707MvaDZs31EiwSUPrS3nuOa5F8r+Eha231sNDpc98VQ5z0de3FyWrdu8dVyEeagmG5Qe7Fplm8kEpZO9N6cuWlKuzdbXXy/Humbl6hJf5j4cjqA0r39q5PrnPHJdcx45n5zHAxufG72u+TM/576s3vT86HZ5Xe5BOyidd/vy8vpNu3aX+17POe8j76ceN+ef+5lnJPc+2+ZZeW3kGAmHs12uQ41Yc33a1yn3oDd6haOdoJRhISgFAAAAAACA/gSlABxyUFojyZvXPjYaKObPOx9/soz2eeKVi5pf+YvnNA+/sLlEme04cc7Nd5awL5FfXdY15X0NFzMCZeLQuvysJbeW2LB3efaXAPGCW+4a3S6vn3PLnaPbJBJ88uXtzXO7dzef/eHcsizvLeeTiLNuV2PDhKunX790dPn7MZmgdLLXqsaSEwWlk7k3uQ6PbX2pBJfHX7qwbBNfOGde8/yrrx6WoPSK5StKnNl7XRO7ZnneW5bl+uY65/zq+cZV9753P+szkXudsLj3vh8378pmy9695b7mfdVr1I5M41vzry7RcaLV/JyQOTFtrnv7Or03ou2B5nsLF4++FoaBoJRhISgFAAAAAACA/gSlAJQ4MEHnRNpBaeLIGie293Pu0ttLANk7TXlbV2A5UVDaGy7W19+y7vHO5RMdO3oDzN5YtkqM+N406WvHLD9U9bx6r2dMFG5G77WaTFA6mXuTEU8zHf6Dz70wJuLs2n+vyQSlHzllVvPolq1ldNUvnjNvzLoadmYU0vyc8811bsef0Xs/Z91wcwmJF93/4JjtIiOg1vfc7z188sw5JUat510D1Th27nuj2h6Kb7/yByVIOpgT9/5hc/zL/7I57tn/tvnK/e80n168vvnoWQs69wkzSVDKsBCUAgAAAAAAQH+CUgBKZJcRHa9dtaZM/d0rozi2o8ts3xVIVjUCzNT1ly27r0xLninN29t80KC0/fr28vY+EgpmRMosbx97MkFpPX4NH9tqtNjeZ+/5VPW8cg17r2tGUU18me0mc626Yslcn/b7mcy96XcN+8WYbZMJSifaT123ftvLZUTR3vOveu9n1zNSZVli2USz/Y5dl7fP++wbbyshakZCzSindzy2vvn6RZOLS39t0drmC3duP4gdzZdXvtl8bd1fb775/H/ffPfNP/5ZaPonzbGP/+3mly64oXPfMBMEpQwLQSkAAAAAAAD0JygFoG/UV/VGl9l+ogD1qxdeVka+TKCXUSUznfm3L/tR2f/5N99ZRqRsx4xdsWC/cLFfDNkbIGYkzG379pUp3DO9evYXmVa+/V5731tVj98VlCYCTQzafs+9I4JW/c63bbLXqiuW7L13k7k3/c6pX4zZVq9L731pm2g/daTQwxmUzr/znvcVlEbOIc9HvW6JeTPiaQ19D6cPnTy7+dhFNzVfuf/t5sR9f9R898AflzC1a1uYboJShoWgFAAAAAAAAPoTlALQN+qreqPLBHeJ70666rpx21ZfPn9+s/X118dNq94VM3bFgv3CxX4xZG+AmKnq2+dc9b7X3vdWHe4p73vPt22y16orlux9P5O5N3XK+4ze2l7eL8Zs63df2g5lyvt+96m+93o/P+iU93V5Pe+EpHkv+bNuUwPXnQcONN9buHh0+VQ45tQLmuOe/kclavrczWPvA8wEQSnDQlAKAAAAAAAA/QlKARgXJfbqjS4zOueed94uUWCmaq/bfXr2BWVUzSyr4WGmem9HkpffvaKMJtkOLKciKM36BKEJQ+s2Ob+Mjtl+r9ku55PzqtvlfDNS6O6332pOv37p6PL3o9/5tk32WnXFkr33bjL3JsHnIy9uLhHrNy5eMLpNwtZEoL0xZlu/+9LriuUrynn0XtelI+8lyzMqaJbl+uY653q33/tV964qU9HX+5nz37hrV7l/+Xvd7rh5V5bp6p98eXsJQruuUdTl9bwzmmmu7eLVD49uEwvuWTXmWZ9SI+/32Mf/VnPSW3/S/Mqld3RvA9NEUMqwEJQCAAAAAABAf4JSAA45KP3o92c3D2x8rgR/CRMTDV6zcnXz4p49JVL8jflXj0aLmUL8oZH9L7zvgTLdfH5OPNoOLGtUmFEtL75jefO5sy7qGy5ONiit+8w+bljzSHPLusfL33Ps9nvNfrIsr739sfVlWviM3JnzzHvMe20f51BNJiid7LXqiiV7791k7k22S2ibkUyzbNGqNWWbF0a2yTF7Y8y2el8SdvZOpx8JWvN+8vqntu8oo4pmBNGsS+SamLR9XUfPd+S4ue51u7yuHZTGvNuXl9dv2rW7hJ/1feV91HC46xq1l9fnqev8Mlpq9pVwtR2tTqUP/2BO85t7/n1zwq7/owSmXdvAdBCUMiwEpQAAAAAAANCfoBSAQw5KIyFgRnfM1OmJHhPmJQg8du4Vo9t84vTzmvs2PFsiwASDiRDPWXp7eU17NM7sa9n6DWW7V958szl54Y8/cFAaP/zJLWXEzZxfQsElD61r7nhs/ZiRS+t7S8SZ88t55jxy3jn/uq/3azJBaUzmWnXFkl33bjL3Jrquz9oXt4yLMdvqfclrurRfm/eU+5p9Z13+XPrwo2Omma/b9b73OlJo+37GmYtvajbu3FW2q9t+Z8Gi0fVd16i9vP089Z5fvU5fmTN/dJvp8Knr1pW46ZM/WtO5HqaDoJRhISgFAAAAAACA/gSlAAy1rliWmXfBLXeNG6H0aPWh3zqzOWHnv26+vv7vdq6H6SAoZVgISgEAAAAAAKA/QSkAQ01QOrMymuqt6x5vjpt35eiyjMa68pmNzY4332hOvPLno48ezb686kBz4r4/Mu09M0ZQyrAQlAIAAAAAAEB/glIAhpqgdGZ9/uyLyhT229/Y39yw5pHm0rvuLdPOZzr7+5/d1HzklFmdrzva/NqiR0rg9GfmXN+5HqaaoJRhISgFAAAAAACA/gSlAAw1QenM+9SsOc3d6zc0Ow+82bz+03ebl/fvaxavfriMXtq1/dHoly68oQROn/zR6s71MNUEpQwLQSkAAAAAAAD0JygFAJhhH/nhghI4ferHj3euh6kmKGVYCEoBAAAAAACgP0EpAMAM+/AP5pTA6bM3bepcD1NNUMqwEJQCAAAAAABAf4JSAIAZ9qdOObsETp+7+YXO9TDVBKUMC0EpAAAAAAAA9CcoBQCYYYJSZpqglGEhKAUAAAAAAID+BKUAADNMUMpME5QyLASlAAAAAAAA0J+gFABghglKmWmCUoaFoBQAAAAAAAD6E5QCAMwwQSkzTVDKsBCUAgAAAAAAQH+CUgCAGSYoZaYJShkWglIAAAAAAADoT1AKADDDBKXMNEEpw0JQCgAAAAAAAP0JSgEAZpiglJkmKGVYCEoBAAAAAACgP0EpAMAME5Qy0wSlDAtBKQAAAAAAAPQnKAUAmGGCUmaaoJRhISgFAAAAAACA/gSlAAAzTFDKTBOUMiwEpQAAAAAAANCfoBQAYIYJSplpglKGhaAUAAAAAAAA+hOUAgDMMEEpM01QyrAQlAIAAAAAAEB/glIAgBkmKGWmCUoZFoJSAAAAAAAA6E9QCgAwwwSlzDRBKcNCUAoAAAAAAAD9CUoBAGaYoJSZJihlWAhKAQAAAAAAoD9BKQDADBOUMtMEpQwLQSkAAAAAAAD0JygFAJhhglJmmqCUYSEoBQAAAAAAgP4EpQAAM0xQykwTlDIsBKUAAAAAAADQn6AUAGCGCUqZaYJShoWgFAAAAAAAAPoTlAIAzDBBKTNNUMqwEJQCAAAAAABAf4JSAIAZJihlpglKGRaCUgAAAAAAAOhPUAoAMMMEpcw0QSnDQlAKAAAAAAAA/QlKAQBmmKCUmSYoZVgISgEAAAAAAKA/QSkAwAwTlDLTBKUMC0EpAAAAAAAA9CcoBQCYYYJSZpqglGEhKAUAAAAAAID+BKUAADNMUMpME5QyLASlAAAAAAAA0J+gFABghglKmWmCUoaFoBQAAAAAAAD6E5QCAMwwQSkzTVDKsBCUAgAAAAAAQH+CUgCAGSYoZaYlKP3OG/+pcx0cTQSlAAAAAAAA0J+gFABghglKmWlfWfVW85t7/n3nOjiaCEoBAAAAAACgP0EpAMAME5Qy07667q82x7/8LzvXwdFEUAoAAAAAAAD9CUoBAGaYoJSZ9s3n/7vmm8/98851cDQRlAIAAAAAAEB/glIAgBkmKGUmfei3ZjXfPfDHza8/+Jc618PRRFAKAAAAAAAA/QlKAWbYr55xfvP0jleK/L1rm0Fw5uKbms2vvdZceOuyzvXT4aLb7m5e/+m75c+u9XCkGpSg9GOnnt3c/+ymZt3mrTP6ffXg8y822/a93vz6nEvLzydcfk2zcdeuZvGah8dtO1WOhO+8w+WXLryhPH+/tmht53o4mghKAQAAAAAAoD9BKTA0fvvanzS73jpQgsO2l/fvKxFSQqmu130Qx869olm96fnR47769lvNIy9uLsvrNkdaUPqRU2aVc4z8PcFWwq0EXF3bV+fddEd5f5ctu69z/XQY1KB0std40PT7zPU62t73+zFTQelZS24tn9tlTzzVuT4Wr3642Ttyn65YvqJ8Tz7+0rbmmVd2Np+efUHn9tOhNyj9zoJFzfY39je3PvrEuG2nypHwnXe4fHnlgea7b/7n5phT53Suh6OJoBQAAAAAAAD6E5QCQ6PGbQ+/sLm59K57iwX3rGqe2r6jxFL3bXi2+dBJp3e+9v34wcjxdrz5RrPzwJvN3es3lOMldsrPceqiJWW76QxKJ3Osb82/uoRac26+s/w8SLHjdAalh/NYR2tQ+rmzLmouvmP56OftpkceLQFe+zMYp113Y+frh8lMBaWJQjOyZ0ba/OI588atT0D65Mvby/P51QsvG7d+pvQGpVNtuo83nT569tXNd9/6L81XHni3cz0cbQSlAAAAAAAA0J+gFBgaNShN1NZe/tHvz24e2/pSGd0uMWV73ftVw82tr7/efPuyhWPWHX/pwmb7/v1llL/EWkdaUJrRCBOXffn8+eVnQWk3Qemh6/cZZGanvM/9eO3dd5pzl94+bt1JV11XAviVz2w8rMH9ByUoPTw+9FtnNr+x9X8qz95Hzxn7v1VwtBKUAgAAAAAAQH+CUmBoTBSzZVnWZZu67FOz5pSRRRNTJRzMn0seWlcC1N7XLVq1pkSYdR8JsxJoJc6s21aJshJnZfTSE69cNCbynHvbsubFPXvKiKm7R/bVe7y8dvZPbmk27tzV7B3Zf2Tq6RMuv2Z0mxonrn1xS7Ns5Pz3vPN2iaEi76Ot91rUc7njsfXj9new2LE3sKw/X7l8ZXPvhmfKyJR5Xzn39vnGMSefWbZ7ef++8pquax2Z1jrvt7737OuUHy0eXX8ox3y/93fR/Q+WP/Oa6oOGZpO9xvGJ088r97V93ksffrTEyb3bZdTd3P/6nGRE3px7O4LN+81zWq99rllel+tTt6nn99DI+V1+94rRbXe88Ub5ebKhYb/P4Lzbl5f7lGvbXt7+HNVzuP/ZTeU+1fef+/LDkc9E+3Vx7NwrmrWbt4x5/73PwJhtRva1Ze/eZv5d985IODmTQWmNRlc8vXHcutyr3IM6YnFXlN77+c0ztHrT881X5rwXpdf95N63v2OjK9TMfcnrs5/sL/vtfc56X9f7bNXvgi69554p63Pv8wzkWcgzUc89++t9ff2c1mO0P09x5uKbxn1H57urrj+U7/yp9KGTZzdfW/fX3wvrbt/WuQ0cjQSlAAAAAAAA0J+gFBga/WK23sAzyzIN9IbtO34WGa4t03Kvee6FEgcl+Kmvzb5qhJQ4aMXTzzbfuHhBWZ44qE5r3ytBUUYATThU46Jsn5FLc7yEfxtGliXkmn/nPaOvS+yWyCqjmyawSui4bd++EtV9be7lZZsa3uVcM+pq4qdrVq5uPjP7wjI6akKnyN9rVFWdfv3SEgnmz7qs7u9gsWNvXFV/zv7q+d7+2PpyD9pBV67/0pHrlfeVoC3XuoawCRtrRJZrmfvx/KuvlusT+XuW1es82WN+kPt7wuU/Kvfu6vvuL8fKn18675ISptXXHarJXuOc/1Mj551rteyJp8p555rl/B7Y+NxoiJY/83PeT8K8ul1el/dT79FHTplVAs32dvV65frkOmW7en5Znmctz12eqYRwWdYbCfbT7zOY65n9rt/28pgwNu+xfi7bz3WmaM/9v2HNI+UZ3z3yvmbf8POo9Lh5V5ZAMOeX86zPSvZVw77cs6zP5yfvJ89Kjp/rc11P2DodZjIordPa5x60p72v303P7d7dfPaHc8csa3+ecr1yX/IZyjOUz1Duc65vrnO2yT3velZ6w9B67zK6c/u+5BlPeNzvdb3PVqbnz7m03fvUM+Uzm2c+z3797sn3bH3+67nnc5b3FznGus1bS9j6vYWLy3dpjtH7nRc5x5zrpl27y3NXPyft76l6DSfznT8lRt73r1x2V/Otzf/iZ8/c893bwVFKUAoAAAAAAAD9CUqBodEVsyW8u3bVmhIA1cgoy89acmuJlebc8t6ofFGjq3ZcVYPDREm9o+d1xVNdalzUDo4i0+8nlkvolJ9/5S+e0zz8wuYSNtWQKzJyYCKkBEn5uYZ3ibISZ9Xtoh6rHYNVNazNe2xHfXV/7zcobYeOkUhw54EDJczKz/W+3Lz2sdFrmD/vfPzJ0Zgw9+WRFzc3W0fe09cvumJ0XzVEfPC5F8prJnvMD3p/o/f9fhCTvcZXLF9RntVEdnVZzivnl+V1FMkEwYkscw3b533VvavGBKV1u/a1j4TL2V8dYbffMzXrhptLpJogri6bSNdnMHLsPHt53vPcZ1li1oSjNTKt55DRHmvoGglE85zk+chzEvnMJEhM3F23S0CdeK+OwnnBLXeVa9GO975wzrwSniam/fhp544unw5TEZRmBMqu5V3y/ZHvkfa09/V+tUcs7v0OyXXK9cp9+eSZPx/VduGKB8r1zXXOz7nnXd+JvWFoAsx8putnNern/NEtW0e/ow8WlPZKcJ99RI3vE8/mfSQUbz//ee7zXOf5rst6jxe93wH1me19RmskW79b6zU82Hf+4ZBn4BdnXdb80twbm0/9+PHm11f/V80JO/9NedZ+87X/q/nEgp9/l8CwEJQCAAAAAABAf4JSYGjU4CgBUK/EQpkivOt1bb1R0USRVNfyLv0iz8RZCZOyr/b2vXpDqhredb2u37GiRlu9045PtL+23riq9+cqAV/72uS826PDVnW687y+hlZdU3Kfdt2NJRBNaDbZY/Yz2fsb/Y71fkzmGuf9JajLPWqPIhm91yfn3TVCbn1W6jm3RwBtb5egNmFtb8zZe34J8zJqY332Dqb3WW2r97uG0XXb3qi19xwSAia0TkCaZzgjU2a73mcl7yPvpz77Naa9a+QaZF1724M55i9e2Hznjf94mP2n5oRX/vcSOR1Ok41Kv3nJVeVeJuytcWV5jkauUXvE4t7vkDyXiXlzzdsRaK9+n6Xez1yXru+t3tdN9GzVEXt7R7Ltp+uz3XWevdvVwLr3OzTan7Wu9xOT/c6PX1u0duT+/sm4+30w3z3wx823Xvgfms8sebr5U987q3PfcLQTlAIAAAAAAEB/glJgaNTgKPFZexrkOsV3YrP2qHLHzr2ijBSXdYmGqnZUNFEk1bW8S7+4qC5vx0WZVv2yZfeVKZQzxXT7vGpI1S+8i37Hiox+mem/E5a1l0+0v7beuKr35/Z27WuT/bbfR69sP1Es1jbZY8YHub/R71htve+t3zWczDWe6N7VdTUAzX7a76Oq17Gec7/t6roar/Y7v7r8YPelmug+5jg5Xh2FMlFeRnA86arryvqJrlH2V2O9eoz2dW+r1y+R4a2PPlE+RxmNNdONZwTXyYTl8cVlO5s/u/zVw+ZL9+1/L/p76780J+z814fFN5/7Z53n3qWGobkHCXPraJu9IxZ3PYe57nldrm+i1Iz4mZFj26N+9vssdT2Dee0TL20rcWbXvet63UTPVkb0TazcOxJv5L0tXvNw2VdGVG0fr/3Z7jrP3u+A3p/bsqyOANt1DaMu73rGe2XU0S/csa35wp3bJ/T5W7eUkUkzEumfmXN986Hfem+EVxhmglIAAAAAAADoT1AKDI2JgqMyFfi775SoMj9nxMfElZn+OtOIJyKKTDffjor6RVJZ3jVCZJVwKNFWorbJxkUJoTL1dCKrTGX+7ct+VM7j/JHzy7Hq+8qynGNXlNTvWHX0y/bohNVE+2ubbFyVn9vXLPtNOHjtqjVjQt8qI05OdO/aJnvMD3p/o9+x2jJ6avu95Oeu7SZzjfvdu6gjGx6uoDTPQMLr6QxKI6M4JkhMoJj30o4ZJ7pG2V9vUNobjld1NNv62nwOM816gtKEpXkWz7vpjjH7nw5TMeX9ocpnId8v+bOO4FpHiK36PYf5Ljvt+htLTJp7mDgzgWrdpt9nqfcZzCif2S73/4wfLy3Lj790YbNx564xx+x9Xb9nK6/dvn9/mZa//R8MRM4555jX5X1mavrs7+r77h/32e76rPR+B/T+3JZRkg9nUAq8f4JSAAAAAAAA6E9QCgyNiWK23nVLHlpbfj5Y/NQvkqrTd/fGWJFYL+HmwaY/7o2LEr5lWu8Hn3thTPTZe+45t5xjV5TU71gZBTJTpicka28fE+2vbbJxVX5uX7NEhO2RKLvUqdWzbe+6z8y+sPnCOfPKNZnsMT/o/Y1+x3o/JnONa/RbI8/2ut4p7/u9v/yc5fWcP+iU93V512eqSz1+v+0TEyZizKjBeT/tz0+/c6jxa53yvl6Lrji6Lc/Nl867pIz6W5d9+7KFZT+JDz9+2rljtp9qR0JQmuuX5yvXLvF617PR+x2SKDOva3+f5JqW0V9HPh8X3HJXWdbvs9T7mcuowfk5IXndput7q/d1Xc9Wnt3HX9pWvl9O+dH46fgzRf/OA+Ofx67Pdu/xurb7oFPe1+W9zzhweAlKAQAAAAAAoD9BKTA0JorZygilP323WXDPqvJztukdYTSj22UUyHZU1C+SqmFQ4rRvXLxgzLo6Yl5CpwRPk42LalCXeK4dytXpnOv7qtt1RUn9jpV4LNNb947gFxPtr603rur9ub1d+5plxMiMDJkYsh335Vwy+mqW5To9tvWl0em46zafP/uiZtOu3WWUwQSXkz3mB72/0e9Y78dkr3FG0M21yj2vy/IsLB05zyyvQXAdXTIj2baflfqc13Ou2/VOBT7/rnvL/mrQ2e/86vL67B3MRJ/ByD3Ic5hjJ75rR8b1WLlH7ee0fp7qM1Cflbw+wWDdLjKCZv085nnrDZlrtNv7+ZgOR0JQGrkuuXYJuOs1ba/v/Q5JIJntewPeGlfWZy0jdObZm3f78tFtcq8yCm37M5dnLMdORF63yz3Ld2n7vmS79ut6n636ucjI09d1BJ5RX3PLusdHl+V1+Tz0frZ7jxe93wH1+e19RjPy6Za9e0dH3O29hnW7uvxg3wPAByMoBQAAAAAAgP4EpcDQqPFQ71TYCagSPr24Z08ZsTDb1tAuAdENax4pwVH+nnioHRVNFBwmVkywFhmtL8fKn/k5AdYPfvaaycZFCbsSeCWQemhk2cL7HihTtOfnnFcNqWp41xUl1X3kHH68+qEyBXuNoBKV9m4fdX+JpNrXrapTiPfGVb0/V/m5fc0ywuEDG58bnSI7sWSmIM/9SET2G/OvLtvV65lp6hP+RmLSLKth6GSPeTjub91HAsSL71jefO6si8ZtM1mTvcZ5Jp7avqM8rxnxMOvy/CbAzDXMtcz+Rq/pyLORER/rdnldOyjNPu9/dtOY7TI6aN5ze4rwfs9UXd4vEO1VP4MTbZ/nMPehPd191GNlXaanz/1ftGpNeUbaz0DUZyUjleb+tt9/7nUi5YxYmW3y+uwn2+Qa5Fr0+yxMpSMlKK3PdZ6TBMy963u/r7qeoXpfEoZ+85KryuvyZ37etm9fWZ/PeD7LieHbn7kcM8fOumxz9/oN5T7lvre/I3sDz95nq97fF0a+R65cvrKcV1U/r/W7L89Fno8cL89Wjt/7PZJnIp+zW0een37feZFgNtvluynPaP0uaz+jvdewvrYuF5TC1BKUAgAAAAAAQH+CUmBo1OAoAVBbliWE+sqcn498GT/8yS1lRMxskxhoyUPrSlTUHtlyouAwjp17Rdl3gqX2sbK8bnMocdEnTj+vuW/DsyVYSsCVAPGcpbeXUKuOXNov/qsSWtX3lRAso1omvOs35Xw75OtSz7s3ruqKrery3muWADKjYeZ95DW5Xr3XKb6zYFF5z3nv9f1nWV1/KMf8oPc357xs/YZyL155883m5IU/HrfNZE32GmfbPAM5bo3s8ufShx8dE1/W7XqflQRueT/t69N17fO6T82aM7pNv2eqLj+cQWmufe5Be7r7qMdKwJv3W99/7mHuZXvbOOHya5pM2V/e/8h2GSHysmX3jRkFt71N9pVrkOPWMHc6HSlBaY0sc63b085XXd9Xvc9Qrmeua65v+7Xtz1yes3s3PFNG0c2xcn+zTb7DEpXveOON0XuSyDSxef5eRy49WFBavwu6tD/T+d6vYX7Oe+3mLSU4zTPYHrk0xy3fPSOvz3v7+Gnn9v2+OXPxTc3Gnbv6fk91XcP2ckEpTC1BKQAAAAAAAPQnKAUYYom3MlV1AqneIJGjzwW33FWCuN4A7kiSeLB3uvvoF7UeLY6UoBTgaCcoBQAAAAAAgP4EpQBwlMmIkZma+7h5V44uq/FwYs0Tr/z5aIlHkpx3Rovsne4+BKUAHA6CUgAAAAAAAOhPUAoAR5nPn31RmXJ7+xv7mxvWPNJcete9zepNz5fpt+9/dlPzkVNmdb5upiQezaipmdY8U6HPu335uG0EpQAcDoJSAAAAAAAA6E9QCgBHoU/NmtPcvX5Ds/PAm83rP323eXn/vmbx6ofLKKBd28+kL513SbNl795m91sHyjkec/KZ47YRlAJwOAhKAQAAAAAAoD9BKQDADBOUAkwPQSkAAAAAAAD0JygFAJhhglKA6SEoBQAAAAAAgP4EpQAAM0xQCjA9BKUAAAAAAADQn6AUAGCGCUoBpoegFAAAAAAAAPoTlAIAzDBBKcD0EJQCAAAAAABAf4JSAIAZJigFmB6CUgAAAAAAAOhPUAoAMMMEpQDTQ1AKAAAAAAAA/QlKAQBmmKAUYHoISgEAAAAAAKA/QSkAwAwTlAJMD0EpAAAAAAAA9CcoBQCYYYJSgOkhKAUAAAAAAID+BKUAADNMUAowPQSlAAAAAAAA0J+gFABghglKAaaHoBQAAAAAAAD6E5QCAMwwQSnA9BCUAgAAAAAAQH+CUgCAGSYoBZgeglIAAAAAAADoT1AKADDDBKUA00NQCgAAAAAAAP0JSgEAZpigFGB6CEoBAAAAAACgP0EpAMAME5QCTA9BKQAAAAAAAPQnKAUAmGGCUoDpISgFAAAAAACA/gSlAAAzTFAKMD0EpQAAAAAAANCfoBQAYIYJSgGmh6AUAAAAAAAA+hOUAgDMMEEpwPQQlAIAAAAAAEB/glIAgBkmKAWYHoJSAAAAAAAA6E9QCgAwwwSlANNDUAoAAAAAAAD9CUoBAGaYoBRgeghKAQAAAAAAoD9BKQDADBOUAkwPQSkAAAAAAAD0JygFAJhhglKA6SEoBQAAAAAAgP4EpQAAM0xQCjA9BKUAAAAAAADQn6AUAGCGCUoBpoegFAAAAAAAAPoTlAIAzDBBKcD0EJQCAAAAAABAf4JSAIAZJigFmB6CUgAAAAAAAOhPUAoAMMMEpQDTQ1AKAAAAAAAA/QlKAQBm2M+D0uc71wNweAhKAQAAAAAAoD9BKQDADPuF0+eVwOnTN6zvXA/A4SEoBQAAAAAAgP4EpQAAM+xPn3tNCZw+uXB153oADg9BKQAAAAAAAPQnKAUAmGEfv+KeEjj9yqV3dq4H4PAQlAIAAAAAAEB/glIAgBn2hTt3lMDpmNPmdq4H4PAQlAIAAAAAAEB/glKAw+hXzzi/eXrHK0X+3rXNIDhz8U3N5tdeay68dVnn+ulw0W13N6//9N3yZ9d6OJp8/cnfa769/V91rgPg8BGUAgAAAAAAQH+CUmDgPfj8iyU8bNvzztvN+m0vN99ZsKjzNVPlSAxKr1i+otm2b1/zzUuuan59zqUjf3+9XLOubavzbrqjefXtt5rLlt3XuX46DEpQetMjj457/mL7G/ubZes3NJ84/bzO10H1i2dc0nz3wB83X7x7V+d6AA4fQSkAAAAAAAD0JygFBl7iyJ0H3myuXbWmufSue4tbH32iLNvx5hvTGpVOZ1A6mWN95JRZzaNbtjYrn9nYfOik0ycdlB4JpjMo/SDHSlCa+DZ/1ufv8rtXNA+NXOO9777TbNi+o/n07As6Xwvx1Ud+t/nugf+7+YXTLupcD8DhIygFAAAAAACA/gSlwMBLHJlIMrFke/lZS24tod+yJ54as3wqHWlB6UlXXVdGypxz853lZ0Fptw8alO5660Dz29f+ZMzyBLxLR9YlKs0ose11UH38intK2PTVtX+1cz0Ah5egFAAAAAAAAPoTlAIDr19Q2hVPJvKb/ZNbmo07d5XQL555ZWdzwuXXjHvd2he3lCnLM31+3cfHTj27Wfrwo2X00wSI+fP2x9aX5Vnfjjzn3raseXHPnmbvyHa73zrQLHloXfPR788ePc4HOZfI8dsSNtbXVVm2cdeu0REyJxuU9gaW9XUZdTOjb768f19Zv+ONN8rPeS/1tcecfGZz5fKVo9vkGvW+98jIsXm/9b3nOpzyo8Wj6+s5ZF/3bnimxMG5ltmufY3iU7PmNHePXJ/2fek9Zg0/F61a02x+7bX3/n7/g+XPvKbK+8z7be9/Iv2C0vjW/KtL0Fuvd31PP1790HvvfeTv9Rpnavzc4/Z7yLNWn63qYM9gdezcK5q1m7eUZ6br2Rq3zci+tuzd28y/694x97PreO2p/Kfymc/r63HzPGX/nzxzzpjnIcf74cg+6+vjgzyDZy6+acw2U+ljFy1tTtz7h80JO/9N8wunX9y5DQCHl6AUAAAAAAAA+hOUAgMvsV5XBJjROROSrXh64+iyxHIJ0R5/aVsJIRMXbtu3rwSGX5t7edmmBm0JzGoMeM3K1SVGe2DjcyW+S1CXqc1r5JnlWV/jusR02/fvb5Y8tLZZcM+qZsPIstdG9jf/znsOy7l8ZvaFzfGXLiwBXOTvvaOUJiJNTHrHY+tHl9X9vd+gNOFkzi/nmvNIzNeOKRMMZlTOvK9c9/Y1um/Ds6Oh4qmLlpR78/yrr5brE/l7lmVd+xwSrdZrlHAyx8s1ru837zPTyr8XDa4tx1zz3AvlmiUizDaR8DMBYs4lr1/x9LPNCZf/qPny+fObq++7vxwrf37pvEtKkFhfdzATBaW917u+p5xD3m+em0S0eS9PjbyHXLeMqJv3kOvXfrby+sk8g9nuuHlXljg09yf3qV7fHW++USLKbJP3mfV55nI/c33Xb3u5XKPr7n+wbJP7lfvWdbzck8SmU/nM5/nNvm5Y80h5/nePvDbHyL7by7eOvNevX3TF6DlP5hnMdcj16H0Gs915N90xes5T4RdOn9d88a5Xmu++9V+a47f9L80vzrqsczsADj9BKQAAAAAAAPQnKAUGXldQ+s1LriojDyYyrKNe/spfPKd5+IXNzbrNW8fEl5kOPuFbgrL8XIO2BHkJ8+p2p1+/tARtCQjrsli8+uHRoLDGde0wMupIlas3PV9+/qDnEvVY7biyLfvKMRPW1mW9gWM//YLS3vOYdcPNJdz7/7f3r8FelXme6Pkqq6zKzK6szvu98tKZWW3eszLTyku1R1LxhiiiKF5T0cRExAuIqCAJgiCIoiAqIHIVQRAveS4RHafPiTjdHdExPVEnJib6RHRMx5k5M9FnZqqjT3RHT/RM98k1+7usZ+fai7U2f9gb2Jv9efEJ+K/1rGc9z7MefPX19yREmN9Zg6zFonUbhoN7+XPJxmfr8N4Pb55XffBnl1RPvbJ9RAgwEuxMsPDJbS/Xz5QxNIOSkcDlvmPHqp/e/v53vWzBA/XYZt7//rH+kaDjs6/trrYdOFB99tJZ9bUSKE3YsIytaM/3ZIwWKC17JmPO77455Uj8BBkTsizXMsaMNdfzLXNtkD2Y9c0+23n4cPXN624ZbpPQccKYJWB99f0P1+vRDHx+4YrZdagyAd2PXnRl/U3ST/Zq83u+Xw32/W9wuvZ8/v2WyrqR75y91r6e5zKPzCe/B9mDuZY9mxBsc8zfu/H2OsBcxjx206rzLry6+vCVd1QfvWlZ9YUlu6vzn/8/Vhe8/bs6zPSNZ/5J9Qc/v6LjOQBOF4FSAAAAAAAA6CdQCkx6CUcmpNeWUFmO1+56pqkE0EpIry90mVBgM5BWfO6ya6vrFi+rvnbNjb0hzxzTnSDciYKcg44l+t4VCdCteGFrlYqTzaPQxxoobT+XqpI5VryMN392rdGVCx+qQ4PprwQNm5Vji4vuurcODiYU2R5DkQBkCU82r7dlrBlzxp7fGVvfc33vGkRXv1n/v7zt7jqc2Qw1d70nc316x846TPvFK2YPX4/2Wg2yByPzbq9v9kH2Q9kvJZz68FCfzT3SVKrcRo7H72pzpvZ8aXf/+o2d18uaDrIH87uEeFMl9WQq0hbf2/uv60DSyfrBgb+p/t7af1h9eMadnf0CcHoJlAIAAAAAAEA/gVJg0kvwLKG9O59YXR9vHalamGOxU9GwWYExwbEblz5eH/WdI7UT7itOFGhrBxS79IXryvVmn2MZS/S9K1KhNUeJJzTXvD5af03t4GPfc+V6GW/uN+fRlv7aAcI+7TE0r7cDnAk7pqpkrjff1/xeeV/7uaLvXU3tuZW1SL/N603Zl6VyZnS9Z7TvWO6VYHDe2ZxTl7K+7bEU5T2pkPrA08/Uey/Byvx7ufWxJ6qPTZsxor/L732wDmimCmgq1C7esGlEZdm+8ZfrzT0zlj3ft2/K9bKm7e/UVtplnqmGm3nlvxMJ9eZes3LsaD45b11dcXRUi1+rPnPvluoTdzxZ/en1D1bnXfR+tVwAzh6BUgAAAAAAAOgnUApMen0hu89ffm314v4DdXXFVFpM1ciE4RIey/HX37vxl/UzVy1aUh99faJAW997mgYN1411LNH3rkigNhUvc2R58/po/TW1g499z5XrZby53w73NqV6Zl8wsK09hub1ZjA0VTwTnk010ByfnjFFjlZvfq+8r/lcU9+7mlI9tTmX/M719JvvmD+b9390y7zjwold7xntO5Yqn6cSKM3x8s3xFKUCbGmfPXLHilV1oDTB0ny/GfctHtFn3p21Ld83YdBUS00/feMv18ueGeue79s35XpZ00H2YHk2Y/rmdbdUC9asr7YdOFDPK0f8X3Db3cNtADi3CJQCAAAAAABAP4FSYNIbLWTXvJfgXMJiqUqYIFlp0w6q9QXactR2jh9PgLF5PaHB9J3Q3aDhurGOJfrelXE8+9ru+sj7Zt8xWn9N7eBj33PlehlvQoYJ811w610j2jWVY/LTtn3vM9Ovqb5wxex63F3hy8jvrFHWKr8XrFk34neRsWZsGWN+Z4xd7aLvXYMYrd+2rveczJH3g+zB8kzX92/KWn9pxvUjjnv/3o231/ty8+491UcvurLuL+uXP0ub/D1ru+/Yseqnt88/Y3u+3a59vazpIHsw788+K3utXL94/n11X6l222wPwLlDoBQAAAAAAAD6CZQCk147OFiUCqWpPPjZS2cNB9VSubEZIpv7yPLqjffePWGgLZUdU8Fx/qq1I67nmPBUWJx298KBw3VjHUv0vSvj2HP0aP1ns32M1l9TO/jY91y5XsZb1igByGZQMRViU4ky1xJI3LDz1eMqqJbv9dQr2+uQZVf4MvK7GeDMu7P+F85bMNwm70tlz4wtYyztms819b1rEKP129b3npuWLa/XLd+/XMu+WDjUd66nOmiuDbIHy/rmmPoEPpvtLrr73roiZ/6eb9QOXpZwa9lTVy58qN6P7felAm6Zc98+LNfHa8+X4Ghp175e1nTQPZiqr+09mP9O5L8XJ/r3AcDkJVAKAAAAAAAA/QRKgUkv4a/2Edc5wjtHoB/67XvVXSufrNslLJewYo61XjP0zO2P/6Y+Fj2/E/I7UaAtVSB/s3VbHVZbumlz/Z5UQ8wR3rme+4OG68Y6lmYfmfuvVq0ZPoI9x4qXY/7bz5T+ErYsa9VUjkNvBx/7xlGul/GWNcq6Z2wJC+ZbvHLwYF2d8jt/W1kz4c+MO98o4cRImDTXSjC0L3yZ380AZ0KUB4a+QcZxz+qn6iqe+XuezZ8ZY9pljH3Bz9JHwpTXLV5Wfe6ya49r02e0ftv65pT98dzuPfVeyp7Kt0ggMnut7K20G2QPpl1Z31QqzZqU/tIu65NQ5c9+Ob9uk+8y72//7aQyZ/Zg9lDfuFIRNs+VPXam9vyggdJB9+Dsh5bV65h9l/2Xdhtf3VWPJwHf5jsAOHcIlAIAAAAAAEA/gVJg0kvwLIG0poTCEiCb8/BjI6ohfmzajOrxzVvqIFnaJFh5xcKH6iPYS+XEvkBbpLLhwrVP14G6vCd/5neu5/6g4boY61giocBUWcxYVm55sa62mN/tipJF6a+5Vk1l3O3gY984yvVmyC+Bvrw/80gfCSMmqPiNWTeNePZHt8yr55y5l/nnWrnfF77M73aA89Jf3z+8DvkmC9asr0ORzcqlowU/M+YENPMt9r71VvWT2391XJs+4xEojeyHjKFvbxUn2oPF9+feUaUKZ72/htrtOHSounHp4yOqdjbbpK98s3y7EkyN9rjK9/zKzPcre56pPT9ooDQG3YM54n7rvv3v78G/XaP2fzMAOLcIlAIAAAAAAEA/gVKAc0iORt995Ej1w5t/H8wEAOB9AqUAAAAAAADQT6AUAACAKUGgFAAAAAAAAPoJlAIAADAlCJQCAAAAAABAP4FSAAAApgSBUgAAAAAAAOgnUAoAAMCUIFAKAAAAAAAA/QRKAQAAmBIESgEAAAAAAKCfQCkAAABTgkApAAAAAAAA9BMoBQAAYEoQKAUAAAAAAIB+AqUAAABMCQKlAAAAAAAA0E+gFAAAgClBoBQAAAAAAAD6CZQCAAAwJQiUAgAAAAAAQD+BUgAAAKYEgVIAAAAAAADoJ1AKAADAlCBQCgAAAAAAAP0ESgEAAJgSBEoBAAAAAACgn0ApAAAAU4JAKQAAAAAAAPQTKAUAAGBKECgFAAAAAACAfgKlAAAATAkCpQAAAAAAANBPoBQAAIApQaAUAAAAAAAA+gmUAgAAMCUIlAIAAAAAAEA/gVIAAACmBIFSAAAAAAAA6CdQCgAAwJQgUAoAAAAAAAD9BEoBAACYEgRKAQAAAAAAoJ9AKQAAAFOCQCkAAAAAAAD0EygFAABgShAoBQAAAAAAgH4CpQAAAEwJAqUAAAAAAADQT6AUAACAKUGgFAAAAAAAAPoJlAIAADAlCJQCAAAAAABAP4FSAAAApgSBUgAAAAAAAOgnUAoAAMCUIFAKAAAAAAAA/QRKAQAAmBIESgEAAAAAAKCfQCkAAABTgkApAAAAAAAA9BMoBQAAYEoQKAUAAAAAAIB+AqUAAABMCQKlAAAAAAAA0E+gFAAAgClBoBQAAAAAAAD6CZQCAAAwJQiUAgAAAAAAQD+BUgAAAKYEgVIAAAAAAADoJ1AKAADAlCBQCgAAAAAAAP0ESgEAAJgSBEoBAAAAAACgn0ApAMf5+C+uqp7fs7eWv3e1Odv+6s5fV/vfPlbd99TTnfdPxsXz76u2v/FGdc0DSzvvAwDnBoFSAAAAAAAA6CdQCkwI0+5eWB145+1qxQtbqw9cMK2zzUTx5EuvVAfffaea/dCyzvsJOCbomMBj1/3J4FQCpd+6/tbqtSNvVs++trv6yIWXd7YZT+MZKJ1x3+Lq9aH9d+PSxzvvnwmfumRm9cimzdXuo0eqw799r95jL+zdW/3olnkj2o3nvAFgqhEoBQAAAAAAgH4CpcCEsPz5rXWIbtebh6uvXXNjZ5uJIoHSjDUVLb8+a+5x9081UDqRqoKeyljmrXyyXpczFaY91WBlvl/22Vdn3tB5/2w4f/bN1SsHD9ah1t9s3VbNfWR5dc/qp+pxJlg65+HHhtsKlALAqRMoBQAAAAAAgH4CpcBZ9+Wr5tThzFS23PPW0eqmZcs7200UJVAaj2/eclxF1akYKE1F0ny/F/cfqHYePlwt3rCps914OlcCpR/82SXVqhdfqueSo/eb9z5/+bX1miZs+qUZ19fXBEoB4NQJlAIAAAAAAEA/gVLgrEuA9MDbx6orFj5UhxI37XrtuCPTmyHOthKsu/bBR+rf+bP5bDvgWdr9atWa6oW9+6pDjWfy3oVrn672HXurbpMj3FMtshkazVj2HD1ajzXH9E+/5/7he9EXKM3R5fX73nu3rjq5bvuO6isz59T3uuaXfuavWjs0lmPVT2+fP9zPt+fcVh+LvvSZ54avRaq8Niu8fujn0+vnM4f0l+qXCcDmaPXyTAmOZlx5X75Dfn/16huOC5Smv1TPzNr84lcLh/uIaXcvrJ+9+dEV1YoXttYB4S9eMXtEmwQ4M741Q3PNmpZxZS3ba3zeTy6uj5/fcehQ/X3a6xXNYGWefXLby3Ug+Yc3jzwiPuuSMWfszfWNrHvadO2d9Hn5vQ/WYc4yho2v7qq+ed0tw22i+V1j6779xwVDR5PxZtxZt3Y4OfIN3xjq98qhfx/5LVAKAKdOoBQAAAAAAAD6CZQCZ1WqMz71yvbh4OItjz5Rh/8uuPWuEe0uuuve6oaHH/u9pY/Xwb2E/GY/tKxu0xUKjHbAs7TLsy+9/nodNPzZL+cPBybT9qENm+r3rN72ch0SvGvlk8P9lQqXv/jVfXU1zlSQTCXJvvfFhfMW1PPaPDTPzHHeE6uH+nhzuPLkZ6ZfU333htvrOUX+nvXIcwlq5pnSV+abgGMz7PmJi2fWocand+ys1zRWbnmxHnuqX2YumVPGtXn3nurT06+unyuB0rTLvYQ2E6j9syuvGxEoTdBx4dC8EkptHr9epCJpCbNecs+iuq+Zi5aMaFMCpbmXwGnW4I4Vq+o1aK5XeVdClGXsC9asr9s8NzT2Mud2sDLvyzM5er+887OXzqq2HThQJaScOWUM67fvrMOsCelm3dOua+9kntkjeTaB1/LNEnLNEfVpkzBpwqDZR/lGkb/nuRn3LR7uazTl3XOWPNp5PyHnjLuErAVKAeDUCZQCAAAAAABAP4FS4Kwq1RlThTG/v3X9rXXY70RHpidUmdBeQpMJT+ZaVygw+gKlCY8mRFraJdCXwGTCg+Va7ieA2Ky4WQKlCfmlbUKMi9ZtGK4u2X5fCW0m8FmCnJHKpqlwWgKQpV0JceZa3pl3J1iZ33lHKlmm/71vvTX8jrKOpa+6YuhQ381xRQlJlvUu70zYNeHV0q45llQ0TcAzodP82ewvyhhLhc3Mcev+/XVQuHybKIHSZiAzEkDNui9Ys264v7x3+fNbRrwrY067tM/vdrDyy1fNqcdRQrW5VtahGTJtfr9yrb13yhya3yISdM5alXdmzAn8NtfuezfeXlddLd/sRNJX177tI1AKAKdOoBQAAAAAAAD6CZQCZ1VCgs1jyhMgzNHlCfM1w5dNCSMmlNiuDNoOBRbtgGdXu/LeZnC0SBix+XwzkFgCp7mfKqq5335fCQA2Q41Rgpjl2PWuQGkZV6psptpmeSZBxvxZ+kzANkfjl3fmOPzmuhbNip2peNn1zmhev/WxJ+ogZ8KpOYq+2V+kMmjulyPZI2uQYHACwuVaCZSW+RZfnzW3bnuigGT7u5V1bT6X4+13Hz1SfXvObfXv3GtXvB0kUFpCru1vlqDqZQseGP7WNy1bXgd0U720a20GkTGe6UDpJ+9aV31hye7RLX6t+uzCLdUn7niy+tPrH6zOu2hWZ18AMJkIlAIAAAAAAEA/gVLgrOmrZNkVUCwS4EzAshngLNqhwCLBu2bAs6tdCVDmepfm8+1AYjn2vBzH3ve+PqMFSiPHqCcsmiPaE3TMu/POrEOpxpkAaQmd5pmu0GSReyU42/fOcn33kSN1QLNrvSPvzvdrB4BLJc9mILMvUFquNwOSCbvOX722vp7j/ZvrVb5bV7AyeyYVY7Nm6SPB2Wdf2z18XHx0rU17T7R/9/nYtBn1d8gYs2fzPfJMs/LtiWT8g7yrGI9A6ff2/us6THOyfnDgb6q/t/YfVh+ecWdnvwAw0QmUAgAAAAAAQD+BUuCsKceRl6BgWzlCvflMqmX2Hb3eFwLsC3g225UAZSqf5h03PPzYCNctXlZ97rL3q6F2BRLvWvlkPa4Fa9b3vu+RTZuP6zdKULMv3FmOs09IcvGGTcMByVQlTeDzL2+7u9q8e09dnbM80xcozZqtfXn7wIHSBCXzzgQz2xVhIxVIU120fLO2Zphz0EBpwpgJqWYNU8E2FWnT5rbHV9Z9lu/WFawsFVjzfNYl61aO9y/GM1AaWdNvXndL/e3z7uyDnYcPVxcMvb+rfVt515wlj3bez/plrGUdxyNQOphp1XkXXl19+Mo7qo/etKz6wpI91d9//p9XF7z9uzqI842N/7j6g59f0fEcAExcAqUAAAAAAADQT6AUOCsSwktgdM/Ro9UdK1YdF7JM6LF9ZHq7Emizv+gLAQ4SKE2lzVSXTNDyy1fNGb7epSuQmPFkXBlfCUOW95Xj09vBxrYThTtztH7+LEHCrM2uN9+s7l27vg6WJmBanhmvI+9f2Luv+sTFM4fXfuWWF0dUk82cEgrOGNrfMAHa3EtwOG1LcPREgdJUYk1F1nZgsv3d+oKVmXv2zkMbNh133H0MEii9cN6C6sBQ3wnxljaRffuFK2ZXn5l+zfDfI38vbS6ef189rlUvvjTi2T4lMNwVoI6scaquloq9Zy5Q2u2Pps2uvrh0f/Xjt//36juv/s/VH188MrQMABOZQCkAAAAAAAD0EygFzoqENhPezHHhXSG6hDATRrxp2fL6d45TTxXOBO8Sbmy3jxICbAbt0neCeicKlEaCrakumQBfc0xfnzW3DgmWa12BxMj7E2BM3833parnywcP1vP9yszfh1XT34z7Fg9XPu0Ld0ZCkgmlpt+sTa6VEGzCkxnPt+fcNty+VH9dtG7DiLnMefix6uC77wyHW08UKG1eTwXOjCF95HcCqalA2jXeyHhyXH4qnOb3oIHSEpi8f/3G4TaZQ+YySKC0nvvQ9cyzfdx9DBIoLd+s/fx3b7i9Du9mTrmeYG47hFxCu+159sl3TPg0cynVaouMI5VhXxkay5dmXF9fO9uB0uIjsxdVPzz076vv7PhX1R/87LLONgAw0QiUAgAAAAAAQD+BUuCsSFA04c1mVc2mEsorgb6EGXP8eoJ37UqYly14oA7lJXS6df/+OmyX9qkumaqhCRbm2okCpSW0mvY5Pj5937P6qToUuXnP3uHj3vsCpc3QY/N9UYKcCQYmuDr3keV1JdMENEtoNnPItYRSf7VqzYhwYapTpkpl1iRrU67PW/lk/b5ScbRcT1+pJpo1LmuWip0ZV+aYuabdyQRK82fWM8e554j3ElrtCzaWwGsJXA4aKC3fMWuT75D1yvrn+w8SKC3Pp21XVdiEQfMtHli/cXjvdO2J8s2ytvle855YXc99x6FD9TH8aZP9mzYJfWa/pd3GV3fV616+6yDSX/otcy57L+tSh3gbx+GXeaeKb/k30NQOpZ5OH7tleR3K+erq/6rzPgBMNAKlAAAAAAAA0E+gFDjjPvTz6fXx7QnLfe2aGzvbRKpyliPLE0JM4K9LM/T4/bl3VFv37a/Dhwn6rdu+ow5dDhIojY9Nm1Et3bR5uNJo/szR7Z+6ZOZwm75AaZSKku1AaaTKaT22996t5e/Tf33/iAqiP/vl/DqAmXcnEFquZ53yzoQNy7Uox6V3hTqzzglUpoJp+ksw8fHNW0bMpSs4Otr1jC9rkpDlY5tf6JxnU0KWCcLOXLRk4EBppJJr9kjWqXzH6xYvqyuPlsqlfYHSSJuyd9r3UnE2R/lnj2QeH73oys49ke9y+b0P1iHg5n76xqybRvQ34rsOtUswNGHU5ncdRL5L9lrZe/lez7y667iKvGXeadOlvb6n29fW/aPqgrd/V/3xL96voAoAE5lAKQAAAAAAAPQTKAXgnFIqvXYdd3+mJezaFfosznT483T40GW31IHSLy8/0nkfACYSgVIAAAAAAADoJ1AKwDnluzfcXu0+cqTzuPszLRVku46lL87k8fSn059v+qfVDw78Tec9AJhIBEoBAAAAAACgn0ApAOeEr11zY3XHilX1EfXb33ijPtq+qx3jL6GchHP+6KJrO+8DwEQhUAoAAAAAAAD9BEoBOCfMXLSkOvTeu3WY9MJ5CzrbcHr86ZyH6nDOR2Yv6rwPABOFQCkAAAAAAAD0EygFAMbkjy+5sQ7nfPzWFZ33AWCiECgFAAAAAACAfgKlAMCYnHfRrDqc8+n5z3TeB4CJQqAUAAAAAAAA+gmUAgBj8oc/v7IO53z2vm2d9wFgohAoBQAAAAAAgH4CpQDAmPzBz6+owzmfW/RS530AmCgESgEAAAAAAKCfQCkAMCYCpQBMFgKlAAAAAAAA0E+gFAAYE4FSACYLgVIAAAAAAADoJ1AKAIyJQCkAk4VAKQAAAAAAAPQTKAUAxkSgFIDJQqAUAAAAAAAA+gmUAgBjIlAKwGQhUAoAAAAAAAD9BEoBgDERKAVgshAoBQAAAAAAgH4CpQDAmAiUAjBZCJQCAAAAAABAP4FSAGBMBEoBmCwESgEAAAAAAKCfQCkAMCYCpQBMFgKlAAAAAAAA0E+gFAAYE4FSACYLgVIAAAAAAADoJ1AKAIyJQCkAk4VAKQAAAAAAAPQTKAUAxkSgFIDJQqAUAAAAAAAA+gmUAgBjIlAKwGQhUAoAAAAAAAD9BEoBgDERKAVgshAoBQAAAAAAgH4CpQDAmAiUAjBZCJQCAAAAAABAP4FSAGBMBEoBmCwESgEAAAAAAKCfQCkAMCYCpQBMFgKlAAAAAAAA0E+gFAAYE4FSACYLgVIAAAAAAADoJ1AKAIyJQCkAk4VAKQAAAAAAAPQTKAUAxkSgFIDJQqAUAAAAAAAA+gmUAgBjIlAKwGQhUAoAAAAAAAD9BEoBgDERKAVgshAoBQAAAAAAgH4CpQDAmAiUAjBZCJQCAAAAAABAP4FSAGBMBEoBmCwESgEAAAAAAKCfQCkAMCYCpQBMFgKlAAAAAAAA0E+gFAAYE4FSACYLgVIAAAAAAADoJ1AKAIyJQCkAk4VAKQAAAAAAAPQTKAUAxkSgFIDJQqAUAAAAAAAA+gmUAgBjIlAKwGQhUAoAAAAAAAD9BEoBgDERKAVgshAoBQAAAAAAgH4CpQDAmAiUAjBZCJQCAAAAAABAP4FSAGBMBEoBmCwESgEAAAAAAKCfQCkAnAXfn3tHtXX//mr+6rWd9ycTgVIAJguBUgAAAAAAAOgnUAowhX3o59OrDTtfrXYfPVJ9e85tnW2+fNWcavsbb9Thx09Pv7r6qzt/Xe1/+1h14J23q2l3L+x8Jj7+i6uq5/fsrQ7/9r3qvqeeHnHvvJ9cXN249PHqlYMHq0PvvVsdGmqz683D1c3LVtT3mm3PVT+6ZV697g88/Uzn/cnkbAZKy35s77HRfOv6W6vXjrxZPfva7uojF17e2abPN2bdVK168aXq9aH9n72979hb1SObNlefumRmZ3sAJhaBUgAAAAAAAOgnUAowxd20bHkd6Jy38snO+zMXLaneeO/d4cBeCfAlTLfiha3VBy6YdtwzkbBpQqftQGlCrCu3vFgHSTfv2VvdsWJVdcujT1TP7d5Tj+M3W7fVbZp9TXbXPvhIvQ75s+v+ZDfZAqXZ6/keeS7Pd7XpcuG8BXWA9MDQc9nDtz/+m2rNS6/U4dIdhw5V58++ufM5ACYOgVIAAAAAAADoJ1AKMMV97Zob6+qgm3a9dly1xoRFExpNiO6CW++qr5UAX0KmqVyaCqbNZ5rPleqjzbDfnCWP1s+mMmezGmmeWbLx2ergu+9UM+5bPHz9XCBQevqcbKA0ezyVSV/cf6DaefhwtXjDps52bZ+//Nr6mQRHv3vD7SPulaDpxld3nXTFUwDOLIFSAAAAAAAA6CdQCjDFlfDnnreOVj+8ed6IeyVs2jwavAT4cux3wp+pcNp8JsqR4gnY5c8S9ksfCa72BVETWk0wb/nzW4evpVrp/FVr634Sykw1yMc3bxlxxPhXZ95QjzPVIuc+sny47Z6jR+vfpYrqoO2KHEv/wt59dTA2c123fUf1lZkjx51xZDzlCPT0eecTq+uwbFmrXC/y/oyjLwj5/bl31GuU9yWM+8rBg9Xl9z44Ymx5Js/OenBpPaa67dAY12/fedz4zoTJFCitK+cOtb/50RX1vs9e/OIVszvbNs1+aFn9PbIXu+4nmJqKvOk/v8tee3JorzXbdY03e+XmZSuG92P+DSxYs364Uu/Hf3FV9fyevfVezHMZf36nsm9XdeErFz5Uh7b7xgowlQmUAgAAAAAAQD+BUgCGj7VvB9NKiC7BtXKtBOJufWxltXX//hFh0yLtE6674eHH6lBdCc+VkN3al7cfF96MBOu+NOP66jPTr6l/f/Bnlwwfj58Aa/p7aMOm+v2bd++pPj396rpd6TfXExCc98Tq+ij9hDFzLWM+mXZRqk7mWP7MJ213vflm3TZjTJu8P+PIswkAZnyrt71cj3fh0Jw//FeX1sHZ2x5fWQcF82eebYZNm8HCn/1yfv3OVM7M+/LevD+B0TlDfZd2eSbfK23LuiSEm3b5nXUrbc+EyRQoTfAzeyBh6UvuWVQ/m/3f1bYp67vv2LHqp7fP77yfvhIqLv+GBg2U5t9B9kqezTvyLZdu2lx/ywSVc78ESrOv8mxCxAvXPl1987pb6n3cri689JnnOgPiAAiUAgAAAAAAwGgESgGoPnvprGrbgQMjgmkJsj257eW6amIqjpa2JRC3YM26ugJi/t4MYub5hEzjO3Nuq0N1JTxXnm2H7PrU1STfebtatG7DiABqwpUJ3JUKjCW8l+PIz59983C7EvLLWE+mXbMiZAmtxvR77q/HU0KDqc5ahz2XPDrcJuNM5cu8J6HFXOs68r4dLEwI9KlXttdh0gQFS7sSWm1Wdc0zCfomiFjWJc8/vWNn/XxX9dfTabIESlOJNOuY75N1y9omFJ11P1EIN3s23zR7qOt+exyDBkrL7+Yez59LNj47HAot+zEB4gSdS19lr+0+eqT69tC/tVwrc2qHTAF4n0ApAAAAAAAA9BMoBaCWyo0JrOXY+fwux923w3bNQFw52j7PlvslBJrqmqnmmKqO7fDcoIHSvkqL7QBsX3jv67Pm1uM7Uciv3a6Ms12xtQQS83wJcOZ3+8j0rN11i5dVn7vs2vr3IIHSBAITDGwe919kLVORNEeZ53eeybPpo9nu/vUbRw09Fp+9d0v12fu2jZvPL361Duf8+O3/vfre7v/HuDj/+X/eOfa29jqOJpVIExwu6xh5rh2a7pJvPtratvf2oIHS/Nm1x8ux9dkzJVAa+XtXu+yR/C79D3Lc/QcvmVt9YfGu6gtLdo/q8w/sqD71q43Vx25ZXv2dmXdXH/jLM1sBF2A8CZQCAAAAAABAP4FSAGolCFqCaKX6Zvs48GYgLhUSU8W0VM8sFRNLQK8dnmuH7k5ktBBf7pUwZ194r1wv7x+0XQmA9snzo4X82gYJlLZ/N5V7JeCaNvmd6812ud63XsUfTZtdB2nG23d2/qvO62PxBz+9tHMOTaOtW1OpAJvqnc2qswlQJ0jdDg+3jbYXoz2Ovr3Wbpf77f3VdKJAaQk5J9ycOWYezWD4aD5517qhdf7dcet+Ij8+9p+qb730P1WfWfD80De6rLNvgIlKoBQAAAAAAAD6CZQCUGseVf+Ji2fW4bsSFG22awfiEjhN8DQB1LTNM+VI8b6Q3dqXt9f3m/3GeT+5uPrSjOurz0y/pv7dF+LLs+njdAdKH9m0ubrh4ceOc9Fd957RQGmOOT8wdG88AqXxR9Ouq/7w51eOmxJS/fyDOzvvn4o/uuj96q4nMtq6NZVqus2wZlP2/WhHxKdybKrtpupu1/1L7llUVz9dsGZd/btvr7XHm/sJgN75xOrOvZZqtyfaa6nkm7mlymmq9p5oLifrAz+ZXv3xJTdWfzLr3rpS6VdX/ZfV9/f9v+pv/hdv/Lvqozc/2vkcwEQkUAoAAAAAAAD9BEoBGJbqpAm3zX5oWR1Q6zp+vR2IS7XHVH1MhcRbH1tZBx9T7bSrbUJuCbx1BVWjVIss7x3rkfftoOig7Uo4cLRjw0uoNePIeJr3MqbM70M/n17/HiRQOh5H3ud65pH5NK+fbn/w8yvqcM7nFr3Uef90aq9jn3zLVOC9d+3640KbCQ7nXtm3XfJv4tDQN+zbE4s3bBrRR99ea483e/xEFUVPFCjNfs27HxoaQ/bQaPt23Azt/4/OXVp9+5V/OfTtf3dWvj3AqRAoBQAAAAAAgH4CpQAMS3AzAc5tB14fEQxt6grwlbBenm1WR+xqO2fJo3U4ctG6DSOqlObvSzY+W1c7nXHf4vpaOYa/3XbOw4/V7UpwbtCg6KDtPn/5tdXLBw/WwdevzPx98DVjyNg+d9n71TPvWLGqHkeqtDbbZB7l2P9cGyRQWo5k33n4cPXN624ZbpfA7ubde0aEcPNMnk0fpV25nnlkPs3rp9tED5RmP2Zf9gUyS5g3odD2vSJ74sX9B6odhw5V373h9hH3UkE2odCNr+4a3vup8vvC3n3HvTN7Jfu/jPeyBQ/UeyhB4lToLe3y3a8aaptrJwqUllB3+sm/wUGOux8vqV76tfX/qP7+n7hjTWcbgIlEoBQAAAAAAAD6CZQCMKyEGhN+TEAtQbV2m64AX/M48VTTHK1tqnb+Zuu2utrjc7v31O0jfz/03rvVyi0vDlf2zHjyO9dXvfhSXU0yVRjTZ0KWZXzjHSiNElp95eDBOjg695Hl9dqkcmmO90+bEvbMeBaseb/yZcaZwODCob5KCLYEY1PF9brFy+pAatfa/OyX8+tgYkKl855YXa/L5j1763FkPKVdnsmzAqW/32OpFtusOlpcdNe9w+vfXOum7LN8m77KucU/GHpXApsJW2df5vtkL2dPJGh6/uybR7RPQDX7fPW2l+uxJDSaZ3OtjKX57yH7K/ss+y37LvvgO3NuO2GgNPKuQY7uPx0+8JcXV9/Z8a+qH7/1/6s+dPnIsC3ARCNQCgAAAAAAAP0ESgEYIWHJZuCtrSsImeDkk9teHlGVs69tpOrijUsfr0N4eVcCownQ5VqzSmMkcJdKpCWwmvDe45u3VJ+6ZOZwm9MRKI2L599Xbd23vx5f5O/Tf33/iGqpGUfGk3FlfBnnzctWjJhH5rB00+Y6GLr3rbeqn9z+q961+f7cO6oc5Z+2WZusy+X3PjjinXlGoPR9ZR2z9l3yrVMxtmu9mhIOTRC4WW22yzdm3VSHhsv3Lt/p6R2vDleuLT42bUa9N5rfMnujvde69njekXfl/iCB0lRJTVj1jBx33+FDV9xe/fjYf66++uR/3XkfYKIQKAUAAAAAAIB+AqUAwJiczUDp2Zag722Pr6xDoKlw+pe33d3Z7nRLEPxMH3ffljBpQqXnXTSr8z7ARCBQCgAAAAAAAP0ESgGAMZnKgdIiFUKfeXVX9d0bzvyR76lwun77zrNy3H3T35n5q3offPqeZzvvA0wEAqUAAAAAAADQT6AUABgTgdKzI+HRax98pHrqle11hdTZDy3rbHfGXDCt+tHR/1h95Tfvdd8HmAAESgEAAAAAAKCfQCkAMCYCpWfHl2ZcX+04dKg68Paxav6qtdV5P7m4s92ZdP6Wv67Of+GvO+8BTAQCpQAAAAAAANBPoBQAGBOBUoqvrvovq+/s/Fed9wAmAoFSAAAAAAAA6CdQCgCMiUApxZdXHKt+cODfdN4DmAgESgEAAAAAAKCfQCkAMCYCpRR/9ugb1Y+O/MfOewATgUApAAAAAAAA9BMoBQDGRKCU4s8ePVj96KhAKTBxCZQCAAAAAABAP4FSAGBMBEopBEqBiU6gFAAAAAAAAPoJlAIAYyJQSiFQCkx0AqUAAAAAAADQT6AUABgTgVIKgVJgohMoBQAAAAAAgH4CpQDAmAiUUgiUAhOdQCkAAAAAAAD0EygFAMZEoJRCoBSY6ARKAQAAAAAAoJ9AKQAwJgKlFAKlwEQnUAoAAAAAAAD9BEoBgDERKKUQKAUmOoFSAAAAAAAA6CdQCgCMiUAphUApMNEJlAIAAAAAAEA/gVIAYEwESikESoGJTqAUAAAAAAAA+gmUAgBjIlBKIVAKTHQCpQAAAAAAANBPoBQAGBOBUgqBUmCiEygFAAAAAACAfgKlAMCYCJRSCJQCE51AKQAAAAAAAPQTKAUAxkSglEKgFJjoBEoBAAAAAACgn0ApADAmAqUUAqXARCdQCgAAAAAAAP0ESgGAMREopRAoBSY6gVIAAAAAAADoJ1AKAIyJQCmFQCkw0QmUAgAAAAAAQD+BUgBgTARKKQRKgYlOoBQAAAAAAAD6CZQCAGMiUEohUApMdAKlAAAAAAAA0E+gFAAYE4FSCoFSYKITKAUAAAAAAIB+AqUAwJgIlFIIlAITnUApAAAAAAAA9BMoBQDGRKCUQqAUmOgESgEAAAAAAKCfQCkAMCYCpRQCpcBEJ1AKAAAAAAAA/QRKAYAxESilECgFJjqBUgAAAAAAAOgnUAoAjIlAKYVAKTDRCZQCAAAAAABAP4FSAGBMBEopBEqBiU6gFAAAAAAAAPoJlAIAYyJQSiFQCkx0AqUAAAAAAADQT6AUABgTgVIKgVJgohMoBQAAAAAAgH4CpQDAmAiUUgiUAhOdQCkAAAAAAAD0EygFAMbkD//BVXU457MLt3TeZ+oQKAUmOoFSAAAAAAAA6CdQCgCMyQen31SHcz45b13nfaYOgVJgohMoBQAAAAAAgH4CpQDAmPzJNffU4ZyP3vxo532mDoFSYKITKAUAAAAAAIB+AqUAwJh84o41dTjnQ5ff3nmfqUOgFJjoBEoBAAAAAACgn0ApADR84IJp1QPrN1Yv7N1bfX3W3M42jPSVle9WP3zzP3TeY2oRKAUmOoFSAAAAAAAA6CdQCjAG5/3k4urGpY9Xrxw8WB16793q0G/fq3a9ebi6edmK+l7XM1PNh34+vZq/am29Llmf2HHoUL1uE3GNEih9fPOWerznz765s83JePKlV+q+vjrzhs77k12COX9x8N9Wf/7sP+u8z9QiUApMdAKlAAAAAAAA0E+gFOAUJSi5csuLdZB085691R0rVlW3PPpE9dzuPXVo8jdbt9Vtup6dKjL/rEPWaP32nfX6DK/R0LUVL2ytPvizSzqfnWhONRh6rgdKPz1/Ux3M+dRd6zvvM7UIlAITnUApAAAAAAAA9BMoBThFc5Y8Wr3x3rvVA08/M6LSZipcLtn4bHXw3XeqGfctHvHMVDPt7oXVgXfertcj61KulzXKvbRpPjNRCZQe7w//wczqLw7+b9UPDvxN9YG/VJEXgVJg4hMoBQAAAAAAgH4CpQCn4CMXXl5t2vVatf2NN6ovXzXnuPsX3HpXte/YW9Xy57cOXytHv7925M3q8G/fq15/5+36aPVPXTJzuE1ChwkfrntlR7Vgzfq6j7TNM7MeXFp94uKZ1WObX6ifTRXUHLV/6a/vH37+2gcfqdvPW/lktW77jjrUWiqofvO6W6rvz72jemHvvvpa7qXNV2aOHP/Hps2olm7aPPzu/Llw7dP1nNvvydH+zfFs3be/fke7Xf4s14oL5y2oj75PZddy7Ruzbhox7oy12V8kjHr5vQ/Wc88703bjq7vq+eX+x39xVfX80Hwjfy/PlbVNwLPZLu+476mnqwNvHxt+phkCzb3MoSn3Zz+0rH5/1rq8I65c+FAdNM63zu9BA6WD7I/I/Oc+srzac/Ro3S7t73xidT2PMrczYmgc39j4j6sL3v5d9XfnTO3gNL8nUApMdAKlAAAAAAAA0E+gFOAUlHDi2pe3j6i8WaRi6ZdmXF99Zvo19e8c616Ox1/14kvVDQ8/Vj20YVO1/+1j1ebde6pPT7+6blf6Tbut+/fXx8Pfs/qpavfRI3U1z91HjtTh0Ob1nYcOVX9+7U318yXAmZBleU9CrfmdAOKet47Wv5vX064cO58wZY6jT5hx6TPPjWjXPMK/vCd9JsyZgGOZTzPIWSqUPjPUJkHVXOtz/uyb64BpgqIJmWaOL73+ej3mH90yb7jdnKExZTwJ9Oa9855YPbRmb9bPpo8SFB00UJq1zrgTZC3B2WYINO3yZ47sT3jzp7fPr79rgsQJFGcczbBt1i1j/uHN7495kEDpoPsjyvxf3H9geB9kXHn2TAVKz7twZnX+lr+uAzmfvW9bZxumJoFSYKITKAUAAAAAAIB+AqUAp+Cv7vx1HfYbNMBXgpWL1m0YEUAt4cBSzbKEHlNtshkivGzBA3XIs309gcJUybz6/ofr3yXo2XxP/nxk0+bjrifE+PSOndXOw4eHq6zetGx5PZ4ENfO7PL/wqafr6zMXLamvlfc0Q6aRMOW+Y8fq0GXz2Ywx4dP712+sK4mWMRQZS4KUGUupNBrfveH2OkRbKr1m7gnatsOipSJsqomebKA0z6VaamkXXSHQ9rXMYcULW+tQ77fn3FZfK+Nrhky7+mobdH+U/hMm/fzl1w63K+s06H48FQngfGT2ouorT7xd/fDN/1D9+Nh/rj5555rOtkxdAqXARCdQCgAAAAAAAP0ESgFOwckGSttVK4vPXjqr2nbgwHAAsR16LMr7Esjsul6OlC9Bz/K7KEe6t4OTuV7CjiVgmqqbX7xi9oh2CUwmOFmCnX3vmbPk0Xo8GVe5loDk9F/fX4cgEyzNcwmXJiRZwqhfu+bGehyl/yJrkrUp4dBL7llUB2vbx8xn7AndXnTXvScdKG23i64QaNe1crx9gr35Xb5HCYD2Pdc26P7o2wdlLu190+eTd62rvrBk9wnsqb684q3q6+v/++pbL/2L6sfH/lMdwLngnd/VR93/ydX3dPbN1CZQCkx0AqUAAAAAAADQT6AU4BSUYN+gAb7RQoW5V0Kc7dBjUd6XAGjX9UECpWmX9u3rZVyjBSzLvRJs7HtPfne9p0iFzVsfe6I+1j7P5xj59F3mkWtdypj63tvUN4/22va1i67v1XUt3yzfLkHchFoTdE3F01RMHe25ttHa5F7ZH33zL3NJ2+b1Pt/b+6//Nhw6uh8e+nfVd1/7X6rzt/yP1VdWvlt9ev6m6oOX/r6CLLQJlAITnUApAAAAAAAA9BMoBTgFJZy49uXtxx3fHuf95OLqSzOurz4z/Zr6d19gMM+mj4keKP3ExTPr4/bHGigtsj4PPP1MXbF09kPLhueRtbjh4ceOk+qjCWz2vbepbx7ttR1tvl3fq+8bprroa0ferKuLZn2efW13vUYneq6pr017f/TNv8ylzA3OFoFSYDJIoPRzD2zvvAcAAAAAAABTmUApwCkoR7En6Pflq+Ycdz8VKlOpshzhPl5H3p/OQOl4HHmf3+U9H/6rS6tVL75UB1ETrm22i+acSv8rXtjaGdAtcmR/ju4vR8wXeeYLQ2NOgPdMB0pzDP+Bd96uHtqwqZ5D87j76HuuadD98dPb5w/tq+P3gUApE4VAKTDRnXfh1XWg9DMLnu+8DwAAAAAAAFOZQCnAKZqz5NHqjfferRat2zAiBJm/L9n4bHXw3XeqGfctrq9Nu3thHTpst53z8GN1uxJCbIceizMRKM3vm5Ytr8cz95Hlw20y3oVD7XJ95qIl9bW+9+R38z3pP2vU7K/I0fepUJpwaMKSG3a+WocqE5pstrvo7nurb173/jHrn7/82urlgwePqwL63Rtur3YfOVIt3rCpHm+qemZeX7vmxuE2JeRb1na8AqU5xn/r/v31+mT8zePuo++5pkH3R3nXi/sP1GtR2pX5t/cNnGkCpcBE96HLbq0DpZ+8c03nfQAAAAAAAJjKBEoBTtGHfj69+s3WbXUo8rnde+pgZOTvh957t1q55cW6Tdqm+md+53qqduYY91S0TPhy81D7BAXT7mwHShOszPhff+ftumpmxpmqpAk1Zq5lPn3vye/mezKvVNfMvNdv31mvT8Kl6SvvaM491UcT+EyVz3tWPzX87rS7f/3G+pj8tCshy/SbvuY9sbraefhwtePQoer82TfXbRKMzXcpbXK8fvrOOMranmygNGHVvPeBobGUI/ib97Ie7aBrpK+8+86hcZYj/IvrFi+rPnfZtQPvjyjzT6g065m1ypH7zbnB2SJQCkx0H537SB0o/ehNyzrvAwAAAAAAwFQmUAowBgk53rj08TrMmABjQn2vHDxYXysByCJhzFSaTPgv4cMEJR/fvKX61CUzh9uc7UBpfGzajGrpps11CDJ95c+Fa58eEZTse09+t9+T5257fGW9LlmfPJfQ6CND72jOPb4/9446BJrAZNYz69pey1TwvPzeB9/vb6hN2q7bvqP6xqybhttkrResWV8fj5/3Zc0T6Mzx+6caKP36rLn18yWo+tGLrhy+V47ibx93H+krY+jSXKtB9kdk/gnJ7jl6tG7XNTc4WwRKgYnu8w/tqgOlH5w+t/M+AAAAAAAATGUCpQAwRqmI2nXc/Zny5avm1FVaBUo52wRKT6++/+mA0eV/bEgV6FTKbv8PBEw933jmn1Q/OPBvOu8BAAAAAADAVCdQCgBjkMqiCSl1HXd/OuSY/NkPjTymd+aiJdUb771bzVv55IjrcKadzUBpQpapWNz+91H0VWqeTARKT03+27zx1V11JedPT7+6sw1Tw3kXzRr6b9T/d+i/VW903gcAAAAAAICpTqAUAE5BAko54v+pV7bXx9P3hdjGU467X7xhU/2+5c9vrW54+LFqwZp11b5jb1Xb33ijPpa/6zk4U852oPTwb9/r/bdwqoHStM9zeb7r/pkkUHpiE+l7MfH8vSf/m/q4+z+++IbO+wAAAAAAADDVCZQCwCn40ozrqx2HDlUH3j5WzV+1tjrvJxd3thtvec+NSx+v333ot+/V4dJVL75UfWXmnM72cCZNhEBpPL55Sx3Abt4XKJ0aBErp89GbHq3DpN/Y8D903gcAAAAAAAAESgEAGCdnO1C65+jR6tnXdlcH3nm7mn7P/SPudwVKE9C+edmK6rUjb9ZB1FT7XbBmffWhn08fDm+WkGqU5xMi33fsWPXT2+cP9/XtObdVu48eqZY+89zwtUg14fTztWturH+n7zxf3plQeAKwn7pk5vAz5d3rXtlRLd20uT7KP/PrCpTmCPfNu/fUIfPv3nD78PW2VFVeuPbpeo5lrg9t2FRfb7a7eP591dZ9+6tD771byzHxP7pl3vD9j//iqur5PXtrsx5cWr1y8GAdbk+4vqzdybQrvjHrpmrd9h31XMt7vz/3jhFtRpvDaN+rOZb8vfT3sWkz6vVt9pf+m2uSStS5l33y2OYX6u+VeWSN2uNr6xpv3pf35n7fuNrfubTLmtz5xOoR+zV7Ke/Jmpb35P7cR5YPh6qb+6ndLt/mExfPHDG3fKtLfz3y30/25yOttWp/x7JWv1q1ph5r+sq6ZeypHPzFK2YPt82YN+16rb7+5atO//8Q8afX3V/98NC/r76/7/9Z/fEv/A8YAAAAAAAA0EegFACAcXG2A6UJzf3iV/dVOw8frl7cf6D6/OXXDt9vB0oTtls4dC0huoQ+b3j4seHwZgKef/TTS+pKxFctWlKHIJdsfLYOviVAd+G8BfW1Wx59Yrj/2Q8tqwN0zXBggnoJ1j29Y2f1wZ9dUlu55cU6MJnKwnlnApEZV0KhCYfmuRIATLuEVDO3O1asOi5omLH8Zuu2OtyXMZWxtJV2mVvm2JxrrpdQYOaQa1m7zC3vTLiw2X8JN2b+u48cqRasWVe33Tx07Y2h8c5Z8uhJtYvzZ99cB2Lzrrwz7V56/fVqz1tHh8OsJ5pDAop936uMpflt8udzQ2ue758QcPrLPmivSQlJJqy88dVddVCzfLNmf23ZX9lHXeNNPxlv17iiL1Ca/bBzaJ3mPbF6+NtkLdO2rF253tzrzf20df/+en3vWf1UvbcSvs73yXdpXs97/vzam+rnS2j5/RDpunouq7e9XPeXUGkZd1mrzDHfL+v4s1/Or0OvGeeVCx8ablsC2Fnzcu10OO+iWdUXluypK5N+d9f/tfrg9Lmd7QAAAAAAAID3CZQCADAuJkKgNOG5hP4SYFu0bsNwlcZ2oDR/5nezTf5MEDFBxh/e/H6QsbRrHqGeSouprJhQaHluxQtb63Z733pr+B3pI33NW/lk/Xva3QvrAF/znTHn4cfqEF6Cd/ldAoAJWSZsWdo1g4bNgGWeL226lPe2j4HP+8qaJDSYsGECsCXYGiXsmcqvzRBkO8RaAoJlTQZtl5Bt/p4Q8Devu2W4XaqtJuhYAoeDzCG/u75XV3DzpmXL67XLXint8k0SMs71mYuW1NdKSLIZMo2EUNtVapsSZs2c1r68fcT+er/K5/vPdY0r+gKl7TX63o2310HX9vVL7llUB2UT/szv0l/721624IG6Xft6gqUJR199/8PD7fL8zPvfX5PIXsie2HbgQPXZS2fV1/rW6oJb76r3QbN6b1n/Zsh0rM678Orqw1feWX107iN1iPTvb/4/1EHS+pj7Z/5J9Yd/9X5lWAAAAAAAAKCfQCkAAOPiWy/9i+oHB//tcIhrPHzx4b2d72prBkoTZlu/fWcdLLzornvr++1AaX43g6NFAm4JoyYcl99dAcUEA5/c9vJwmK4ETBPgy58lQJqKnwkPlncmUNf1zvSRvnIEeIJ67UBh0byeypAJAzaPNu/T997PXXZtdd3iZfVx/CWEWMbe1Hy+LwRZqrG2Q5Anapd3Z07tSpVZh6xHeX6QOeT3IIHShFhTNbZ9DHu0K2eWkGTZD0UqrDb3U1sJ6EaO8+9q07dGfYHSvnbN0GrzelmDvv1U1ur+9Rs7r7fn3Jb+0m/6z+++tSrfsvx7Kf9+Bj3u/ruv/d87/9twIn8x9N+ir637b6u/M/Puzn4BAAAAAACA4wmUAgAwLr6+/r+rg1znb/nrcZEKgx+eMTJA2KcdbstR6Qkg5ljzhPDagdK0T/itTwnFdQUUI1UcS6XJhDHz7rwzQblyxH1CkM0Kju0xNuVeCTj2BQDL9UjFx3Zlyj6jvbfoCwOWe+XI8r5wY7lexjxou7K+7fUvyvODzCG6vld7LO3fzefLvRLu7VuX/G7upy6X3/tgvQdT7TNVXhdv2DR8jHz0jaP9/Qdt174+aKC0vbfL9eacE4pNJdn2t0q/5Zv0rVUkqJzAcv6tlAB2qvqeKAwdn7xzTfWFJbtP6LMLt1afuGNN9afXP1j90bTrOvsCAAAAAAAARidQCgDAuJgoR96Xa3etfLI69N67dTXPrkBpQpl3PrG6uuHhx44zWsXLSKXMhAUTLE1QsBwJn6qkOar9L2+7u9q8e8+IyptdY4yE6lJlctBAaY4K/9WqNdUrBw/WlVibx4t36Xtv02hhwFTjPN2B0sy/6zvkuPWEcweZQ3R9r/ZY+sYWpYLqeARKI33k+PyMP/st+zFB48ypbxzt7z9ou/b18QqUpmrrrjffrF56/fV6LukvsvfSb/6edn1rVfpI5df8W8k+Srg0fbXbAQAAAAAAAGeXQCkAAONiogVKE75LhdIEP596ZfuIAGBCfQn4XXDrXcPtu/SF7krIL6G6/Fnuf+v6W+vw3b1r19fB0gRMyzN9x7af7JH35YjzGfctroN5Ccw227XlSPOE+RLqa15PEDVHjuedYz3yvlw/UQiy3a4EDU9UrXKQOeR31/dqj2U8jrzP7+Z+aivfsYyrXMu8S2XbvjVqf/9B27Wvj1egdMGadZ1zTX/pN/3nd99aRVnzrfv3Vyu3vDjwcfcAAAAAAADAmSVQCgDAuJhogdK4cN6COjiaoFszFJfKl6n0meDgeT+5eLj9p6dfXV21aMnwtb7QXSRomRBm7ieQmWslOPfakTfr8TQDkNPuXlgdGGq/aN2GEeHJOQ8/Vo9l/qq19e8TBQXL9bwr4bzML/Nstm0qcy39F7c+9kR1YGjsGVfmnbBfqnPm76XN+bNvro9rLxVY+8KN5XoZ26Dt0ueGna/WgdWELEu7uOjue4eP9B9kDvnd9b26xnLTsuV1f3MfWT7cLt9k4dBzuV6qZ/aFJPO7K2RZpApnqrq2x5uKtuW5Upk237RUxI2EnPNNT7SWJ9on4xUozf2scXOPZY9kr6Tf9J9rfWtVZM2zJlnfQY+7BwAAAAAAAM4sgVIAAMbFRAyUJrSWAGc7UJrKlr/Zuq06NHQ91UsTLLxjxar6GPmdhw9X3/nbIGgqjiYcunXf/rpNM/hXQoOpLpoqo+V6qnzmfaXiaLleAqA59nzViy/VR7o/tGFTPa4cj1+CnCcKCjavf33W3LraYzsI2lTmmiDf0k2b6/eWMGyulyPzU001bV7cf6AOPpb1aAZW+8KN5XoZ26DtooR+Uxn0ntVP1eNL0DfjS2XShHsHnUPX9+oaS/5M9do8n37KO9N/s7++kGR+N/dTW1f/qfSZeSa4W75VQpbZg9krGe8DTz9Tt8keOdFanmifjFegtASh00e+T75J/p51yZ/pP+361qoo3yb/Zhx3DwAAAAAAABOTQCkAAONiIgZK4/OXX1uHJNsBwIQGU0EyIbcE4RL+S9DzG7NuGm6TQOrdv1lTV2hMyK8ZhEtYMe8sx6MXORo+FTfbQb3oeufjm7dUn7pk5nCbEwUF29dLELRdbbUpwdaFa5+uw4p5b/7M72bgNS6ef18dxsxcI0HVH93y+yP6+8KN5XoZ26Dtiu/PvaNKqDLzSMAyVVFvXPr4iPkMMoeu79U3lo9Nm1GHU0frry8kmd+jBUqj3X/ZX1+Z+fuj3rMfFqxZX483bbIv7nxidb3uJ1rLE+2T8QqUxqW/vr8OLpd1ypgXb9g0onJp31oVpSKr4+4BAAAAAABg4hIoBQBgXJzNQCkwsaUqa6qzOu4eAAAAAAAAJi6BUgAAxoVAKdBn+j33V/uOHXPcPQAAAAAAAExgAqUAAIwLgVKg7Yc3z6vuWf1Utfvokeq53XtGHNsPAAAAAAAATCwCpQAAjAuBUqAtYdJD771bvbB3X/XN627pbAMAAAAAAABMDAKlAACMC4FSAAAAAAAAAJi8BEoBABgXAqUAAAAAAAAAMHkJlAIAMC4ESgEAAAAAAABg8hIoBQBgXAiUAgAAAAAAAMDkJVAKAMC4ECgFAAAAAAAAgMlLoBQAgHEhUAoAAAAAAAAAk5dAKQAA40KgFAAAAAAAAAAmL4FSAADGhUApAAAAAAAAAExeAqUAAIwLgVIAAAAAAAAAmLwESgEAGBcCpQAAAAAAAAAweQmUAgAwLgRKAQAAAAAAAGDyEigFAGBcCJQCAAAAAAAAwOQlUAoAwLgQKAUAAAAAAACAyUugFACAcSFQCgAAAAAAAACTl0ApAADjQqAUAAAAAAAAACYvgVIAAMaFQCkAAAAAAAAATF4CpQAAjAuBUgAAAAAAAACYvARKAQAYFwKlAAAAAAAAADB5CZQCADAuBEoBAAAAAAAAYPISKAUAYFwIlAIAAAAAAADA5CVQCgDAuBAoBQAAAAAAAIDJS6AUAIBxIVAKAAAAAAAAAJOXQCkT0gcumFY9sH5j9cLevdXXZ83tbDMW9z31dLX/7WPVX9356877Z9PHf3FV9fyevbX8vavNmTSR14pu1z74SHX4t+/Vf3bdH037e188/75q+xtvVNc8sPS4tuOl69/7mdp3Z2J+MJUIlAIAAAAAAADA5CVQyhn15Euv1EG3pkNDdr15uJq/am31oZ9Pr9slYPb45i319fNn33xcP2M10UKS0+5eWO05erT+U6B0fGS8GXd7v73+ztvVM6/uqr4/947O584F4xkonXHf4nrNblz6+HFtx0vXv/czte/OxPxgKhEoBQAAAAAAAIDJS6CUMyqB0n3H3qrufGJ1dcPDj9VuefSJ6rnde+pg6W+2bhsOlZ5OZzIkmTknKPfVmTd03o+lzzxXPfva7uojF14+pQOlg6zVoEqgdO3L24f3Wjzw9DP1HowL5y3ofPZUjCXEOd7GM1B6qsr6p7+u+ycy3vtuov27gnOVQCkAAAAAAAAATF4CpZxRfYHBhEgTJj3wztt1lc7mvdNhIoUkv3zVnPrI7VRozW+B0vENlHYFGhMkTaD0qVe2Vx/82SXH3T8VAqUjjbb+gxjvfSdQCmeGQCkAAAAAAAAATF4CpZxRowUGL7lnUX30dAmgdbU97ycXVzcvW1G9duTNOjCXUOCCNeuPq2r6o1vmVS/s3Vcdeu/d2tZ9+6uf/XL+8P0SVpv14NJq3fYd1cF336nbrd++s/rKzDkj+vrGrJuqVS++VI8t78y75z6yvD6mO/dLUC3vS78HhvrN78Ubn63bN2VOzb5j9kPLhub5ZvWt6289rr9Uci1zzZH4+Z01KM9mDNN/fX89vzLXPNc8zj3rl3Vc98qOaummzfVcyzjy/OX3Pli9cvBgXSE29za+uqv65nW3DD8/6FqNZSx5xyBrdTJGCzQmRPr0jp3D+6sEMH+1as37+2bo7yWMmb2VsG/5DtkHOZ79U5fMrO+X9zTH3ty3J3q+GORbRNZz067X3v8OQ+3SPs+V/RiDBkrz/fId863SX77v8ue3jghydvWVSroL1z5d//vLvfyZ7/mxaTOGv3GuF6W/0fZi/myuW9l3qWBcf5O/HePKrduqz1127fBY+uZans9703dzPFH2Rdfzg3yzMpc1Q33nvwfNf6fN/z7AVCNQCgAAAAAAAACTl0ApZ1Q7NNbUDgC22yagtXDoXsJdCb3l+PISSkvYqwS4SvXJl15/vQ6jRf6ea+WI87zjjffera8lLJq+0mf6yu9StfL82TdXOw4dqnYePlzNe2J1HRQrYb4EQdOmBEATeMv4E8pL2O7PrryuHnsCewmb/fT2+dVnpl9TP1PkPamS2ayU2ewvz92z+ql6Dptz7bfvVXetfHL4+TlD4856JHiYsWWMCaem4unXZ82t25TgW/rbffRIva53rFg1/Hzmkjk1n8+cM/e0GXStxjKWzPlEa3Wy2vuprbm/Sqgwc8peSbXcBJAzt5VbXqzHW+b+0IZNdb+bd++pPj396jp8mCqztz2+su4jf35pxvV18HeQ58t4BvkWGVO+Q9mPZV/kuTxf+uoLWTbl3RlDvtnSZ56r35l9mD2W8fUFSvPvLP/e8s78+2v+O8y3/9i0K+v5X7VoSR2uXrLx2Xp9sk6j7cX2v/d8t4yl+e+97Lu8J6HWrvEVeb7MI3vpuzfcXoedI3/Pnut6ftBvVuaS69nj+R6ZSwK+zfWDqUagFAAAAAAAAAAmL4FSzqh2aKzppmXL6wBZQnJdbUtAcNG6DcPh0fyZwNqet45WP7x5Xh0GSyhu56FD1Z9fe9Nw3+VY+Se3vVw/U8JqCaiWvvJsqlYmrJf2uZaAWJ5LwLHdV9rmmRIAbQZWm0ab8wW33lUH62YuWjJ8rfSXOaXSarmeIFsqNebdX7xidvV3/4srqrUvb69DmCUcF+krAdCyjiX41gwmlv627t9fv6v5fMaUuZQg5iBrNdaxFKOt1ckaLVBa5r7twIHqs5fOGg4VJkia4GNpN+3uhdWBd94eseeihD9TxbJc6wo2Dvr8IN9ieG8PrXmzammeTdAx+6Ls266xtGVvJzR562Pvf5vIGB/ZtHlEILLdV96RMeR7lzmV5/YdOzb8b6Vr/Uf7/n2B0maAOu/JWmZPXbnwofpa31zzfHMe5d9Ve43bzw/6zfrmUiotL1izbvgaTCUCpQAAAAAAAAAweQmUckZ1BQZTyXHGfYuHro+sZtkVMCvB0fJsJFiWgFkCYd+ec1sd0Ewlw2abuOiue6vLFjxQB/PaYbPi/vUbjxtfWzuY1hdUK0YLSSacVgKi5dpo/SWk1jXupnaQrwTfMo5muxJ8m9cI7EXWJ+uU9crvsazVoGMpRlurk9V+d/H5y6+tnvjbCpQL1qyvr/WFElO5s2vPJYSaMGqqiY5WKXPQ5wf5FqPt7QR2BwlZFglKJhCatf7aNTeOuNf+3u2+Svg1vjHr96Httq71H+37t799375LYDXB1dJv31zbz/f9u2o/P+g365tL/vuVKrvNeY/mk3etq76wZPfoFr9WfXbhluoTdzxZ/en1D1bnXTSrsy+YCARKAQAAAAAAAGDyEijljEr4KuGtLql6mCO9m22bAbPRno0EwvpChG19YbVcb74zUiX0mVd31YG/5vtKMG20AGi051GUYN7iDZtGXB+tv3b4LWHcG5c+Xh+znYBkc3xlDfqCb+2++gy6VmMZS9G3VkVZm66+28peaLYtMr5U1CzVSPvWYrTx5F4zDNzVx6DP972/abS9Xe6VQOqJ+httj7W/d1dfl9/7YB26TAXRVOjMHm5WBI6u8Y72/dtr1R5H0e6jb67t5/vm3H5+0G/WN5dyves7dfne3n9dXfBOddJ+cOBvqr+39h9WH55xZ2e/cLYIlAIAAAAAAADA5CVQyhmV8FWO8L7zidXVDQ8/Vrtu8bLqO3NuqwOJ7bbNYFfXs02ptNgVYuvSF1bL9eY7Uzky7VKV8Be/Wlhf/+4Nt1db9/3+ePLRwnnRnkeRvlPJMMeaN6+P1l8z/JYqkwnyJeiaY/+/d+Mv63dctWhJdaCxBrmW97eDb+0gXZ9B1mqsYyn61qooFTub371dSbIoeyGVOJvt8/xo69q83jeeUuHzVAOl7ef73t802t6+cN6Cep3PVKA0UqVz5tD3zRzz7zIh3VT3zDfK/a7xjvb922vVHkfR7qNvfO3n++bcfn7Qb9Y3l3K96zudumnVeRdeXX34yjuqj960rPrCkj3V33/+n1cXvP27Olz6jY3/uPqDn1/R8RyceQKlAAAAAAAAADB5CZRyRvWFtbq02yasluBaO4DZVI6bTtv2vc9Mv6b6whWz62BYX1gt15vvXPXiS/Xv5rHg7WBaX1Ct6JpzxrDiha3Vs6/tHj4yvRitv+a4v3zVnLqq65PbXq77K23aQb4ScGsH30oIMcelN6+nr6xT1iu/B1mrsY6lOJn9cSLtd4+mL5Q46PHnudbVx6DPD/ItTseR9/lm+XbNe+3v3e6rHPfe3Lf5e75djqLPkfS51rX+o33/9rdvj6No99s31/bzff+u2s8P+s365lKuN+d9uvzRtNnVF5fur3789v9efefV/7n644vH/u8GxkqgFAAAAAAAAAAmL4FSzqiTCQy226ay5MF336kDdc1qpjk6PpUwcy1Brw07X62rCDaDcp+//Nrqxf0Hqqde2V5XUOwLq+V6850ZQwKqCaqWNt+87pY6iFeCaaMFQKNrzt+6/taha29WNy1bPqJtlP4Snk3QsFzPPF/Yu68+Jj9/L8G1BAObIc65jyyvw4Ul0FbatYNvWZOXDx48LtSaCqy7jxwZPop/kLUa61iKrrU6Ve3g4Wj6QonT7l5YHXjn7WrRug0j5jXn4cfqvTh/1drha119DPr8IN8i+zb7N3sve7C0yV7YvHvPiD3fN5+m7L2MId+oXMsYE3Rufu92Xwmt5ps25x4JtTaf61r/0b5/+9vnuRypv3Doz7J2+TNrmTXN2uZaCeM239M1j75/p+35DfrN+uZSrg+y78bLR2Yvqn546N9X39nxr6o/+NllnW3gTBEoBQAAAAAAAIDJS6CUM+pkAoPtth/6+fTqN1u31SGzBOsShLtjxarqlYMH65Bdjs1PuwTMEsZ86fXX65BbJEzaDGgm7NUMmxW53nxnQnd5X/rKux7ZtLnuJwG0EkzrC6oVCQMmiPbA+o11KDbBwPSbQGmCpe32pb8cIZ53LVizbngOGcutj71fxbIEDNNuzdBa3f74b6r123fWvzO+EmjrC75FCcml6mLWc94Tq+u13HHoUHX+7JvrNoOs1XiMJbrWqqvdILoCjX3aocIi71+55cV6HqlWmyPzHxoaY/pNiDNhztK2BBGf3rGzum7xsupzl117Us8P8i1+9sv59Z7I9dzPvtg8tFfyXJ4vffXNp6kEUV8fGnOqcuad+Ybpq/m9231lfz7XeC5zyh7NuErYOe2ytxPG3rpvf913qvyO9v3b/97z3bLfM56ydvkza5m9lf8epF3el/dmzAvWrK/XJONrz6Ps0YzzV6vWVBfddW99vT2/Qb9Z31zK9UH23Xj62C3L6+Pvv7r6v+q8D2eKQCkAAAAAAAAATF4CpZxR7dDYaLraJkSWCoEJqiUEllBbQl/fmHXTiGd/dMu8uppnQmGRv+dauZ+wVzNs1rzefGcqFCYMt+fo0fp9eW/CoAmmlcqlJwqUpk09lqHnExbMEeapRJkKis0KiEXpL0HBhFjLXDOGjKX5zMemzage37ylDs+VeV6x8KH6mVIttC/4Frl/+b0P1qHcEt5bt33HiPUcdK3GOpZor9VHL7qys90gxiNQGl17LvP81CUzj2u3dNPmev5733qr+sntvzqp5wf5FvH9uXfUa1Ov81C7tM9zzX0x2nyavjJzznDwt7xv3sonR3zvrr7yrTPXEq4u/w7TX2mT8dz9mzV19dD0P3PRklG/f/vfe75bjtDPv7d6T/ztGLN2eX/z2axJgqvNdWvPIxLITSXXjDmh0Vzrmt8g36xvLuX6IPtuvH1t3T+qLnj7d9Uf/+L6zvtwJgiUAgAAAAAAAMDkJVAKZ9gFt95V7T56pD46vOs+wKn40GW31IHSLy8/0nkfzgSBUgAAAAAAAACYvARKAeAc8eeb/mn1gwN/03kPzgSBUgAAAAAAAACYvARKAeAc8fkHd1QXvFNVf3TRtZ334XQTKAUAAAAAAACAyUugFADOEX8656E6UPqR2Ys678PpJlAKAAAAAAAAAJOXQCkAnCP++JIb60Dpx29d0XkfTjeBUgAAAAAAAACYvARKAeAccd5Fs+pA6afnP9N5H043gVIAAAAAAAAAmLwESgHgHPGHP7+yDpR+9r5tnffhdBMoBQAAAAAAAIDJS6AUAM4Rf/DzK+pA6ecWvdR5H043gVIAAAAAAAAAmLwESgHgHCFQytkmUAoAAAAAAAAAk5dAKQCcIwRKOdu+9Njh6oeH/0PnPQAAAAAAAABgYhMoBYBzhEApZ9vfe/K/qb6/93/tvAcAAAAAAAAATGwCpQBwjhAo5Wz782f+afXtV/5l5z0AAAAAAAAAYGITKAWAc4RAKWfbd3f936qvr//vOu8BAAAAAAAAABObQCkAnCMmS6D0IxdeXq3c8mK1fvvO6uO/uKqzzZnw5EuvVLvePFx9deYN9e/vz72j2rp/fzV/9drj2p4uF8+/r9r+xhvVNQ8s7bw/mZx34cx6/332vhc77wMAAAAAAAAAE5tAKQCcI85WoPSyBQ9Ur7/zdrX0mec678f8VWurQ799r7pp2fI6ULrx1V3VC3v3VZ+efnVn+zOhHSj90S3zqt1Hj1QPPP3McW1Plxn3La7X7salj3fen0w+c++Wev99ZNa9nfcBAAAAAAAAgIlNoBQAzhFnK1CaUGgqe6bS5hevmH3c/QRIn31tdx3e/No1Nx53/2xpB0pPtzP9vjPqgmnVD/b/TfX9vf9r9YG/vLi7DQAAAAAAAAAwoQmUAsA54mweeX/fU09Xb7z3bnXlwoeOu3fBrXdV+469Va14YWv1gQumHXf/bBEoHT+ff2BHvfc+fc+mzvsAAAAAAAAAwMQnUAoA54izGSgtodHlz2897l4Jm85ctKT+/fFfXFU9v2dvLX/PtfN+cnF187IV1WtH3qwO//a9+hj4VS++VH1l5pwR/ex/+1j1V3f+evhadAU1vzHrpvr59JP+0u/cR5aPCLS2n0u/6T/vye9rH3ykfrZLe+w5sn7HoUP1sf4H332nWrd9x/DY01/7+by7+Y78md/FxfPvq7bu218dGlq3eGHvvvpI/nK/uYazHlxavXLwYP3uA0PjX7BmffWhn08f0d/p9CfX3FP9+Nh/rr659f80tL6/6GwDAAAAAAAAAEx8AqUAcI44m4HScqx9+9j7EnzcduBA9dlLZ4241gxl3rXyyTo4uXrby9UNDz9WhyIT7kxQ8kszrq/bDBooPX/2zXW4c+fhw9W8J1bXQdJNu16rg56zH1rW+1w7UJrj+TOWpseee6EOgK7c8mL1wZ9dUgdUFw61T2A2Adbm2J/bvaeeX+Qd67fvrIOtP719fvWZ6dfU7+gKlGaMGeuL+w9Utzz6RHXHilX1OiSwe+G8BXWbsoYJkO4+cmTonevqtpuHrmUsc5Y8Otzf6fTx21dWPzr6H+uj7s+7cGZnGwAAAAAAAABgchAoBYBzxNkMlEYCje1j70tIc/GG3x+F3g6UfvSiK6vNu/fUVTg/cfHvQ4m3L/9NXXXz6vsfrn8PGihNADPB1gQ3S5svXzWnvvb0jp11ELTruXagtO3rs+bWfUT+nmsJz2Yey5/fMqL66fxVa+vqqJfcs2j4Wvt90Q6Ufnr61dXW/fvrtcjfS7sSkk1oN+HdsobNkGl8e85t1e6jR+pwa7k23v74F3OqT/1qY/XNbf/ner9966X/qTrvovfDwgAAAAAAAADA5CVQCgDniD+8cGYd8Pvxsf9Unb/lr8fFnz36Rue7unzr+lvrCpwrXtg6HK5MOPPAO29X0+5eONyuHShNwPOpV7bXYctmCLRt0EBpl/Y7c+1kAqU5Qv43W7fVc5l+z/3H3W/rqjw6SKA0AdQEUeetfHK4TbH0meeqPW8drX5487zO+UQCuQmj5l3NZ7t88JK51RcW76q+sGT3qP7skQPVV5/8r6u//9w/q/7i9X9b77H4wYF/U4eXP/CT9wO6AAAAAAAAAMDkJlAKAOeI86ZdW31nx/9lOPA3Hn705v+n811dSjA0FTxTEbRU2yxVNUu7rjBkQpJ5LuHKhFJT8fNHt8wbUfXzZAKlefaZV3fV4cz0WZxqoDTH5qf66qJ1G0aMKTK3+avX1n2lomrzfScbKG3/bsq1UgG2L1Barg8SKP3kXeuGvvHvjvvmbT9+6z9V39///66+vf1fVt/Y+D9UX1j8WvXRmx6tPvCXgqQAAAAAAAAAcC4RKAWAc8TZPvI+Zi5aUoc482eqkqaiZ45/b7bpC0OmCuhFd99bh0kTKk04MwHV0mbQQGmqfKbdpl2vVb/41cL6+ndvuL3aum//iHcOGijNs7uPHKmP5W8eQx8Zc8aY5zLPHE2f/m57fOW4B0rnLHl0XAOlAAAAAAAAAABNAqUAcI6YCIHSVCZNpdEce794w6bhI9qbbdphyIQy81wzGHneTy6uHnj6mTpUevX9D9fXBg2Urnrxpfr31665cbhN+51dz3UFSlN9dOOru6p9x96qfvbL44/jzxH9+44dH0LtCoYOEigd65H35bpAKQAAAAAAAABwsgRKAeAcMRECpbH8+a118DFVRlO9M0fhN++3w5AJSKZ9QqjN4+RLuLKELVOhMwHT2Q8tG26TwGeO1G8GNROmzLu/PmvucLtvXndLtfPw4REBzBMFSjOWhUN/P/Teu9VdHQHPKM/cv37j8LU8l6PxTyVQmgqoW/fvr17Yu29ENdRUPt1x6FA918y5vYalXbkuUAoAAAAAAAAAnCyBUgA4R0yUQGk56j7hz5uWLT/ufjsMmcDpyi0v1sHNVBe94eHHqnlPrK4DoAmGfuv6W+vn8md+73rzzfr+HStWVS+9/np9DHwzqJl35t25lzaPbNpcVxhNcLMZwDxRoDQVSfPcywcPVjcvW1GPq7hu8bLqc5ddOxwATfA1Qdq8b/PQO/L+dqA0FVsPvvtO9cD6jdVlCx6o590OlEYCs2n34v4D1S2PPlH3+crQGDKWC+ctqNu017A8W64LlAIAAAAAAAAAJ0ugFADOERMlUFpClglrNo+dL7rCkDn2fv6qtXVgNAHLBCo37Xqt+v7cO0Y8e+mv76+P1E+bhDgf2/xCtWTjsyOCoakQOveR5dWeo0frdukzIdNUS21WLj1RoLSEPbukXdqn3VdmzqnWb99ZB2Iz7nXbd9SB0wOtyqV5byqPJmyauX30ois7A6Vx8fz7qq379td9Rp770S3zhu8LlAIAAAAAAAAA402gFADOERMlUAoAAAAAAAAAwOQjUAoA5wiBUgAAAAAAAAAATpVAKQCcIwRKAQAAAAAAAAA4VQKlAHCOECgFAAAAAAAAAOBUCZQCwDlCoBQAAAAAAAAAgFMlUAoA5wiBUgAAAAAAAAAATpVAKQCcIwRKAQAAAAAAAAA4VQKlAHCOECgFAAAAAAAAAOBUCZQCwDlCoBQAAAAAAAAAgFMlUAoA54g//AdX1YHSzy7c0nkfAAAAAAAAAAD6CJQCwDnig9NvqgOln5y3rvM+AAAAAAAAAAD0ESgFgHPEn1xzTx0o/ejNj3beBwAAAAAAAACAPgKlAHCO+MQda+pA6Ycuv73zPgAAAAAAAAAA9BEoBcbdBy6YVj2wfmP1wt691ddnze1sM5lNpvl9/BdXVc/v2VvL37vaMFK+6cZXd1WH3nu32n30SPW9GydPOPMrK9+tfvjmf+i8BwAAAAAAAAAAoxEoBQbyV3f+utr/9rHq8G/fG9WTL71SBy4f37yl2vXm4er82Td39ne6fOjn06v5q9bW7z40NJ7YcehQdePSx6vzfnJx5zMn62zO72QJlJ6cj1x4eR0mzV5/aMOm6ur7H66vdbWdaD7wk0uqvzj4b6s/f/afdd4HAAAAAAAAAIDRCJQCA/ncZddW1y1eVt3w8GO1+556unr9nbertS9vH74WF911b+fzZ0LCpL/Zuq2uLLl++87qlkefqD23e099bcULW6sP/uySzmfPVWMJlJYQcb511/2TNd79nQ4/vHleteeto9XSZ57rvD+RfXr+pvq4+0/dtb7zPgAAAAAAAAAAjEagFDglEzEcOO3uhdWBd96ulmx8tq4iWq7n77mWe2nTfOZcJ1B6cibDGLv84T+YWf3Fwf+t+sGBv6k+8JfjU4kXAAAAAAAAAICpRaAUOCUnCt7l6PscCf/VmTfUv9Mu7VMx9IW9++qKoQfffaeucJrqp5f++v7qlYMH6yPqU/n0sc0vVB+bNmNEn9+YdVO1bvuO+rk8n36+P/eO4fvXPvhIfex+/mw+FxfOW1AffX/HilXD13KU+cK1T1f7jr1VP/fakTeruY8sHw6jZuyZw7pXdlRLN22u35t55V57foP019Umf6bv9lxPVdZx+xtvDPe9eMOmauv+/ccFSrOWq158qV7r9ljLvHO9yLfLN8+z5/3k4urGpY/X65nvlXXJd/nKzDnD/TeN1l8JvOZbZo8cGLpexjrIe8o3v3nZinrPZD5pu3Xf/hF7I0bsn6E26XfOw48Nf5980+YYo7m/f3TLvBF7tz2WssfnPbG6/gbNNTuthsb/jY3/uLrg7d9Vf3fO4u42AAAAAAAAAABwAgKlwCk5lUBpQnwJDOY48YQXn3ple30t7XLM+EMbNo243uz7/Nk31wHAhE4TCk0w9aXXX6+fS9AvbUqF0mde3XXCgGY5Hj9zyHtzXP/qbS/XYcG7Vj5ZtylByFzbffRIPacSSG3Pb5D+Elx8fPOWOoyYEGnalKDqxqExJ2yadqcqodmESHcePlyHGjPWrFeCkc1AaVnL0i5rvmnXa/U4Zj+0rA5yfmnG9dVVi5bU3yvVXb981Zx6jpnDwqHv8sbQvBJIzRwWrFlfz/u53Xs6q6CO1l8JlGad0kdCmgnc/ul/ccVA7ymB0j1Hj9ZrmLlk/dOuOee8P2ux6803R8w5+6x8n89Mv2bEGPNty/NlbTcP9Zm9lz7SV/pM32lT9njWMe9e/vyW6pvX3VLfO13Ou3Bmdf6Wv66Puv/sfds62wAAAAAAAAAAwCAESoFTciqB0oQDE+QrbRIo3LDz1eOuf/bSWdW2Aweqzbv3VB+96Mrqgz+7pA4VJgDZDOh994bbq91HjlTLn99a/y5hx4T6EjC8f/3Gun2pQNk0Z8mjdTXL9njWb99ZV5f84hWzhwOlCV8mhNl8vj2/QfpLiDJzSFXWMqb8+cimzdW+Y8eqn94+f/jZk5UwatYy65F1Kde/MPTeBG+b4coETTOm5vsytlx7esfOer1zresbZx4lLNlc1/mr1tbzv+SeRcPX2rr6K4HShDUT2izXB31PCZQmzJv1Lu0SWm6u6dX3P1zvi3yn0qasTdlnuTbaGFOd9NPTrx6+Pv2e++sA87y/DaTmmbwje7Brz42XD/zkkuojsxdVX3ni7eqHb/6H6sfH/nP1yTvXdLYFAAAAAAAAAIBBCZQCp+RUAqVpn+fa7XLc+tdnzT3ueglBfu2aG+u+SnC0SIgyVSZLu1xLkG/6r++vXtx/oA73leqVCSKWwGHaPLnt5eGgZ7PPhAPLOEugNGNptonm/AbtL2HEHD8fOX692W6ssn5Zx4yjGWYsYcjmGnXpaneib9xUgp35s+t+dPU36PiK9nv63pvgaHO/leq1Dz/z3KiVYLvGWK6V4GiRb51vXvZH3x4fzSfvWld9YcnuE9hTfXnFW9XX1//31bde+hfVj4/9p7oi6QXv/K4+6v5Prr6ns28AAAAAAAAAADgZAqXAKekK3jU1A5f53Re2a7drXi8hw/KuBAe79IURE+C89bEnho99T7XQtCshxnY/RRnnoIHSQfvLc5ff+2B9TH/Crql8unjDpurPr+0Pl7bn3rWGzXbt71HG1l6jH90yr3rm1V11tc/mWJvt+vpMIHP+6rX1/EtotxjPQOkg7+kLlOZ3c60SJn7g6Wfqo/VzJH2Ors/e+Ni0GSOe6xpjeUefsj/69vhovrf3X/9tOHR0Pzz076rvvva/VOdv+R+rr6x8t/r0/E3VBy89vcfpAwAAAAAAAAAwtQiUAqekL2xYtIOifWG7drvm9RIyLO/KUfE3PPzYcS5b8MDwMe1dzvvJxe+HCX/7XjX7oWXDIcYEOhMqbPd33eJl1ecuu/akA6Un6q88m6DkzEVL6j5y1HtCjjmivWsOeS7P9/VV9H2PrsBmjotP21R3/cWvFtZzyDH5W/ftH9Guq88EM596ZXt9PVVfz599c/38bY+vrMOV7WBnU1d/XeOLQd9Twp7t9+Z3nm3vtxztnyP/EyhNsDTrP+O+xcP3u8ZY3vHIps0jvmtx0V331u369jgAAAAAAAAAAEwGAqXAKekK3jW1g6J9Ybt2u+b1EjL89pzbqt1Hj1QrXtg64jj3pg//1aXVqhdfql7Yu6/60ozrj7vfHG+Cm0/v2FkfV56AYbttkTENEigdtL8ESdO+eeR6/p6+9h07Vv309vkj2p+McuR91qB5vSuwmTYZ+9euuXHUdl3fOGPMWNvfvS/Y2dTVX9d7Y9D39L03v5v77TPTr6n3RcLFpc33bry92nn4cLV5957qoxddWV/rGmMCuKnkmmBrudYlzzTfCQAAAAAAAAAAk4lAKXBKuoJ3Te2gaF/Yrt2ueb2EDBO63LDz1fqo+Hbo8qK7762+ed37R3/nHW+8924195HlI9pEKoemQuktjz5R/06VylQGTUiwGVJNMPPi+ffV1wYNlOb3IP1dufChenztYGLGNNYgYkKtqeiZgGRZj0jANUHXZmAzY0/4NGMr7fJMnj1RoLRcu3/9xuFrmduidRvGNVA66HsGDZQuf35rXY30glvvGm5TgsAnmvPnL7+2evngwXodvzLz94HhjCfVTUvF2L49DgAAAAAAAAAAk4FAKXBKuoJ3Te3AZV/Yrt2ueb0Z9Ltw3oI6EJhKpfesfqo+ajwhwVSOTOgwlSc/Pf3qKse4J9i5fvvOOqiZcOlvtm6r26USZdqkv/yZ3zn2PP2kv/Sb/nMcekKEJxMoHaS/zOW5oTYZS464T5sFa9bV89q6f//w2E5VWaMEQ+c9sboOuSYImfVoruVNy5bX4dqXXn+9b1CtJwAAA5tJREFUbpOj3PNcgpnNdt+6/tY6eJqj8LOOqWiaMWasmUPmWY6PT38nCpR29Zd35Z3N98ag7xk0UPqzX6bi6e/XJmufSq1Zm8UbNg0/17ev5wy1z7d9ZWg9M5aMPwHejC/rmTYCpQAAAAAAAAAATGYCpcApOdOB0vj+3DvqwGiCfQkW7jh0qLpx6eMjjjFPNdPbHl9ZB/8SFkzYMKHOhCY/dcnM4XbxsWkzqqWNMGX+bLY7mUBpnKi/rjYJJCbY2Kx8ORaX/vr+upJmef+CNeurda/sGLGWqayZQOSeo0frdgl5JhSZgGSzcmna3f2bNdWBoe+WtZy5aEl9PWNNYDfX8i3Wbd9RXbd4Wd2uWVG0rau/vkBpDPKeQQOl0dw/Zd6pFvuhn08fbjPavk6l2YRhM57I36cPrXfmlfsCpQAAAAAAAAAATGYCpQAAAAAAAAAAAABTnEApAAAAAAAAAAAAwBQnUAoAAAAAAAAAAAAwxQmUAgAAAAAAAAAAAExxAqUAAAAAAAAAAAAAU5xAKQAAAAAAAAAAAMAUJ1AKAAAAAAAAAAAAMMUJlAIAAAAAAAAAAABMcQKlAAAAAAAAAAAAAFOcQCkAAAAAAAAAAADAFCdQCgAAAAAAAAAAADDFCZQCAAAAAAAAAAAATHECpQAAAAAAAAAAAABTnEApAAAAAAAAAAAAwBQnUAoAAAAAAAAAAAAwxQmUAgAAAAAAAAAAAExxAqUAAAAAAAAAAAAAU5xAKQAAAAAAAAAAAMAUJ1AKAAAAAAAAAAAAMMUJlAIAAAAAAAAAAABMcQKlAAAAAAAAAAAAAFOcQCkAAAAAAAAAAADAFCdQCgAAAAAAAAAAADDFCZQCAAAAAAAAAAAATHECpQAAAAAAAAAAAABTnEApAAAAAAAAAAAAwBQnUAoAAAAAAAAAAAAwxQmUAgAAAAAAAAAAAExxAqUAAAAAAAAAAAAAU5xAKQAAAAAAAAAAAMAUJ1AKAAAAAAAAAAAAMMUJlAIAAAAAAAAAAABMcQKlAAAAAAAAAAAAAFOcQCkAAAAAAAAAAADAFCdQCgAAAAAAAAAAADDFCZQCAAAAAAAAAAAATHECpQAAAAAAAAAAAABTnEApAAAAAAAAAAAAwBQnUAoAAAAAAAAAAAAwxQmUAgAAAAAAAAAAAExxAqUAAAAAAAAAAAAAU5xAKQAAAAAAAAAAAMAUJ1AKAAAAAAAAAAAAMKVdVP3/AfolM0Cz/5FJAAAAAElFTkSuQmCC" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### IP Explorer Mindmap\n", - "Below mindmap diagram shows hunting workflow depending upon the type of IP address provided\n", - "\n", - "![ipexplorer-mindmapv2.PNG](attachment:ipexplorer-mindmapv2.PNG)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "### Notebook initialization\n", - "The next cell:\n", - "- Checks for the correct Python version\n", - "- Checks versions and optionally installs required packages\n", - "- Imports the required packages into the notebook\n", - "- Sets a number of configuration options.\n", - "\n", - "This should complete without errors. If you encounter errors or warnings look at the following two notebooks:\n", - "- [TroubleShootingNotebooks](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/TroubleShootingNotebooks.ipynb)\n", - "- [ConfiguringNotebookEnvironment](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)\n", - "\n", - "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", - "- [Run TroubleShootingNotebooks](./TroubleShootingNotebooks.ipynb)\n", - "- [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)\n", - "\n", - "You may also need to do some additional configuration to successfully use functions such as Threat Intelligence service lookup and Geo IP lookup. \n", - "There are more details about this in the `ConfiguringNotebookEnvironment` notebook and in these documents:\n", - "- [msticpy configuration](https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html)\n", - "- [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:01:51.949751Z", - "start_time": "2020-05-15T23:01:51.909753Z" - } - }, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import os\n", - "import sys\n", - "import warnings\n", - "from IPython.display import display, HTML, Markdown\n", - "\n", - "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", - "\n", - "display(HTML(\"

Starting Notebook setup...

\"))\n", - "if Path(\"./utils/nb_check.py\").is_file():\n", - " from utils.nb_check import check_python_ver, check_mp_ver\n", - "\n", - " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", - " try:\n", - " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", - " except ImportError:\n", - " !pip install --upgrade msticpy\n", - " if \"msticpy\" in sys.modules:\n", - " importlib.reload(sys.modules[\"msticpy\"])\n", - " else:\n", - " import msticpy\n", - " check_mp_ver(REQ_MSTICPY_VER)\n", - " \n", - "from msticpy.nbtools import nbinit\n", - "extra_imports = [\n", - " \"msticpy.nbtools.entityschema, IpAddress\",\n", - " \"msticpy.nbtools.entityschema, GeoLocation\",\n", - " \"msticpy.sectools.ip_utils, create_ip_record\",\n", - " \"msticpy.sectools.ip_utils, get_ip_type\",\n", - " \"msticpy.sectools.ip_utils, get_whois_info\",\n", - "]\n", - "nbinit.init_notebook(\n", - " namespace=globals(),\n", - " extra_imports=extra_imports,\n", - ");\n", - "WIDGET_DEFAULTS = {\n", - " \"layout\": widgets.Layout(width=\"95%\"),\n", - " \"style\": {\"description_width\": \"initial\"},\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Get WorkspaceId and Authenticate to Log Analytics \n", - "<details>\n", - "  Details...\n", - "If you are using user/device authentication, run the following cell. \n", - "- Click the 'Copy code to clipboard and authenticate' button.\n", - "- This will pop up an Azure Active Directory authentication dialog (in a new tab or browser window). The device code will have been copied to the clipboard. \n", - "- Select the text box and paste (Ctrl-V/Cmd-V) the copied value. \n", - "- You should then be redirected to a user authentication page where you should authenticate with a user account that has permission to query your Log Analytics workspace.\n", - "\n", - "Use the following syntax if you are authenticating using an Azure Active Directory AppId and Secret:\n", - "```\n", - "%kql loganalytics://tenant(aad_tenant).workspace(WORKSPACE_ID).clientid(client_id).clientsecret(client_secret)\n", - "```\n", - "instead of\n", - "```\n", - "%kql loganalytics://code().workspace(WORKSPACE_ID)\n", - "```\n", - "\n", - "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", - "On successful authentication you should see a ```popup schema``` button.\n", - "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - "</details>" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:02:52.662562Z", - "start_time": "2020-05-15T23:02:52.653563Z" - } - }, - "outputs": [], - "source": [ - "#See if we have an Azure Sentinel Workspace defined in our config file, if not let the user specify Workspace and Tenant IDs\n", - "from msticpy.nbtools.wsconfig import WorkspaceConfig\n", - "ws_config = WorkspaceConfig()\n", - "try:\n", - " ws_id = ws_config['workspace_id']\n", - " ten_id = ws_config['tenant_id']\n", - " config = True\n", - " md(\"Workspace details collected from config file\")\n", - "except KeyError:\n", - " md(('Please go to your Log Analytics workspace, copy the workspace ID'\n", - " ' and/or tenant Id and paste here to enable connection to the workspace and querying of it..
'))\n", - " ws_id_wgt = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", - " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", - " ten_id_wgt = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", - " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", - " config = False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:03:22.552179Z", - "start_time": "2020-05-15T23:02:56.043852Z" - } - }, - "outputs": [], - "source": [ - "# Authentication\n", - "qry_prov = QueryProvider(data_environment=\"LogAnalytics\")\n", - "qry_prov.connect(connection_str=ws_config.code_connect_str)\n", - "table_index = qry_prov.schema_tables" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Enter the IP Address and query time window\n", - "\n", - "Type the IP address you want to search for and the time bounds over which search.\n", - "\n", - "You can specify the IP address value in the widget e.g. 192.168.1.1" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:03:22.632179Z", - "start_time": "2020-05-15T23:03:22.619179Z" - } - }, - "outputs": [], - "source": [ - "ipaddr_text = widgets.Text(\n", - " description=\"Enter the IP Address to search for:\", **WIDGET_DEFAULTS\n", - ")\n", - "display(ipaddr_text)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:03:56.698491Z", - "start_time": "2020-05-15T23:03:56.631491Z" - } - }, - "outputs": [], - "source": [ - "query_times = nbwidgets.QueryTime(units=\"day\", max_before=20, before=5, max_after=7)\n", - "query_times.display()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:04:05.784278Z", - "start_time": "2020-05-15T23:04:05.776278Z" - } - }, - "outputs": [], - "source": [ - "# Set up function to allow easy reference to common parameters for queries throughout the notebook\n", - "def ipaddr_query_params():\n", - " return {\n", - " \"start\": query_times.start,\n", - " \"end\": query_times.end,\n", - " \"ip_address\": ipaddr_text.value.strip()\n", - " }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Detemine IP Address Type" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:04:47.927548Z", - "start_time": "2020-05-15T23:04:43.963316Z" - } - }, - "outputs": [], - "source": [ - "ipaddr_type = get_ip_type(ipaddr_query_params()['ip_address'])\n", - "\n", - "md(f'Depending on the IP Address origin, different sections of this notebook are applicable', styles=[\"bold\", \"large\"])\n", - "md(f'Please follow either the Interal IP Address or External IP Address sections based on below Recommendation', styles=[\"bold\"])\n", - "\n", - "#Get details from Heartbeat table for the given IP Address and Time Parameters\n", - "heartbeat_df = qry_prov.Heartbeat.get_info_by_ipaddress(**ipaddr_query_params())\n", - "\n", - "# Set hostnames retrived from Heartbeat table if available\n", - "if not heartbeat_df.empty:\n", - " hostname = heartbeat_df[\"Computer\"][0]\n", - "else:\n", - " hostname = \"\"\n", - " \n", - "if not heartbeat_df.empty:\n", - " ipaddr_origin = \"Internal\"\n", - " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", - " display(Markdown('#### Recommendation - Go to section [InternalIP](#goto_internalIP)'))\n", - "elif ipaddr_type==\"Private\" and heartbeat_df.empty:\n", - " ipaddr_origin = \"Unknown\"\n", - " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", - " display(Markdown('#### Recommendation - Go to section [InternalIP](#goto_internalIP)'))\n", - "else:\n", - " ipaddr_origin = \"External\"\n", - " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", - " display(Markdown('#### Recommendation - Go to section [ExternalIP](#goto_externalIP)'))\n", - " \n", - "#Populate related IP addresses for the calculated hostname\n", - "az_net_df = pd.DataFrame()\n", - "if \"AzureNetworkAnalytics_CL\" in table_index:\n", - " aznet_query = f\"\"\"\n", - " AzureNetworkAnalytics_CL | where ResourceType == 'NetworkInterface' \n", - " | where SubType_s == \"Topology\" \n", - " | search \\'{ipaddr_text.value}\\' \n", - " | where TimeGenerated >= datetime({query_times.start}) \n", - " | where TimeGenerated <= datetime({query_times.end}) \n", - " | where VirtualMachine_s has '{hostname}' \n", - " | top 1 by TimeGenerated desc \n", - " | project PrivateIPAddresses = PrivateIPAddresses_s, PublicIPAddresses = PublicIPAddresses_s\"\"\"\n", - " az_net_df = qry_prov.exec_query(query=aznet_query)\n", - " \n", - "# Create IP Entity record using available dataframes or input ip address if nothing present\n", - "if az_net_df.empty and heartbeat_df.empty:\n", - " ip_entity = IpAddress()\n", - " ip_entity['Address'] = ipaddr_query_params()['ip_address']\n", - " ip_entity['Type'] = 'ipaddress'\n", - " ip_entity['OSType'] = 'Unknown'\n", - " md('No Heartbeat Data and Network topology data found')\n", - "elif not heartbeat_df.empty:\n", - " if az_net_df.empty:\n", - " ip_entity = create_ip_record(\n", - " heartbeat_df=heartbeat_df)\n", - " else:\n", - " ip_entity = create_ip_record(\n", - " heartbeat_df=heartbeat_df, az_net_df=az_net_df)\n", - "#Display IP Entity\n", - "md(\"Displaying IP Entity\", styles=[\"green\",\"bold\"])\n", - "print(ip_entity)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## External IP" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### GeoIP Lookups for External IP Addresses" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-27T08:33:37.478812Z", - "start_time": "2020-04-27T08:33:37.470173Z" - } - }, - "outputs": [], - "source": [ - "# msticpy- geoip module to retrieving Geo Location for Public IP addresses\n", - "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", - "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", - " iplocation = GeoLiteLookup()\n", - "\n", - " loc_results, ext_ip_entity = iplocation.lookup_ip(ip_address=ipaddr_query_params()['ip_address'])\n", - " md(\n", - " 'Geo Location for the IP Address ::', styles=[\"bold\",\"green\"]\n", - " )\n", - " print(ext_ip_entity[0])\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Whois Registrars for External IP Addresses" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-27T08:33:39.572115Z", - "start_time": "2020-04-27T08:33:39.566009Z" - } - }, - "outputs": [], - "source": [ - "# ipwhois module to retrieve whois registrar for Public IP addresses\n", - "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", - "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", - " from ipwhois import IPWhois\n", - "\n", - " whois = IPWhois(ipaddr_query_params()['ip_address'])\n", - " whois_result = whois.lookup_whois()\n", - " if whois_result:\n", - " md(f'Whois Registrar Info ::', styles=[\"bold\",\"green\"])\n", - " display(whois_result)\n", - " else:\n", - " md(\n", - " f'No whois records available', styles=[\"bold\",\"orange\"]\n", - " )\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Opensource and Azure Sentinel ThreatIntel Lookups" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Configure your TI Provider settings\n", - "If you have not used threat intelligence lookups before you will need to supply API keys for the \n", - "TI Providers that you want to use. Please see the section on configuring [msticpyconfig.yaml](#msticpyconfig.yaml-configuration-File)\n", - "\n", - "Then reload provider settings:\n", - "```\n", - "mylookup = TILookup()\n", - "mylookup.reload_provider_settings()\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-27T08:33:43.562087Z", - "start_time": "2020-04-27T08:33:43.554830Z" - }, - "scrolled": true - }, - "outputs": [], - "source": [ - "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", - "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", - " mylookup = TILookup()\n", - " mylookup.loaded_providers\n", - " resp = mylookup.lookup_ioc(observable=ipaddr_query_params()['ip_address'], ioc_type=\"ipv4\")\n", - " md(f'ThreatIntel Lookup for IP ::', styles=[\"bold\",\"green\"])\n", - " display(mylookup.result_to_df(resp).T)\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Passive DNS lookups for External IP Addresses" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-27T08:33:45.838706Z", - "start_time": "2020-04-27T08:33:45.829919Z" - } - }, - "outputs": [], - "source": [ - "# To force Passive DNS lookup for Internal public IP, change and with or in if\n", - "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", - " # retrieve passive dns from TI Providers\n", - " pdns = mylookup.lookup_ioc(\n", - " observable=ipaddr_query_params()['ip_address'],\n", - " ioc_type=\"ipv4\",\n", - " ioc_query_type=\"passivedns\",\n", - " providers=[\"XForce\"],\n", - " )\n", - " pdns_df = mylookup.result_to_df(pdns)\n", - " if not pdns_df.empty and pdns_df[\"RawResult\"][0] and \"RDNS\" in pdns_df[\"RawResult\"][0]:\n", - " pdnsdomains = pdns_df[\"RawResult\"][0][\"RDNS\"]\n", - " md(\n", - " 'Passive DNS domains for IP: {pdnsdomains}',styles=[\"bold\",\"green\"]\n", - " )\n", - " display(mylookup.result_to_df(pdns).T)\n", - " else:\n", - " md(\n", - " 'No passive domains found from the providers', styles=[\"bold\",\"orange\"]\n", - " )\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Internal IP Address" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Data Sources available to query related to IP" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:04:59.773853Z", - "start_time": "2020-05-15T23:04:53.039482Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin in [\"Internal\",\"Unknown\"]:\n", - " # KQL query for full text search of IP address and display all datatypes populated for the time period\n", - " datasource_status = \"\"\"\n", - " search \\'{ip_address}\\' or \\'{hostname}\\'\n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | summarize RowCount=count() by Table=$table\n", - " \"\"\".format(\n", - " **ipaddr_query_params(), hostname=hostname\n", - " )\n", - " %kql -query datasource_status\n", - " datasource_status_df = _kql_raw_result_.to_dataframe()\n", - "\n", - " # Display result as transposed matrix of datatypes availabel to query for the query period\n", - " if not datasource_status_df.empty:\n", - " available_datasets = datasource_status_df['Table'].values\n", - " md(\"Datasources available to query for IP ::\", styles=[\"green\",\"bold\"])\n", - " display(datasource_status_df)\n", - " else:\n", - " md_warn(\"No datasources contain given IP address for the query period\")\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address type is: {ipaddr_type}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Check if IP is assigned to multiple hostnames" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:03.895367Z", - "start_time": "2020-05-15T23:05:02.486243Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\" or not datasource_status_df.empty:\n", - " # Get single event - try process creation\n", - " if ip_entity['OSType'] =='Windows':\n", - " if \"SecurityEvent\" not in available_datasets:\n", - " raise ValueError(\"No Windows event log data available in the workspace\")\n", - " host_name = None\n", - " matching_hosts_df = qry_prov.WindowsSecurity.list_host_processes(\n", - " query_times, host_name=hostname, add_query_items=\"| distinct Computer\"\n", - " )\n", - " elif ip_entity['OSType'] =='Linux':\n", - " if \"Syslog\" not in available_datasets:\n", - " raise ValueError(\"No Linux syslog data available in the workspace\")\n", - " else:\n", - " linux_syslog_query = f\"\"\" Syslog | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end}) | where HostIP == '{ipaddr_text.value}' | distinct Computer \"\"\"\n", - " matching_hosts_df = qry_prov.exec_query(query=linux_syslog_query)\n", - "\n", - " if len(matching_hosts_df) > 1:\n", - " print(f\"Multiple matches for '{hostname}'. Please select a host from the list.\")\n", - " choose_host = nbwidgets.SelectString(\n", - " item_list=list(matching_hosts_df[\"Computer\"].values),\n", - " description=\"Select the host.\",\n", - " auto_display=True,\n", - " )\n", - " elif not matching_hosts_df.empty:\n", - " host_name = matching_hosts_df[\"Computer\"].iloc[0]\n", - " print(f\"Unique host found for IP: {hostname}\")\n", - "elif datasource_status_df.empty:\n", - " md_warn(\"No datasources contain given IP address for the query period\")\n", - "else: \n", - " md(f'Analysis section Not Applicable since IP address type is : {ipaddr_type}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### System Info" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:07.346683Z", - "start_time": "2020-05-15T23:05:07.330684Z" - } - }, - "outputs": [], - "source": [ - "# Retrieving System info from internal table if IP address is not Public\n", - "if ipaddr_origin == \"Internal\" and not heartbeat_df.empty:\n", - " md(\n", - " 'System Info retrieved from Heartbeat table ::', styles=[\"green\",\"bold\"]\n", - " )\n", - " display(heartbeat_df.T)\n", - "else:\n", - " md_warn(\n", - " 'No records available in HeartBeat table'\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### ServiceMap - Get List of Services for Host" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:10.389939Z", - "start_time": "2020-05-15T23:05:10.369939Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\":\n", - " if \"ServiceMapProcess_CL\" not in available_datasets:\n", - " md_warn(\"ServiceMap data is not enabled\")\n", - " md(\n", - " f\"Enable ServiceMap Solution from Azure marketplce:
\"\n", - " +\"https://docs.microsoft.com/en-us/azure/azure-monitor/insights/service-map#enable-service-map\",\n", - " styles=[\"bold\"]\n", - " )\n", - "\n", - " else:\n", - " servicemap_proc_query = \"\"\"\n", - " ServiceMapProcess_CL\n", - " | where Computer == \\'{hostname}\\'\n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | project Computer, Services_s, DisplayName_s, ExecutableName_s , ExecutablePath_s \n", - " \"\"\".format(\n", - " hostname=hostname, **ipaddr_query_params()\n", - " )\n", - "\n", - " %kql -query servicemap_proc_query\n", - " servicemap_proc_df = _kql_raw_result_.to_dataframe()\n", - " display(servicemap_proc_df)\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address type is {ipaddr_type}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Related Alerts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:14.185177Z", - "start_time": "2020-05-15T23:05:14.123178Z" - } - }, - "outputs": [], - "source": [ - "ra_query_times = nbwidgets.QueryTime(\n", - " units=\"day\",\n", - " origin_time=query_times.origin_time,\n", - " max_before=28,\n", - " max_after=5,\n", - " before=5,\n", - " auto_display=True,\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualization - Timeline of Related Alerts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:19.536611Z", - "start_time": "2020-05-15T23:05:17.943028Z" - } - }, - "outputs": [], - "source": [ - "#Provide hostname if present to the query\n", - "if hostname:\n", - " md(f\"Searching for alerts related to {hostname}...\")\n", - " related_alerts = qry_prov.SecurityAlert.list_related_alerts(\n", - " ra_query_times, host_name=hostname\n", - " )\n", - "else:\n", - " md(f\"Searching for alerts related to ip address(es) {ipaddr_query_params()['ip_address']}\")\n", - " related_alerts = qry_prov.SecurityAlert.list_alerts_for_ip(\n", - " ra_query_times, source_ip_list=ipaddr_query_params()['ip_address']\n", - " )\n", - "\n", - "\n", - "def print_related_alerts(alertDict, entityType, entityName):\n", - " if len(alertDict) > 0:\n", - " md(\n", - " f\"Found {len(alertDict)} different alert types related to this {entityType} (`{entityName}`)\",styles=[\"bold\",\"orange\"]\n", - " )\n", - " for (k, v) in alertDict.items():\n", - " print(f\"- {k}, # Alerts: {v}\")\n", - " else:\n", - " print(f\"No alerts for {entityType} entity `{entityName}`\")\n", - "\n", - "\n", - "if isinstance(related_alerts, pd.DataFrame) and not related_alerts.empty:\n", - " host_alert_items = (\n", - " related_alerts[[\"AlertName\", \"TimeGenerated\"]]\n", - " .groupby(\"AlertName\")\n", - " .TimeGenerated.agg(\"count\")\n", - " .to_dict()\n", - " )\n", - " print_related_alerts(host_alert_items, \"host\", hostname)\n", - " nbdisplay.display_timeline(\n", - " data=related_alerts, title=\"Alerts\", source_columns=[\"AlertName\"], height=200\n", - " )\n", - "else:\n", - " md(\"No related alerts found.\",styles=[\"bold\",\"green\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " ### Browse List of Related Alerts\n", - " Select an Alert to view details" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:24.530275Z", - "start_time": "2020-05-15T23:05:24.464277Z" - } - }, - "outputs": [], - "source": [ - "def disp_full_alert(alert):\n", - " global related_alert\n", - " related_alert = SecurityAlert(alert)\n", - " nbdisplay.display_alert(related_alert, show_entities=True)\n", - "\n", - "recenter_wgt = widgets.Checkbox(\n", - " value=True,\n", - " description='Center subsequent query times round selected Alert?',\n", - " disabled=False,\n", - " **WIDGET_DEFAULTS\n", - ")\n", - "if related_alerts is not None and not related_alerts.empty:\n", - " related_alerts[\"CompromisedEntity\"] = related_alerts[\"Computer\"]\n", - " md(\"Click on alert to view details.\", styles=[\"bold\"])\n", - " display(recenter_wgt)\n", - " rel_alert_select = nbwidgets.AlertSelector(\n", - " alerts=related_alerts,\n", - " action=disp_full_alert,\n", - " )\n", - " rel_alert_select.display()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Related Hosts\n", - "**Hypothesis:** That an attacker has gained access to the host, compromized credentials for the accounts and laterally moving to the network gaining access to more hosts.\n", - "\n", - "This section provides related hosts of IP address which is being investigated. .If you wish to expand the scope of hunting then investigate each hosts in detail, it is recommended that to use the **Host Explorer Notebook (include link).**\n", - "\n", - "#### __NOTE - the following sections are only relevant for Internal IP Addresses.__" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Visualization - Networkx Graph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:30.863302Z", - "start_time": "2020-05-15T23:05:29.870080Z" - } - }, - "outputs": [], - "source": [ - "import networkx as nx\n", - "if ipaddr_origin == \"Internal\":\n", - " # Retrived relatd accounts from SecurityEvent table for Windows OS\n", - " if ip_entity['OSType'] =='Windows':\n", - " if \"SecurityEvent\" not in available_datasets:\n", - " raise ValueError(\"No Windows event log data available in the workspace\")\n", - " else:\n", - " related_hosts = \"\"\"\n", - " SecurityEvent\n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", - " | summarize count() by Computer, IpAddress\n", - " \"\"\".format(\n", - " **ipaddr_query_params(), hostname=hostname\n", - " )\n", - " %kql -query related_hosts\n", - " related_hosts_df = _kql_raw_result_.to_dataframe()\n", - "\n", - " elif ip_entity['OSType'] =='Linux':\n", - " if \"Syslog\" not in available_datasets:\n", - " raise ValueError(\"No Linux syslog data available in the workspace\")\n", - " else:\n", - " related_hosts_df = qry_prov.LinuxSyslog.list_logons_for_source_ip(invest_times, ip_address=ipaddr_query_params()['ip_address'],add_query_items='extend IpAddress = HostIP | summarize count() by Computer, IpAddress')\n", - "\n", - " # Displaying networkx - static graph. for interactive graph uncomment and run next block of code.\n", - " plt.figure(10, figsize=(22, 14))\n", - " g = nx.from_pandas_edgelist(related_hosts_df, \"IpAddress\", \"Computer\")\n", - " md('Entity Relationship Graph - Related Hosts :: ',styles=[\"bold\",\"green\"])\n", - " nx.draw_circular(g, with_labels=True, size=40, font_size=12, font_color=\"blue\")\n", - "\n", - "\n", - " # Uncomment below cells if you want to dispaly interactive graphs using Pyvis library, Azure notebook free tier may not render the graph correctly.\n", - " # logonpyvis_graph = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n", - "\n", - " # # set the physics layout of the network\n", - " # logonpyvis_graph.barnes_hut()\n", - "\n", - " # sources = related_hosts_df['Computer']\n", - " # targets = related_hosts_df['IpAddress']\n", - " # weights = related_hosts_df['count_']\n", - "\n", - " # edge_data = zip(sources, targets, weights)\n", - "\n", - " # for e in edge_data:\n", - " # src = e[0]\n", - " # dst = e[1]\n", - " # w = e[2]\n", - "\n", - " # logonpyvis_graph.add_node(src, src, title=src)\n", - " # logonpyvis_graph.add_node(dst, dst, title=dst)\n", - " # logonpyvis_graph.add_edge(src, dst, value=w)\n", - "\n", - " # neighbor_map = logonpyvis_graph.get_adj_list()\n", - "\n", - " # # add neighbor data to node hover data\n", - " # for node in logonpyvis_graph.nodes:\n", - " # node[\"title\"] += \" Neighbors:
\" + \"
\".join(neighbor_map[node[\"id\"]])\n", - " # node[\"value\"] = len(neighbor_map[node[\"id\"]]) \n", - "\n", - " # logonpyvis_graph.show(\"hostlogonpyvis_graph.html\")\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Related Accounts\n", - "**Hypothesis:** That an attacker has gained access to the host, compromized credentials for the accounts on it and laterally moving to the network gaining access to more accounts.\n", - "\n", - "This section provides related accounts of IP address which is being investigated. .If you wish to expand the scope of hunting then investigate each accounts in detail, it is recommended that to use the **Account Explorer Notebook (include link).**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-09-10T20:12:42.022358Z", - "start_time": "2019-09-10T20:12:42.010961Z" - } - }, - "source": [ - "[Contents](#toc)\n", - "### Visualization - Networkx Graph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:36.951055Z", - "start_time": "2020-05-15T23:05:35.741976Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\":\n", - " # Retrived relatd accounts from SecurityEvent table for Windows OS\n", - " if ip_entity['OSType'] =='Windows':\n", - " if \"SecurityEvent\" not in available_datasets:\n", - " raise ValueError(\"No Windows event log data available in the workspace\")\n", - " else:\n", - " related_accounts = \"\"\"\n", - " SecurityEvent\n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", - " | summarize count() by Account, Computer\n", - " \"\"\".format(\n", - " **ipaddr_query_params(), hostname=hostname\n", - " )\n", - " %kql -query related_accounts\n", - " related_accounts_df = _kql_raw_result_.to_dataframe()\n", - "\n", - " elif ip_entity['OSType'] =='Linux':\n", - " if \"Syslog\" not in available_datasets:\n", - " raise ValueError(\"No Linux syslog data available in the workspace\")\n", - " else:\n", - " related_accounts_df = qry_prov.LinuxSyslog.list_logons_for_source_ip(invest_times, ip_address=ipaddr_query_params()['ip_address'],add_query_items='extend Account = AccountName | summarize count() by Account, Computer')\n", - "\n", - "\n", - " # Uncomment- below cells if above visualization does not render - Networkx connected Graph\n", - " plt.figure(10, figsize=(22, 14))\n", - " g = nx.from_pandas_edgelist(related_accounts_df, \"Computer\", \"Account\")\n", - " md('Entity Relationship Graph - Related Accounts :: ',styles=[\"bold\",\"green\"])\n", - " nx.draw_circular(g, with_labels=True, size=40, font_size=12, font_color=\"blue\")\n", - "\n", - " # Uncomment below cells if you want to display interactive graphs using Pyvis library, Azure notebook free tier may not render the graph correctly.\n", - " # acclogon_pyvisgraph = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n", - "\n", - " # # set the physics layout of the network\n", - " # acclogon_pyvisgraph.barnes_hut()\n", - "\n", - "\n", - " # sources = related_accounts_df['Computer']\n", - " # targets = related_accounts_df['Account']\n", - " # weights = related_accounts_df['count_']\n", - "\n", - " # edge_data = zip(sources, targets, weights)\n", - "\n", - " # for e in edge_data:\n", - " # src = e[0]\n", - " # dst = e[1]\n", - " # w = e[2]\n", - "\n", - " # acclogon_pyvisgraph.add_node(src, src, title=src)\n", - " # acclogon_pyvisgraph.add_node(dst, dst, title=dst)\n", - " # acclogon_pyvisgraph.add_edge(src, dst, value=w)\n", - "\n", - " # neighbor_map = acclogon_pyvisgraph.get_adj_list()\n", - "\n", - " # # add neighbor data to node hover data\n", - " # for node in acclogon_pyvisgraph.nodes:\n", - " # node[\"title\"] += \" Neighbors:
\" + \"
\".join(neighbor_map[node[\"id\"]])\n", - " # node[\"value\"] = len(neighbor_map[node[\"id\"]]) # this value attrribute for the node affects node size\n", - "\n", - " # acclogon_pyvisgraph.show(\"accountlogonpyvis_graph.html\")\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-08-30T15:50:05.854226Z", - "start_time": "2019-08-30T15:50:04.517392Z" - } - }, - "source": [ - "[Contents](#toc)\n", - "## Logon Summary for Related Entities\n", - "**Hypothesis:** By analyzing logon activities of the related entities, we can identify change in logon patterns and narrow down the entities to few suspicious logon patterns.\n", - "\n", - "This section provides various visualization of logon attributes such as \n", - "- Weekly Failed Logon trend\n", - "- Logon Types \n", - "- Logon Processes\n", - "\n", - "If you wish to expand the scope of hunting then investigate specific host in detail, it is recommended that to use the **Host Explorer Notebook (include link).**" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-09-10T20:18:33.673179Z", - "start_time": "2019-09-10T20:18:33.670042Z" - } - }, - "source": [ - "[Contents](#toc)\n", - "### HeatMap for Weekly failed logons" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:46.615934Z", - "start_time": "2020-05-15T23:05:44.570772Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\":\n", - " # Retrived related accounts from SecurityEvent table for Windows OS\n", - " if ip_entity['OSType'] =='Windows':\n", - " if \"SecurityEvent\" not in available_datasets:\n", - " raise ValueError(\"No Windows event log data available in the workspace\")\n", - " else:\n", - " failed_logons = \"\"\"\n", - " SecurityEvent\n", - " | where EventID in (4624,4625) | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | extend DayofWeek = case(dayofweek(TimeGenerated) == time(1.00:00:00), \"Monday\", \n", - " dayofweek(TimeGenerated) == time(2.00:00:00), \"Tuesday\",\n", - " dayofweek(TimeGenerated) == time(3.00:00:00), \"Wednesday\",\n", - " dayofweek(TimeGenerated) == time(4.00:00:00), \"Thursday\",\n", - " dayofweek(TimeGenerated) == time(5.00:00:00), \"Friday\",\n", - " dayofweek(TimeGenerated) == time(6.00:00:00), \"Saturday\",\n", - " \"Sunday\")\n", - " | summarize LogonCount=count() by DayofWeek, HourOfDay=format_datetime(bin(TimeGenerated,1h),'HH:mm')\n", - " \"\"\".format(\n", - " **ipaddr_query_params(), hostname=hostname\n", - " )\n", - " %kql -query failed_logons\n", - " failed_logons_df = _kql_raw_result_.to_dataframe()\n", - "\n", - " elif ip_entity['OSType'] =='Linux':\n", - " if \"Syslog\" not in available_datasets:\n", - " raise ValueError(\"No Linux syslog data available in the workspace\")\n", - " else: \n", - " failed_logons_df = qry_prov.LinuxSyslog.user_logon(invest_times, account_name ='', add_query_items=\"\"\"| where HostIP == '{ipaddr_text.value}' |extend Account = AccountName | extend DayofWeek = case(dayofweek(TimeGenerated) == time(1.00:00:00), \"Monday\", dayofweek(TimeGenerated) == time(2.00:00:00), \"Tuesday\",\n", - " dayofweek(TimeGenerated) == time(3.00:00:00), \"Wednesday\",\n", - " dayofweek(TimeGenerated) == time(4.00:00:00), \"Thursday\",\n", - " dayofweek(TimeGenerated) == time(5.00:00:00), \"Friday\",\n", - " dayofweek(TimeGenerated) == time(6.00:00:00), \"Saturday\", \"Sunday\") | summarize LogonCount=count() by DayofWeek, HourOfDay=format_datetime(bin(TimeGenerated,1h),'HH:mm')\"\"\")\n", - "\n", - " # Plotting hearmap using seaborn library if there are failed logons\n", - " if len(failed_logons_df) > 0:\n", - " df_pivot = (\n", - " failed_logons_df.reset_index()\n", - " .pivot_table(index=\"DayofWeek\", columns=\"HourOfDay\", values=\"LogonCount\")\n", - " .fillna(0)\n", - " )\n", - " display(\n", - " Markdown(\n", - " f'### Heatmap - Weekly Failed Logon Trend :: '\n", - " )\n", - " )\n", - " f, ax = plt.subplots(figsize=(16, 8))\n", - " hm1 = sns.heatmap(df_pivot, cmap=\"YlGnBu\", ax=ax)\n", - " plt.xticks(rotation=45)\n", - " plt.yticks(rotation=30)\n", - " else:\n", - " linux_logons=qry_prov.LinuxSyslog.list_logons_for_source_ip(**ipaddr_query_params())\n", - " failed_logons = (logon_events[logon_events['LogonResult'] == 'Failure'])\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Host Logons Timeline" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:49.256466Z", - "start_time": "2020-05-15T23:05:49.190460Z" - } - }, - "outputs": [], - "source": [ - "# set the origin time to the time of our alert\n", - "try:\n", - " origin_time = (related_alert.TimeGenerated \n", - " if recenter_wgt.value \n", - " else query_times.origin_time)\n", - "except NameError:\n", - " origin_time = query_times.origin_time\n", - " \n", - "logon_query_times = nbwidgets.QueryTime(\n", - " units=\"day\",\n", - " origin_time=origin_time,\n", - " before=5,\n", - " after=1,\n", - " max_before=20,\n", - " max_after=20,\n", - ")\n", - "logon_query_times.display()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:05:55.096129Z", - "start_time": "2020-05-15T23:05:52.661823Z" - } - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\":\n", - " host_logons = qry_prov.WindowsSecurity.list_host_logons(\n", - " logon_query_times, host_name=hostname\n", - " )\n", - "\n", - " if host_logons is not None and not host_logons.empty:\n", - " display(Markdown(\"### Logon timeline.\"))\n", - " tooltip_cols = [\n", - " \"TargetUserName\",\n", - " \"TargetDomainName\",\n", - " \"SubjectUserName\",\n", - " \"SubjectDomainName\",\n", - " \"LogonType\",\n", - " \"IpAddress\",\n", - " ]\n", - " nbdisplay.display_timeline(\n", - " data=host_logons,\n", - " group_by=\"TargetUserName\",\n", - " source_columns=tooltip_cols,\n", - " legend=\"right\", yaxis=True\n", - " )\n", - "\n", - " display(Markdown(\"### Counts of logon events by logon type.\"))\n", - " display(Markdown(\"Min counts for each logon type highlighted.\"))\n", - " logon_by_type = (\n", - " host_logons[[\"Account\", \"LogonType\", \"EventID\"]]\n", - " .astype({'LogonType': 'int32'})\n", - " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", - " left_on=\"LogonType\", right_index=True)\n", - " .drop(columns=\"LogonType\")\n", - " .groupby([\"Account\", \"LogonTypeDesc\"])\n", - " .count()\n", - " .unstack()\n", - " .rename(columns={\"EventID\": \"LogonCount\"})\n", - " .fillna(0)\n", - " .style\n", - " .background_gradient(cmap=\"viridis\", low=0.5, high=0)\n", - " .format(\"{0:0>3.0f}\")\n", - " )\n", - " display(logon_by_type)\n", - " else:\n", - " display(Markdown(\"No logon events found for host.\"))\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Failed Logons Timeline" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:06:01.493064Z", - "start_time": "2020-05-15T23:05:59.580819Z" - }, - "scrolled": true - }, - "outputs": [], - "source": [ - "if ipaddr_origin == \"Internal\":\n", - " failedLogons = qry_prov.WindowsSecurity.list_host_logon_failures(\n", - " logon_query_times, host_name=ip_entity.hostname\n", - " )\n", - " if failedLogons.empty:\n", - " print(\"No logon failures recorded for this host between \",\n", - " f\" {logon_query_times.start} and {logon_query_times.end}\"\n", - " )\n", - " else:\n", - " nbdisplay.display_timeline(\n", - " data=host_logons.query('TargetLogonId != \"0x3e7\"'),\n", - " overlay_data=failedLogons,\n", - " alert=related_alert,\n", - " title=\"Logons (blue=user-success, green=failed)\",\n", - " source_columns=tooltip_cols,\n", - " height=200,\n", - " )\n", - " display(failedLogons\n", - " .astype({'LogonType': 'int32'})\n", - " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", - " left_on=\"LogonType\", right_index=True)\n", - " [['Account', 'EventID', 'TimeGenerated',\n", - " 'Computer', 'SubjectUserName', 'SubjectDomainName',\n", - " 'TargetUserName', 'TargetDomainName',\n", - " 'LogonTypeDesc','IpAddress', 'WorkstationName'\n", - " ]])\n", - "else:\n", - " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-08-30T15:52:54.700099Z", - "start_time": "2019-08-30T15:52:54.661189Z" - } - }, - "source": [ - "[Contents](#toc)\n", - "## Network Connection Analysis\n", - "\n", - "**Hypothesis:** That an attacker is remotely communicating with the host in order to compromise the host or for outbound communication to C2 for data exfiltration purposes after compromising the host.\n", - "\n", - "This section provides an overview of network activity to and from the host during hunting time frame, the purpose of this is for the identification of anomalous network traffic. If you wish to investigate a specific IP in detail it is recommended that to use another instance of this notebook with each IP addresses." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Network Check Communications with Other Hosts" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:06:06.486183Z", - "start_time": "2020-05-15T23:06:06.429184Z" - } - }, - "outputs": [], - "source": [ - "ip_q_times = nbwidgets.QueryTime(\n", - " label=\"Set time bounds for network queries\",\n", - " units=\"day\",\n", - " max_before=28,\n", - " before=2,\n", - " after=5,\n", - " max_after=28,\n", - " origin_time=logon_query_times.origin_time\n", - ")\n", - "ip_q_times.display()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Query Flows by IP Address" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:06:22.160247Z", - "start_time": "2020-05-15T23:06:10.292782Z" - } - }, - "outputs": [], - "source": [ - "if \"AzureNetworkAnalytics_CL\" not in available_datasets:\n", - " md_warn(\"No network flow data available.\")\n", - " md(\"Please skip the remainder of this section and go to [Time-Series-Anomalies](#Outbound-Data-transfer-Time-Series-Anomalies)\")\n", - " az_net_comms_df = None\n", - "else:\n", - " all_host_ips = (\n", - " ip_entity['private_ips'] + ip_entity['public_ips']\n", - " )\n", - " host_ips = [i.Address for i in all_host_ips]\n", - "\n", - " az_net_comms_df = qry_prov.Network.list_azure_network_flows_by_ip(\n", - " ip_q_times, ip_address_list=host_ips\n", - " )\n", - "\n", - " if isinstance(az_net_comms_df, pd.DataFrame) and not az_net_comms_df.empty:\n", - " az_net_comms_df['TotalAllowedFlows'] = az_net_comms_df['AllowedOutFlows'] + az_net_comms_df['AllowedInFlows']\n", - " nbdisplay.display_timeline(\n", - " data=az_net_comms_df,\n", - " group_by=\"L7Protocol\",\n", - " title=\"Network Flows by Protocol\",\n", - " time_column=\"FlowStartTime\",\n", - " source_columns=[\"FlowType\", \"AllExtIPs\", \"L7Protocol\", \"FlowDirection\"],\n", - " height=300,\n", - " legend=\"right\",\n", - " yaxis=True\n", - " )\n", - " nbdisplay.display_timeline(\n", - " data=az_net_comms_df,\n", - " group_by=\"FlowDirection\",\n", - " title=\"Network Flows by Direction\",\n", - " time_column=\"FlowStartTime\",\n", - " source_columns=[\"FlowType\", \"AllExtIPs\", \"L7Protocol\", \"FlowDirection\"],\n", - " height=300,\n", - " legend=\"right\",\n", - " yaxis=True\n", - " )\n", - " else:\n", - " md_warn(\"No network data for specified time range.\")\n", - " md(\"Please skip the remainder of this section and go to [Time-Series-Anomalies](#Outbound-Data-transfer-Time-Series-Anomalies)\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:06:50.373391Z", - "start_time": "2020-05-15T23:06:50.084392Z" - } - }, - "outputs": [], - "source": [ - "try:\n", - " flow_plot = nbdisplay.display_timeline_values(\n", - " data=az_net_comms_df,\n", - " group_by=\"L7Protocol\",\n", - " source_columns=[\"FlowType\", \n", - " \"AllExtIPs\", \n", - " \"L7Protocol\", \n", - " \"FlowDirection\", \n", - " \"TotalAllowedFlows\"],\n", - " time_column=\"FlowStartTime\",\n", - " y=\"TotalAllowedFlows\",\n", - " legend=\"right\",\n", - " height=500,\n", - " kind=[\"vbar\", \"circle\"],\n", - " );\n", - "except NameError as err:\n", - " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:06:55.928554Z", - "start_time": "2020-05-15T23:06:55.790553Z" - } - }, - "outputs": [], - "source": [ - "try:\n", - " if az_net_comms_df is not None and not az_net_comms_df.empty:\n", - " cm = sns.light_palette(\"green\", as_cmap=True)\n", - "\n", - " cols = [\n", - " \"VMName\",\n", - " \"VMIPAddress\",\n", - " \"PublicIPs\",\n", - " \"SrcIP\",\n", - " \"DestIP\",\n", - " \"L4Protocol\",\n", - " \"L7Protocol\",\n", - " \"DestPort\",\n", - " \"FlowDirection\",\n", - " \"AllExtIPs\",\n", - " \"TotalAllowedFlows\",\n", - " ]\n", - " flow_index = az_net_comms_df[cols].copy()\n", - "\n", - " def get_source_ip(row):\n", - " if row.FlowDirection == \"O\":\n", - " return row.VMIPAddress if row.VMIPAddress else row.SrcIP\n", - " else:\n", - " return row.AllExtIPs if row.AllExtIPs else row.DestIP\n", - "\n", - " def get_dest_ip(row):\n", - " if row.FlowDirection == \"O\":\n", - " return row.AllExtIPs if row.AllExtIPs else row.DestIP\n", - " else:\n", - " return row.VMIPAddress if row.VMIPAddress else row.SrcIP\n", - " \n", - " flow_index[\"source\"] = flow_index.apply(get_source_ip, axis=1)\n", - " flow_index[\"dest\"] = flow_index.apply(get_dest_ip, axis=1)\n", - " display(flow_index)\n", - "\n", - " # Uncomment to view flow_index results\n", - " # with warnings.catch_warnings():\n", - " # warnings.simplefilter(\"ignore\")\n", - " # display(\n", - " # flow_index[\n", - " # [\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\", \"TotalAllowedFlows\"]\n", - " # ]\n", - " # .groupby([\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\"])\n", - " # .sum()\n", - " # .reset_index()\n", - " # .style.bar(subset=[\"TotalAllowedFlows\"], color=\"#d65f5f\")\n", - " # )\n", - "except NameError as err:\n", - " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "### Bulk whois lookup " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:08:00.744206Z", - "start_time": "2020-05-15T23:07:01.493951Z" - } - }, - "outputs": [], - "source": [ - "# Bulk WHOIS lookup function\n", - "from functools import lru_cache\n", - "from ipwhois import IPWhois\n", - "from ipaddress import ip_address\n", - "\n", - "try:\n", - " # Add ASN informatio from Whois\n", - " flows_df = (\n", - " flow_index[[\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\", \"TotalAllowedFlows\"]]\n", - " .groupby([\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\"])\n", - " .sum()\n", - " .reset_index()\n", - " )\n", - "\n", - " num_ips = len(flows_df[\"source\"].unique()) + len(flows_df[\"dest\"].unique())\n", - " print(f\"Performing WhoIs lookups for {num_ips} IPs \", end=\"\")\n", - " #flows_df = flows_df.assign(DestASN=\"\", DestASNFull=\"\", SourceASN=\"\", SourceASNFull=\"\")\n", - " flows_df[\"DestASN\"] = flows_df.apply(lambda x: get_whois_info(x.dest, True), axis=1)\n", - " flows_df[\"SourceASN\"] = flows_df.apply(lambda x: get_whois_info(x.source, True), axis=1)\n", - " print(\"done\")\n", - "\n", - " # Split the tuple returned by get_whois_info into separate columns\n", - " flows_df[\"DestASNFull\"] = flows_df.apply(lambda x: x.DestASN[1], axis=1)\n", - " flows_df[\"DestASN\"] = flows_df.apply(lambda x: x.DestASN[0], axis=1)\n", - " flows_df[\"SourceASNFull\"] = flows_df.apply(lambda x: x.SourceASN[1], axis=1)\n", - " flows_df[\"SourceASN\"] = flows_df.apply(lambda x: x.SourceASN[0], axis=1)\n", - "\n", - " our_host_asns = [get_whois_info(ip.Address)[0] for ip in ip_entity.public_ips]\n", - " md(f\"Host {ip_entity.hostname} ASNs:\", \"bold\")\n", - " md(str(our_host_asns))\n", - "\n", - " flow_sum_df = flows_df.groupby([\"DestASN\", \"SourceASN\"]).agg(\n", - " TotalAllowedFlows=pd.NamedAgg(column=\"TotalAllowedFlows\", aggfunc=\"sum\"),\n", - " L7Protocols=pd.NamedAgg(column=\"L7Protocol\", aggfunc=lambda x: x.unique().tolist()),\n", - " source_ips=pd.NamedAgg(column=\"source\", aggfunc=lambda x: x.unique().tolist()),\n", - " dest_ips=pd.NamedAgg(column=\"dest\", aggfunc=lambda x: x.unique().tolist()),\n", - " ).reset_index()\n", - " flow_sum_df\n", - "except NameError as err:\n", - " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Choose ASNs/IPs to Check for Threat Intel Reports\n", - "Choose from the list of Selected ASNs for the IPs you wish to check on.\n", - "The Source list is been pre-populated with all ASNs found in the network flow summary.\n", - "\n", - "As an example, we've populated the `Selected` list with the ASNs that have the lowest number of flows to and from the host. We also remove the ASN that matches the ASN of the host we are investigating.\n", - "\n", - "Please edit this list, using flow summary data above as a guide and leaving only ASNs that you are suspicious about. Typicially these would be ones with relatively low `TotalAllowedFlows` and possibly with unusual `L7Protocols`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:08:01.347207Z", - "start_time": "2020-05-15T23:08:01.287206Z" - } - }, - "outputs": [], - "source": [ - "try:\n", - " if isinstance(flow_sum_df, pd.DataFrame) and not flow_sum_df.empty:\n", - " all_asns = list(flow_sum_df[\"DestASN\"].unique()) + list(flow_sum_df[\"SourceASN\"].unique())\n", - " all_asns = set(all_asns) - set([\"private address\"])\n", - "\n", - " # Select the ASNs in the 25th percentile (lowest number of flows)\n", - " quant_25pc = flow_sum_df[\"TotalAllowedFlows\"].quantile(q=[0.25]).iat[0]\n", - " quant_25pc_df = flow_sum_df[flow_sum_df[\"TotalAllowedFlows\"] <= quant_25pc]\n", - " other_asns = list(quant_25pc_df[\"DestASN\"].unique()) + list(quant_25pc_df[\"SourceASN\"].unique())\n", - " other_asns = set(other_asns) - set(our_host_asns)\n", - " md(\"Choose IPs from Selected ASNs to look up for Threat Intel.\", \"bold\")\n", - " sel_asn = nbwidgets.SelectSubset(source_items=all_asns, default_selected=other_asns)\n", - "except NameError as err:\n", - " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:08:14.516746Z", - "start_time": "2020-05-15T23:08:01.935205Z" - } - }, - "outputs": [], - "source": [ - "try:\n", - " if isinstance(flow_sum_df, pd.DataFrame) and not flow_sum_df.empty:\n", - " ti_lookup = TILookup()\n", - " from itertools import chain\n", - " dest_ips = set(chain.from_iterable(flow_sum_df[flow_sum_df[\"DestASN\"].isin(sel_asn.selected_items)][\"dest_ips\"]))\n", - " src_ips = set(chain.from_iterable(flow_sum_df[flow_sum_df[\"SourceASN\"].isin(sel_asn.selected_items)][\"source_ips\"]))\n", - " selected_ips = dest_ips | src_ips\n", - " print(f\"{len(selected_ips)} unique IPs in selected ASNs\")\n", - "\n", - " # Add the IoCType to save cost of inferring each item\n", - " selected_ip_dict = {ip: \"ipv4\" for ip in selected_ips}\n", - " ti_results = ti_lookup.lookup_iocs(data=selected_ip_dict)\n", - "\n", - " print(f\"{len(ti_results)} results received.\")\n", - "\n", - " # ti_results_pos = ti_results[ti_results[\"Severity\"] > 0]\n", - " #####\n", - " # WARNING - faking results for illustration purposes\n", - " #####\n", - " ti_results_pos = ti_results.sample(n=2)\n", - "\n", - " print(f\"{len(ti_results_pos)} positive results found.\")\n", - "\n", - "\n", - " if not ti_results_pos.empty:\n", - " src_pos = flows_df.merge(ti_results_pos, left_on=\"source\", right_on=\"Ioc\")\n", - " dest_pos = flows_df.merge(ti_results_pos, left_on=\"dest\", right_on=\"Ioc\")\n", - " ti_ip_results = pd.concat([src_pos, dest_pos])\n", - " md_warn(\"Positive Threat Intel Results found for the following flows\")\n", - " md(\"Please examine these IP flows using the IP Explorer notebook.\", \"bold, large\")\n", - " display(ti_ip_results)\n", - "except NameError as err:\n", - " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " ### GeoIP Map of External IPs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:08:16.023912Z", - "start_time": "2020-05-15T23:08:15.611915Z" - } - }, - "outputs": [], - "source": [ - "iplocation = GeoLiteLookup()\n", - "def format_ip_entity(row, ip_col):\n", - " ip_entity = entities.IpAddress(Address=row[ip_col])\n", - " iplocation.lookup_ip(ip_entity=ip_entity)\n", - " ip_entity.AdditionalData[\"protocol\"] = row.L7Protocol\n", - " if \"severity\" in row:\n", - " ip_entity.AdditionalData[\"threat severity\"] = row[\"severity\"]\n", - " if \"Details\" in row:\n", - " ip_entity.AdditionalData[\"threat details\"] = row[\"Details\"]\n", - " return ip_entity\n", - "\n", - "# from msticpy.nbtools.foliummap import FoliumMap\n", - "folium_map = FoliumMap()\n", - "if az_net_comms_df is None or az_net_comms_df.empty:\n", - " print(\"No network flow data available.\")\n", - "else:\n", - " # Get the flow records for all flows not in the TI results\n", - " selected_out = flows_df[flows_df[\"DestASN\"].isin(sel_asn.selected_items)]\n", - " selected_out = selected_out[~selected_out[\"dest\"].isin(ti_ip_results[\"Ioc\"])]\n", - " if selected_out.empty:\n", - " ips_out = []\n", - " else:\n", - " ips_out = list(selected_out.apply(lambda x: format_ip_entity(x, \"dest\"), axis=1))\n", - " \n", - " selected_in = flows_df[flows_df[\"SourceASN\"].isin(sel_asn.selected_items)]\n", - " selected_in = selected_in[~selected_in[\"source\"].isin(ti_ip_results[\"Ioc\"])]\n", - " if selected_in.empty:\n", - " ips_in = []\n", - " else:\n", - " ips_in = list(selected_in.apply(lambda x: format_ip_entity(x, \"source\"), axis=1))\n", - "\n", - " ips_threats = list(ti_ip_results.apply(lambda x: format_ip_entity(x, \"Ioc\"), axis=1))\n", - "\n", - " display(HTML(\"

External IP Addresses communicating with host

\"))\n", - " display(HTML(\"Numbered circles indicate multiple items - click to expand\"))\n", - " display(HTML(\"Location markers:
Blue = outbound, Purple = inbound, Green = Host, Red = Threats\"))\n", - "\n", - " icon_props = {\"color\": \"green\"}\n", - " for ips in ip_entity.public_ips:\n", - " ips.AdditionalData[\"host\"] = ip_entity.hostname\n", - " folium_map.add_ip_cluster(ip_entities=ip_entity.public_ips, **icon_props)\n", - " icon_props = {\"color\": \"blue\"}\n", - " folium_map.add_ip_cluster(ip_entities=ips_out, **icon_props)\n", - " icon_props = {\"color\": \"purple\"}\n", - " folium_map.add_ip_cluster(ip_entities=ips_in, **icon_props)\n", - " icon_props = {\"color\": \"red\"}\n", - " folium_map.add_ip_cluster(ip_entities=ips_threats, **icon_props)\n", - " \n", - " display(folium_map)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-09-05T18:03:37.980223Z", - "start_time": "2019-09-05T18:03:37.804856Z" - } - }, - "source": [ - "[Contents](#toc)\n", - "### Outbound Data transfer Time Series Anomalies" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This section will look into the network datasources to check outbound data transfer trends. \n", - "You can also use time series analysis using below built-in KQL query example to analyze anamalous data transfer trends.below example shows sample dataset trends comparing with actual vs baseline traffic trends." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:08:42.937737Z", - "start_time": "2020-05-15T23:08:41.794266Z" - } - }, - "outputs": [], - "source": [ - "if \"VMConnection\" in table_index or \"CommonSecurityLog\" in table_index:\n", - " # KQL query for full text search of IP address and display all datatypes\n", - " dataxfer_stats = \"\"\"\n", - " union isfuzzy=true\n", - " (\n", - " CommonSecurityLog \n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", - " | where isnotempty(DestinationIP) and isnotempty(SourceIP)\n", - " | where SourceIP == \\'{ip_address}\\'\n", - " | extend SentBytesinKB = (SentBytes / 1024), ReceivedBytesinKB = (ReceivedBytes / 1024)\n", - " | summarize DailyCount = count(), ListOfDestPorts = make_set(DestinationPort), TotalSentBytesinKB = sum(SentBytesinKB), TotalReceivedBytesinKB = sum(ReceivedBytesinKB) by SourceIP, DestinationIP, DeviceVendor, bin(TimeGenerated,1d)\n", - " | project DeviceVendor, TimeGenerated, SourceIP, DestinationIP, ListOfDestPorts, TotalSentBytesinKB, TotalReceivedBytesinKB \n", - " ),\n", - " (\n", - " VMConnection \n", - " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end}) \n", - " | where isnotempty(DestinationIp) and isnotempty(SourceIp)\n", - " | where SourceIp == \\'{ip_address}\\'\n", - " | extend DeviceVendor = \"VMConnection\", SourceIP = SourceIp, DestinationIP = DestinationIp\n", - " | extend SentBytesinKB = (BytesSent / 1024), ReceivedBytesinKB = (BytesReceived / 1024)\n", - " | summarize DailyCount = count(), ListOfDestPorts = make_set(DestinationPort), TotalSentBytesinKB = sum(SentBytesinKB),TotalReceivedBytesinKB = sum(ReceivedBytesinKB) by SourceIP, DestinationIP, DeviceVendor, bin(TimeGenerated,1d)\n", - " | project DeviceVendor, TimeGenerated, SourceIP, DestinationIP, ListOfDestPorts, TotalSentBytesinKB, TotalReceivedBytesinKB \n", - " )\n", - " \"\"\".format(**ipaddr_query_params())\n", - " %kql -query dataxfer_stats\n", - " dataxfer_stats_df = _kql_raw_result_.to_dataframe()\n", - "\n", - "#Display result as transposed matrix of datatypes availabel to query for the query period\n", - "if len(dataxfer_stats_df) > 0:\n", - " md(\n", - " 'Data transfer daily stats for IP ::', styles=[\"bold\",\"green\"]\n", - " )\n", - " #display(dataxfer_stats_df)\n", - "else:\n", - " md_warn(\n", - " f'No Data transfer logs found for the query period'\n", - " )\n", - " #####\n", - " # WARNING - faking results for illustration purposes\n", - " #####\n", - "md(\n", - " 'Visualizing time series data transfer on dummy dataset for demonstration ::', styles=[\"bold\",\"green\"]\n", - " )\n", - "\n", - "#Generating graph based on dummy dataset in custom table representing Flow records outbound data transfer\n", - "timechartquery = \"\"\"\n", - "let TimeSeriesData = PaloAltoBytesSent_CL\n", - "| extend TimeGenerated = todatetime(EventTime_s), TotalBytesSent = todouble(TotalBytesSent_s) \n", - "| summarize TimeGenerated=make_list(TimeGenerated, 10000),TotalBytesSent=make_list(TotalBytesSent, 10000) by deviceVendor_s\n", - "| project TimeGenerated, TotalBytesSent;\n", - "TimeSeriesData\n", - "| extend (baseline,seasonal,trend,residual) = series_decompose(TotalBytesSent)\n", - "| mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), baseline to typeof(long), seasonal to typeof(long), trend to typeof(long), residual to typeof(long)\n", - "| project TimeGenerated, TotalBytesSent, baseline\n", - "| render timechart with (title=\"Palo Alto Outbound Data Transfer Time Series decomposition\")\n", - "\"\"\"\n", - "%kql -query timechartquery" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### List of Suspicious Activities/ Observables/Hunting bookmarks\n", - "- Suspicious alerts for the IP\n", - "- Anamalous Failed Logon trend on few days at 04:00 AM\n", - "- Anamalous spike in traffic logs on http\n", - "- Positive TI Hit from Open source feeds.\n", - "- Unusual data transfer deviating from normal baseline." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Contents](#toc)\n", - "## Appendices" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Available DataFrames" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-04-02T10:00:41.436112Z", - "start_time": "2020-04-02T10:00:41.426605Z" - } - }, - "outputs": [], - "source": [ - "print('List of current DataFrames in Notebook')\n", - "print('-' * 50)\n", - "current_vars = list(locals().keys())\n", - "for var_name in current_vars:\n", - " if isinstance(locals()[var_name], pd.DataFrame) and not var_name.startswith('_'):\n", - " print(var_name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Saving Data to Excel\n", - "To save the contents of a pandas DataFrame to an Excel spreadsheet\n", - "use the following syntax\n", - "```\n", - "writer = pd.ExcelWriter('myWorksheet.xlsx')\n", - "my_data_frame.to_excel(writer,'Sheet1')\n", - "writer.save()\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration\n", - "\n", - "### `msticpyconfig.yaml` configuration File\n", - "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", - "\n", - "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" - ] - } - ], - "metadata": { - "hide_input": false, - "kernelspec": { - "display_name": "Python 3.6", - "language": "python", - "name": "python36" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - }, - "latex_envs": { - "LaTeX_envs_menu_present": true, - "autoclose": false, - "autocomplete": true, - "bibliofile": "biblio.bib", - "cite_by": "apalike", - "current_citInitial": 1, - "eqLabelWithNumbers": true, - "eqNumInitial": 1, - "hotkeys": { - "equation": "Ctrl-E", - "itemize": "Ctrl-I" - }, - "labels_anchors": false, - "latex_user_defs": false, - "report_style_numbering": false, - "user_envs_cfg": false - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": true, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": true, - "toc_position": { - "height": "calc(100% - 180px)", - "left": "10px", - "top": "150px", - "width": "299px" - }, - "toc_section_display": true, - "toc_window_display": true - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "position": { - "height": "400px", - "left": "1549px", - "right": "20px", - "top": "120px", - "width": "351px" - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Title: IP Explorer\n", + "
\n", + "  Details...\n", + " \n", + "**Notebook Version:** 1.0
\n", + "**Python Version:** Python 3.7 (including Python 3.6 - AzureML)
\n", + "**Required Packages**: kqlmagic, msticpy, pandas, numpy, matplotlib, networkx, ipywidgets, ipython, scikit_learn, dnspython, ipwhois, folium, holoviews
\n", + "**Platforms Supported**:\n", + "- Azure Notebooks Free Compute\n", + "- Azure Notebooks DSVM\n", + "- OS Independent\n", + "\n", + "**Data Sources Required**:\n", + "- Log Analytics \n", + " - Heartbeat\n", + " - SecurityAlert\n", + " - SecurityEvent\n", + " - AzureNetworkAnalytics_CL\n", + " \n", + "- (Optional) \n", + " - VirusTotal (with API key)\n", + " - Alienvault OTX (with API key) \n", + " - IBM Xforce (with API key) \n", + " - CommonSecurityLog\n", + "
\n", + "\n", + "\n", + "Brings together a series of queries and visualizations to help you assess the security state of an IP address. It works with both internal addresses and public addresses. \n", + "
For internal addresses it focuses on traffic patterns and behavior of the host using that IP address. \n", + "
For public IPs it lets you perform threat intelligence lookups, passive dns, whois and other checks. \n", + "
It also allows you to examine any network traffic between the external IP address and your resources." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "toc": true + }, + "source": [ + "

Table of Contents

\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Hunting Hypothesis\n", + "Our broad initial hunting hypothesis is that a we have received IP address entity which is suspected to be compromized internal host or external public address to whom internal hosts are communicating in malicious manner, we will need to hunt from a range of different positions to validate or disprove this hypothesis.\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### IP Explorer Mindmap\n", + "Below mindmap diagram shows hunting workflow depending upon the type of IP address provided\n", + "\n", + "![IPExplorerMindMap](https://github.com/Azure/Azure-Sentinel-Notebooks/raw/master/images/nb_ipexplorer-mindmap.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "### Notebook initialization\n", + "The next cell:\n", + "- Checks for the correct Python version\n", + "- Checks versions and optionally installs required packages\n", + "- Imports the required packages into the notebook\n", + "- Sets a number of configuration options.\n", + "\n", + "This should complete without errors. If you encounter errors or warnings look at the following two notebooks:\n", + "- [TroubleShootingNotebooks](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/TroubleShootingNotebooks.ipynb)\n", + "- [ConfiguringNotebookEnvironment](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)\n", + "\n", + "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", + "- [Run TroubleShootingNotebooks](./TroubleShootingNotebooks.ipynb)\n", + "- [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)\n", + "\n", + "You may also need to do some additional configuration to successfully use functions such as Threat Intelligence service lookup and Geo IP lookup. \n", + "There are more details about this in the `ConfiguringNotebookEnvironment` notebook and in these documents:\n", + "- [msticpy configuration](https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html)\n", + "- [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:01:51.949751Z", + "start_time": "2020-05-15T23:01:51.909753Z" + } + }, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import os\n", + "import sys\n", + "import warnings\n", + "from IPython.display import display, HTML, Markdown\n", + "\n", + "REQ_PYTHON_VER=(3, 6)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", + "\n", + "display(HTML(\"

Starting Notebook setup...

\"))\n", + "if Path(\"./utils/nb_check.py\").is_file():\n", + " from utils.nb_check import check_python_ver, check_mp_ver\n", + "\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", + " try:\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", + " except ImportError:\n", + " !pip install --upgrade msticpy\n", + " if \"msticpy\" in sys.modules:\n", + " importlib.reload(sys.modules[\"msticpy\"])\n", + " else:\n", + " import msticpy\n", + " check_mp_ver(REQ_MSTICPY_VER)\n", + " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", + "from msticpy.nbtools import nbinit\n", + "extra_imports = [\n", + " \"msticpy.nbtools.entityschema, IpAddress\",\n", + " \"msticpy.nbtools.entityschema, GeoLocation\",\n", + " \"msticpy.sectools.ip_utils, create_ip_record\",\n", + " \"msticpy.sectools.ip_utils, get_ip_type\",\n", + " \"msticpy.sectools.ip_utils, get_whois_info\",\n", + "]\n", + "nbinit.init_notebook(\n", + " namespace=globals(),\n", + " extra_imports=extra_imports,\n", + ");\n", + "WIDGET_DEFAULTS = {\n", + " \"layout\": widgets.Layout(width=\"95%\"),\n", + " \"style\": {\"description_width\": \"initial\"},\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Get WorkspaceId and Authenticate to Log Analytics \n", + "
\n", + "  Details...\n", + "If you are using user/device authentication, run the following cell. \n", + "- Click the 'Copy code to clipboard and authenticate' button.\n", + "- This will pop up an Azure Active Directory authentication dialog (in a new tab or browser window). The device code will have been copied to the clipboard. \n", + "- Select the text box and paste (Ctrl-V/Cmd-V) the copied value. \n", + "- You should then be redirected to a user authentication page where you should authenticate with a user account that has permission to query your Log Analytics workspace.\n", + "\n", + "Use the following syntax if you are authenticating using an Azure Active Directory AppId and Secret:\n", + "```\n", + "%kql loganalytics://tenant(aad_tenant).workspace(WORKSPACE_ID).clientid(client_id).clientsecret(client_secret)\n", + "```\n", + "instead of\n", + "```\n", + "%kql loganalytics://code().workspace(WORKSPACE_ID)\n", + "```\n", + "\n", + "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", + "On successful authentication you should see a ```popup schema``` button.\n", + "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:02:52.662562Z", + "start_time": "2020-05-15T23:02:52.653563Z" + } + }, + "outputs": [], + "source": [ + "#See if we have an Azure Sentinel Workspace defined in our config file, if not let the user specify Workspace and Tenant IDs\n", + "from msticpy.nbtools.wsconfig import WorkspaceConfig\n", + "ws_config = WorkspaceConfig()\n", + "try:\n", + " ws_id = ws_config['workspace_id']\n", + " ten_id = ws_config['tenant_id']\n", + " config = True\n", + " md(\"Workspace details collected from config file\")\n", + "except KeyError:\n", + " md(('Please go to your Log Analytics workspace, copy the workspace ID'\n", + " ' and/or tenant Id and paste here to enable connection to the workspace and querying of it..
'))\n", + " ws_id_wgt = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", + " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", + " ten_id_wgt = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", + " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", + " config = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:03:22.552179Z", + "start_time": "2020-05-15T23:02:56.043852Z" + } + }, + "outputs": [], + "source": [ + "# Authentication\n", + "qry_prov = QueryProvider(data_environment=\"LogAnalytics\")\n", + "qry_prov.connect(connection_str=ws_config.code_connect_str)\n", + "table_index = qry_prov.schema_tables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Enter the IP Address and query time window\n", + "\n", + "Type the IP address you want to search for and the time bounds over which search.\n", + "\n", + "You can specify the IP address value in the widget e.g. 192.168.1.1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:03:22.632179Z", + "start_time": "2020-05-15T23:03:22.619179Z" + } + }, + "outputs": [], + "source": [ + "ipaddr_text = widgets.Text(\n", + " description=\"Enter the IP Address to search for:\", **WIDGET_DEFAULTS\n", + ")\n", + "display(ipaddr_text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:03:56.698491Z", + "start_time": "2020-05-15T23:03:56.631491Z" + } + }, + "outputs": [], + "source": [ + "query_times = nbwidgets.QueryTime(units=\"day\", max_before=20, before=5, max_after=7)\n", + "query_times.display()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Determine IP Address Type" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:04:47.927548Z", + "start_time": "2020-05-15T23:04:43.963316Z" + } + }, + "outputs": [], + "source": [ + "# Set up function to allow easy reference to common parameters for queries throughout the notebook\n", + "def ipaddr_query_params():\n", + " return {\n", + " \"start\": query_times.start,\n", + " \"end\": query_times.end,\n", + " \"ip_address\": ipaddr_text.value.strip()\n", + " }\n", + "\n", + "ipaddr_type = get_ip_type(ipaddr_query_params()['ip_address'])\n", + "\n", + "md(f'Depending on the IP Address origin, different sections of this notebook are applicable', styles=[\"bold\", \"large\"])\n", + "md(f'Please follow either the Interal IP Address or External IP Address sections based on below Recommendation', styles=[\"bold\"])\n", + "\n", + "#Get details from Heartbeat table for the given IP Address and Time Parameters\n", + "heartbeat_df = qry_prov.Heartbeat.get_info_by_ipaddress(**ipaddr_query_params())\n", + "\n", + "# Set hostnames retrived from Heartbeat table if available\n", + "if not heartbeat_df.empty:\n", + " hostname = heartbeat_df[\"Computer\"][0]\n", + "else:\n", + " hostname = \"\"\n", + " \n", + "if not heartbeat_df.empty:\n", + " ipaddr_origin = \"Internal\"\n", + " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", + " display(Markdown('#### Recommendation - Go to section [InternalIP](#goto_internalIP)'))\n", + "elif ipaddr_type==\"Private\" and heartbeat_df.empty:\n", + " ipaddr_origin = \"Unknown\"\n", + " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", + " display(Markdown('#### Recommendation - Go to section [InternalIP](#goto_internalIP)'))\n", + "else:\n", + " ipaddr_origin = \"External\"\n", + " md(f'IP Address type based on subnet: {ipaddr_type} & IP Address Owner based on available logs : {ipaddr_origin}', styles=[\"blue\",\"bold\"])\n", + " display(Markdown('#### Recommendation - Go to section [ExternalIP](#goto_externalIP)'))\n", + " \n", + "#Populate related IP addresses for the calculated hostname\n", + "az_net_df = pd.DataFrame()\n", + "if \"AzureNetworkAnalytics_CL\" in table_index:\n", + " aznet_query = f\"\"\"\n", + " AzureNetworkAnalytics_CL | where ResourceType == 'NetworkInterface' \n", + " | where SubType_s == \"Topology\" \n", + " | search \\'{ipaddr_text.value}\\' \n", + " | where TimeGenerated >= datetime({query_times.start}) \n", + " | where TimeGenerated <= datetime({query_times.end}) \n", + " | where VirtualMachine_s has '{hostname}' \n", + " | top 1 by TimeGenerated desc \n", + " | project PrivateIPAddresses = PrivateIPAddresses_s, PublicIPAddresses = PublicIPAddresses_s\"\"\"\n", + " az_net_df = qry_prov.exec_query(query=aznet_query)\n", + " \n", + "# Create IP Entity record using available dataframes or input ip address if nothing present\n", + "if az_net_df.empty and heartbeat_df.empty:\n", + " ip_entity = IpAddress()\n", + " ip_entity['Address'] = ipaddr_query_params()['ip_address']\n", + " ip_entity['Type'] = 'ipaddress'\n", + " ip_entity['OSType'] = 'Unknown'\n", + " md('No Heartbeat Data and Network topology data found')\n", + "elif not heartbeat_df.empty:\n", + " if az_net_df.empty:\n", + " ip_entity = create_ip_record(\n", + " heartbeat_df=heartbeat_df)\n", + " else:\n", + " ip_entity = create_ip_record(\n", + " heartbeat_df=heartbeat_df, az_net_df=az_net_df)\n", + "#Display IP Entity\n", + "md(\"Displaying IP Entity\", styles=[\"green\",\"bold\"])\n", + "print(ip_entity)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## External IP" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### GeoIP Lookups for External IP Addresses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-04-27T08:33:37.478812Z", + "start_time": "2020-04-27T08:33:37.470173Z" + } + }, + "outputs": [], + "source": [ + "# msticpy- geoip module to retrieving Geo Location for Public IP addresses\n", + "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", + "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", + " iplocation = GeoLiteLookup()\n", + "\n", + " loc_results, ext_ip_entity = iplocation.lookup_ip(ip_address=ipaddr_query_params()['ip_address'])\n", + " md(\n", + " 'Geo Location for the IP Address ::', styles=[\"bold\",\"green\"]\n", + " )\n", + " print(ext_ip_entity[0])\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Whois Registrars for External IP Addresses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-04-27T08:33:39.572115Z", + "start_time": "2020-04-27T08:33:39.566009Z" + } + }, + "outputs": [], + "source": [ + "# ipwhois module to retrieve whois registrar for Public IP addresses\n", + "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", + "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", + " from ipwhois import IPWhois\n", + "\n", + " whois = IPWhois(ipaddr_query_params()['ip_address'])\n", + " whois_result = whois.lookup_whois()\n", + " if whois_result:\n", + " md(f'Whois Registrar Info ::', styles=[\"bold\",\"green\"])\n", + " display(whois_result)\n", + " else:\n", + " md(\n", + " f'No whois records available', styles=[\"bold\",\"orange\"]\n", + " )\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Opensource and Azure Sentinel ThreatIntel Lookups" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Configure your TI Provider settings\n", + "If you have not used threat intelligence lookups before you will need to supply API keys for the \n", + "TI Providers that you want to use. Please see the section on configuring [msticpyconfig.yaml](#msticpyconfig.yaml-configuration-File)\n", + "\n", + "Then reload provider settings:\n", + "```\n", + "mylookup = TILookup()\n", + "mylookup.reload_provider_settings()\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-04-27T08:33:43.562087Z", + "start_time": "2020-04-27T08:33:43.554830Z" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "# To force Threatinel lookup for Internal public IP, replace and with or in if condition\n", + "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", + " mylookup = TILookup()\n", + " mylookup.loaded_providers\n", + " resp = mylookup.lookup_ioc(observable=ipaddr_query_params()['ip_address'], ioc_type=\"ipv4\")\n", + " md(f'ThreatIntel Lookup for IP ::', styles=[\"bold\",\"green\"])\n", + " display(mylookup.result_to_df(resp).T)\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Passive DNS lookups for External IP Addresses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-04-27T08:33:45.838706Z", + "start_time": "2020-04-27T08:33:45.829919Z" + } + }, + "outputs": [], + "source": [ + "# To force Passive DNS lookup for Internal public IP, change and with or in if\n", + "if ipaddr_type == \"Public\" and ipaddr_origin == \"External\" :\n", + " # retrieve passive dns from TI Providers\n", + " pdns = mylookup.lookup_ioc(\n", + " observable=ipaddr_query_params()['ip_address'],\n", + " ioc_type=\"ipv4\",\n", + " ioc_query_type=\"passivedns\",\n", + " providers=[\"XForce\"],\n", + " )\n", + " pdns_df = mylookup.result_to_df(pdns)\n", + " if not pdns_df.empty and pdns_df[\"RawResult\"][0] and \"RDNS\" in pdns_df[\"RawResult\"][0]:\n", + " pdnsdomains = pdns_df[\"RawResult\"][0][\"RDNS\"]\n", + " md(\n", + " 'Passive DNS domains for IP: {pdnsdomains}',styles=[\"bold\",\"green\"]\n", + " )\n", + " display(mylookup.result_to_df(pdns).T)\n", + " else:\n", + " md(\n", + " 'No passive domains found from the providers', styles=[\"bold\",\"orange\"]\n", + " )\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "## Internal IP Address" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Data Sources available to query related to IP" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:04:59.773853Z", + "start_time": "2020-05-15T23:04:53.039482Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin in [\"Internal\",\"Unknown\"]:\n", + " # KQL query for full text search of IP address and display all datatypes populated for the time period\n", + " datasource_status = \"\"\"\n", + " search \\'{ip_address}\\' or \\'{hostname}\\'\n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | summarize RowCount=count() by Table=$table\n", + " \"\"\".format(\n", + " **ipaddr_query_params(), hostname=hostname\n", + " )\n", + " datasource_status_df = qry_prov.exec_query(datasource_status)\n", + "\n", + " # Display result as transposed matrix of datatypes availabel to query for the query period\n", + " if not datasource_status_df.empty:\n", + " available_datasets = datasource_status_df['Table'].values\n", + " md(\"Datasources available to query for IP ::\", styles=[\"green\",\"bold\"])\n", + " display(datasource_status_df)\n", + " else:\n", + " md_warn(\"No datasources contain given IP address for the query period\")\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address type is: {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Check if IP is assigned to multiple hostnames" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:03.895367Z", + "start_time": "2020-05-15T23:05:02.486243Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\" or not datasource_status_df.empty:\n", + " # Get single event - try process creation\n", + " if ip_entity['OSType'] =='Windows':\n", + " if \"SecurityEvent\" not in available_datasets:\n", + " raise ValueError(\"No Windows event log data available in the workspace\")\n", + " host_name = None\n", + " matching_hosts_df = qry_prov.WindowsSecurity.list_host_processes(\n", + " query_times, host_name=hostname, add_query_items=\"| distinct Computer\"\n", + " )\n", + " elif ip_entity['OSType'] =='Linux':\n", + " if \"Syslog\" not in available_datasets:\n", + " raise ValueError(\"No Linux syslog data available in the workspace\")\n", + " else:\n", + " linux_syslog_query = f\"\"\" Syslog | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end}) | where HostIP == '{ipaddr_text.value}' | distinct Computer \"\"\"\n", + " matching_hosts_df = qry_prov.exec_query(query=linux_syslog_query)\n", + "\n", + " if len(matching_hosts_df) > 1:\n", + " print(f\"Multiple matches for '{hostname}'. Please select a host from the list.\")\n", + " choose_host = nbwidgets.SelectString(\n", + " item_list=list(matching_hosts_df[\"Computer\"].values),\n", + " description=\"Select the host.\",\n", + " auto_display=True,\n", + " )\n", + " elif not matching_hosts_df.empty:\n", + " host_name = matching_hosts_df[\"Computer\"].iloc[0]\n", + " print(f\"Unique host found for IP: {hostname}\")\n", + "elif datasource_status_df.empty:\n", + " md_warn(\"No datasources contain given IP address for the query period\")\n", + "else: \n", + " md(f'Analysis section Not Applicable since IP address type is : {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### System Info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:07.346683Z", + "start_time": "2020-05-15T23:05:07.330684Z" + } + }, + "outputs": [], + "source": [ + "# Retrieving System info from internal table if IP address is not Public\n", + "if ipaddr_origin == \"Internal\" and not heartbeat_df.empty:\n", + " md(\n", + " 'System Info retrieved from Heartbeat table ::', styles=[\"green\",\"bold\"]\n", + " )\n", + " display(heartbeat_df.T)\n", + "else:\n", + " md_warn(\n", + " 'No records available in HeartBeat table'\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### ServiceMap - Get List of Services for Host" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:10.389939Z", + "start_time": "2020-05-15T23:05:10.369939Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\":\n", + " if \"ServiceMapProcess_CL\" not in available_datasets:\n", + " md_warn(\"ServiceMap data is not enabled\")\n", + " md(\n", + " f\"Enable ServiceMap Solution from Azure marketplce:
\"\n", + " +\"https://docs.microsoft.com/en-us/azure/azure-monitor/insights/service-map#enable-service-map\",\n", + " styles=[\"bold\"]\n", + " )\n", + "\n", + " else:\n", + " servicemap_proc_query = \"\"\"\n", + " ServiceMapProcess_CL\n", + " | where Computer == \\'{hostname}\\'\n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | project Computer, Services_s, DisplayName_s, ExecutableName_s , ExecutablePath_s \n", + " \"\"\".format(\n", + " hostname=hostname, **ipaddr_query_params()\n", + " )\n", + "\n", + " servicemap_proc_df = qry_prov.exec_query(servicemap_proc_query)\n", + " display(servicemap_proc_df)\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address type is {ipaddr_type}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Related Alerts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:14.185177Z", + "start_time": "2020-05-15T23:05:14.123178Z" + } + }, + "outputs": [], + "source": [ + "ra_query_times = nbwidgets.QueryTime(\n", + " units=\"day\",\n", + " origin_time=query_times.origin_time,\n", + " max_before=28,\n", + " max_after=5,\n", + " before=5,\n", + " auto_display=True,\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualization - Timeline of Related Alerts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:19.536611Z", + "start_time": "2020-05-15T23:05:17.943028Z" + } + }, + "outputs": [], + "source": [ + "#Provide hostname if present to the query\n", + "if hostname:\n", + " md(f\"Searching for alerts related to {hostname}...\")\n", + " related_alerts = qry_prov.SecurityAlert.list_related_alerts(\n", + " ra_query_times, host_name=hostname\n", + " )\n", + "else:\n", + " md(f\"Searching for alerts related to ip address(es) {ipaddr_query_params()['ip_address']}\")\n", + " related_alerts = qry_prov.SecurityAlert.list_alerts_for_ip(\n", + " ra_query_times, source_ip_list=ipaddr_query_params()['ip_address']\n", + " )\n", + "\n", + "\n", + "def print_related_alerts(alertDict, entityType, entityName):\n", + " if len(alertDict) > 0:\n", + " md(\n", + " f\"Found {len(alertDict)} different alert types related to this {entityType} (`{entityName}`)\",styles=[\"bold\",\"orange\"]\n", + " )\n", + " for (k, v) in alertDict.items():\n", + " print(f\"- {k}, # Alerts: {v}\")\n", + " else:\n", + " print(f\"No alerts for {entityType} entity `{entityName}`\")\n", + "\n", + "\n", + "if isinstance(related_alerts, pd.DataFrame) and not related_alerts.empty:\n", + " host_alert_items = (\n", + " related_alerts[[\"AlertName\", \"TimeGenerated\"]]\n", + " .groupby(\"AlertName\")\n", + " .TimeGenerated.agg(\"count\")\n", + " .to_dict()\n", + " )\n", + " print_related_alerts(host_alert_items, \"host\", hostname)\n", + " nbdisplay.display_timeline(\n", + " data=related_alerts, title=\"Alerts\", source_columns=[\"AlertName\"], height=200\n", + " )\n", + "else:\n", + " md(\"No related alerts found.\",styles=[\"bold\",\"green\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### Browse List of Related Alerts\n", + " Select an Alert to view details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:24.530275Z", + "start_time": "2020-05-15T23:05:24.464277Z" + } + }, + "outputs": [], + "source": [ + "def disp_full_alert(alert):\n", + " global related_alert\n", + " related_alert = SecurityAlert(alert)\n", + " nbdisplay.display_alert(related_alert, show_entities=True)\n", + "\n", + "recenter_wgt = widgets.Checkbox(\n", + " value=True,\n", + " description='Center subsequent query times round selected Alert?',\n", + " disabled=False,\n", + " **WIDGET_DEFAULTS\n", + ")\n", + "if related_alerts is not None and not related_alerts.empty:\n", + " related_alerts[\"CompromisedEntity\"] = related_alerts[\"Computer\"]\n", + " md(\"Click on alert to view details.\", styles=[\"bold\"])\n", + " display(recenter_wgt)\n", + " rel_alert_select = nbwidgets.SelectAlert(\n", + " alerts=related_alerts,\n", + " action=disp_full_alert,\n", + " )\n", + " rel_alert_select.display()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Related Hosts\n", + "**Hypothesis:** That an attacker has gained access to the host, compromized credentials for the accounts and laterally moving to the network gaining access to more hosts.\n", + "\n", + "This section provides related hosts of IP address which is being investigated. .If you wish to expand the scope of hunting then investigate each hosts in detail, it is recommended that to use the **Host Explorer (Windows/Linux)**\n", + " - [Entity Explorer - Windows Host](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/Entity%20Explorer%20-%20Windows%20Host.ipynb)\n", + " - [Entity Explorer - Linux Host](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/Entity%20Explorer%20-%20Linux%20Host.ipynb)\n", + " \n", + "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", + " - [Run Entity Explorer - Windows Host](./Entity%20Explorer%20-%20Windows%20Host.ipynb)\n", + " - [Run Entity Explorer - Linux Host](./Entity%20Explorer%20-%20Linux%20Host.ipynb)\n", + "\n", + "#### __NOTE - the following sections are only relevant for Internal IP Addresses.__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Visualization - Networkx Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:30.863302Z", + "start_time": "2020-05-15T23:05:29.870080Z" + } + }, + "outputs": [], + "source": [ + "import networkx as nx\n", + "if ipaddr_origin == \"Internal\":\n", + " # Retrived relatd accounts from SecurityEvent table for Windows OS\n", + " if ip_entity['OSType'] =='Windows':\n", + " if \"SecurityEvent\" not in available_datasets:\n", + " raise ValueError(\"No Windows event log data available in the workspace\")\n", + " else:\n", + " related_hosts = \"\"\"\n", + " SecurityEvent\n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", + " | summarize count() by Computer, IpAddress\n", + " \"\"\".format(\n", + " **ipaddr_query_params(), hostname=hostname\n", + " )\n", + "\n", + " related_hosts_df = qry_prov.exec_query(related_hosts)\n", + "\n", + " elif ip_entity['OSType'] =='Linux':\n", + " if \"Syslog\" not in available_datasets:\n", + " raise ValueError(\"No Linux syslog data available in the workspace\")\n", + " else:\n", + " related_hosts_df = qry_prov.LinuxSyslog.list_logons_for_source_ip(invest_times, ip_address=ipaddr_query_params()['ip_address'],add_query_items='extend IpAddress = HostIP | summarize count() by Computer, IpAddress')\n", + "\n", + " # Displaying networkx - static graph. for interactive graph uncomment and run next block of code.\n", + " plt.figure(10, figsize=(22, 14))\n", + " g = nx.from_pandas_edgelist(related_hosts_df, \"IpAddress\", \"Computer\")\n", + " md('Entity Relationship Graph - Related Hosts :: ',styles=[\"bold\",\"green\"])\n", + " nx.draw_circular(g, with_labels=True, size=40, font_size=12, font_color=\"blue\")\n", + "\n", + "\n", + " # Uncomment below cells if you want to dispaly interactive graphs using Pyvis library, Azure notebook free tier may not render the graph correctly.\n", + " # logonpyvis_graph = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n", + "\n", + " # # set the physics layout of the network\n", + " # logonpyvis_graph.barnes_hut()\n", + "\n", + " # sources = related_hosts_df['Computer']\n", + " # targets = related_hosts_df['IpAddress']\n", + " # weights = related_hosts_df['count_']\n", + "\n", + " # edge_data = zip(sources, targets, weights)\n", + "\n", + " # for e in edge_data:\n", + " # src = e[0]\n", + " # dst = e[1]\n", + " # w = e[2]\n", + "\n", + " # logonpyvis_graph.add_node(src, src, title=src)\n", + " # logonpyvis_graph.add_node(dst, dst, title=dst)\n", + " # logonpyvis_graph.add_edge(src, dst, value=w)\n", + "\n", + " # neighbor_map = logonpyvis_graph.get_adj_list()\n", + "\n", + " # # add neighbor data to node hover data\n", + " # for node in logonpyvis_graph.nodes:\n", + " # node[\"title\"] += \" Neighbors:
\" + \"
\".join(neighbor_map[node[\"id\"]])\n", + " # node[\"value\"] = len(neighbor_map[node[\"id\"]]) \n", + "\n", + " # logonpyvis_graph.show(\"hostlogonpyvis_graph.html\")\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Related Accounts\n", + "**Hypothesis:** That an attacker has gained access to the host, compromized credentials for the accounts on it and laterally moving to the network gaining access to more accounts.\n", + "\n", + "This section provides related accounts of IP address which is being investigated. .If you wish to expand the scope of hunting then investigate each accounts in detail, it is recommended that to use the **Account Explorer.**\n", + " - [Entity Explorer - Account](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/Entity%20Explorer%20-%20Account.ipynb)\n", + "\n", + "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", + " - [Run Entity Explorer - Account](./Entity%20Explorer%20-%20Account.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-09-10T20:12:42.022358Z", + "start_time": "2019-09-10T20:12:42.010961Z" + } + }, + "source": [ + "[Contents](#toc)\n", + "### Visualization - Networkx Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:36.951055Z", + "start_time": "2020-05-15T23:05:35.741976Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\":\n", + " # Retrived relatd accounts from SecurityEvent table for Windows OS\n", + " if ip_entity['OSType'] =='Windows':\n", + " if \"SecurityEvent\" not in available_datasets:\n", + " raise ValueError(\"No Windows event log data available in the workspace\")\n", + " else:\n", + " related_accounts = \"\"\"\n", + " SecurityEvent\n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", + " | summarize count() by Account, Computer\n", + " \"\"\".format(\n", + " **ipaddr_query_params(), hostname=hostname\n", + " )\n", + " related_accounts_df = qry_prov.exec_query(related_accounts)\n", + "\n", + " elif ip_entity['OSType'] =='Linux':\n", + " if \"Syslog\" not in available_datasets:\n", + " raise ValueError(\"No Linux syslog data available in the workspace\")\n", + " else:\n", + " related_accounts_df = qry_prov.LinuxSyslog.list_logons_for_source_ip(invest_times, ip_address=ipaddr_query_params()['ip_address'],add_query_items='extend Account = AccountName | summarize count() by Account, Computer')\n", + "\n", + "\n", + " # Uncomment- below cells if above visualization does not render - Networkx connected Graph\n", + " plt.figure(10, figsize=(22, 14))\n", + " g = nx.from_pandas_edgelist(related_accounts_df, \"Computer\", \"Account\")\n", + " md('Entity Relationship Graph - Related Accounts :: ',styles=[\"bold\",\"green\"])\n", + " nx.draw_circular(g, with_labels=True, size=40, font_size=12, font_color=\"blue\")\n", + "\n", + " # Uncomment below cells if you want to display interactive graphs using Pyvis library, Azure notebook free tier may not render the graph correctly.\n", + " # acclogon_pyvisgraph = Network(notebook=True, height=\"750px\", width=\"100%\", bgcolor=\"#222222\", font_color=\"white\")\n", + "\n", + " # # set the physics layout of the network\n", + " # acclogon_pyvisgraph.barnes_hut()\n", + "\n", + "\n", + " # sources = related_accounts_df['Computer']\n", + " # targets = related_accounts_df['Account']\n", + " # weights = related_accounts_df['count_']\n", + "\n", + " # edge_data = zip(sources, targets, weights)\n", + "\n", + " # for e in edge_data:\n", + " # src = e[0]\n", + " # dst = e[1]\n", + " # w = e[2]\n", + "\n", + " # acclogon_pyvisgraph.add_node(src, src, title=src)\n", + " # acclogon_pyvisgraph.add_node(dst, dst, title=dst)\n", + " # acclogon_pyvisgraph.add_edge(src, dst, value=w)\n", + "\n", + " # neighbor_map = acclogon_pyvisgraph.get_adj_list()\n", + "\n", + " # # add neighbor data to node hover data\n", + " # for node in acclogon_pyvisgraph.nodes:\n", + " # node[\"title\"] += \" Neighbors:
\" + \"
\".join(neighbor_map[node[\"id\"]])\n", + " # node[\"value\"] = len(neighbor_map[node[\"id\"]]) # this value attrribute for the node affects node size\n", + "\n", + " # acclogon_pyvisgraph.show(\"accountlogonpyvis_graph.html\")\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-08-30T15:50:05.854226Z", + "start_time": "2019-08-30T15:50:04.517392Z" + } + }, + "source": [ + "[Contents](#toc)\n", + "## Logon Summary for Related Entities\n", + "**Hypothesis:** By analyzing logon activities of the related entities, we can identify change in logon patterns and narrow down the entities to few suspicious logon patterns.\n", + "\n", + "This section provides various visualization of logon attributes such as \n", + "- Weekly Failed Logon trend\n", + "- Logon Types \n", + "- Logon Processes\n", + "\n", + "If you wish to expand the scope of hunting then investigate specific host in detail, it is recommended that to use the **Host Explorer (Windows/Linux)**\n", + "\n", + " - [Entity Explorer - Windows Host](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/Entity%20Explorer%20-%20Windows%20Host.ipynb)\n", + " - [Entity Explorer - Linux Host](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/Entity%20Explorer%20-%20Linux%20Host.ipynb)\n", + " \n", + "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", + " - [Run Entity Explorer - Windows Host](./Entity%20Explorer%20-%20Windows%20Host.ipynb)\n", + " - [Run Entity Explorer - Linux Host](./Entity%20Explorer%20-%20Linux%20Host.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-09-10T20:18:33.673179Z", + "start_time": "2019-09-10T20:18:33.670042Z" + } + }, + "source": [ + "[Contents](#toc)\n", + "### HeatMap for Weekly failed logons" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:46.615934Z", + "start_time": "2020-05-15T23:05:44.570772Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\":\n", + " # Retrived related accounts from SecurityEvent table for Windows OS\n", + " if ip_entity['OSType'] =='Windows':\n", + " if \"SecurityEvent\" not in available_datasets:\n", + " raise ValueError(\"No Windows event log data available in the workspace\")\n", + " else:\n", + " failed_logons = \"\"\"\n", + " SecurityEvent\n", + " | where EventID in (4624,4625) | where IpAddress == \\'{ip_address}\\' or Computer == \\'{hostname}\\' \n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | extend DayofWeek = case(dayofweek(TimeGenerated) == time(1.00:00:00), \"Monday\", \n", + " dayofweek(TimeGenerated) == time(2.00:00:00), \"Tuesday\",\n", + " dayofweek(TimeGenerated) == time(3.00:00:00), \"Wednesday\",\n", + " dayofweek(TimeGenerated) == time(4.00:00:00), \"Thursday\",\n", + " dayofweek(TimeGenerated) == time(5.00:00:00), \"Friday\",\n", + " dayofweek(TimeGenerated) == time(6.00:00:00), \"Saturday\",\n", + " \"Sunday\")\n", + " | summarize LogonCount=count() by DayofWeek, HourOfDay=format_datetime(bin(TimeGenerated,1h),'HH:mm')\n", + " \"\"\".format(\n", + " **ipaddr_query_params(), hostname=hostname\n", + " )\n", + " failed_logons_df = qry_prov.exec_query(failed_logons)\n", + "\n", + " elif ip_entity['OSType'] =='Linux':\n", + " if \"Syslog\" not in available_datasets:\n", + " raise ValueError(\"No Linux syslog data available in the workspace\")\n", + " else: \n", + " failed_logons_df = qry_prov.LinuxSyslog.user_logon(invest_times, account_name ='', add_query_items=\"\"\"| where HostIP == '{ipaddr_text.value}' |extend Account = AccountName | extend DayofWeek = case(dayofweek(TimeGenerated) == time(1.00:00:00), \"Monday\", dayofweek(TimeGenerated) == time(2.00:00:00), \"Tuesday\",\n", + " dayofweek(TimeGenerated) == time(3.00:00:00), \"Wednesday\",\n", + " dayofweek(TimeGenerated) == time(4.00:00:00), \"Thursday\",\n", + " dayofweek(TimeGenerated) == time(5.00:00:00), \"Friday\",\n", + " dayofweek(TimeGenerated) == time(6.00:00:00), \"Saturday\", \"Sunday\") | summarize LogonCount=count() by DayofWeek, HourOfDay=format_datetime(bin(TimeGenerated,1h),'HH:mm')\"\"\")\n", + "\n", + " # Plotting hearmap using seaborn library if there are failed logons\n", + " if len(failed_logons_df) > 0:\n", + " df_pivot = (\n", + " failed_logons_df.reset_index()\n", + " .pivot_table(index=\"DayofWeek\", columns=\"HourOfDay\", values=\"LogonCount\")\n", + " .fillna(0)\n", + " )\n", + " display(\n", + " Markdown(\n", + " f'### Heatmap - Weekly Failed Logon Trend :: '\n", + " )\n", + " )\n", + " f, ax = plt.subplots(figsize=(16, 8))\n", + " hm1 = sns.heatmap(df_pivot, cmap=\"YlGnBu\", ax=ax)\n", + " plt.xticks(rotation=45)\n", + " plt.yticks(rotation=30)\n", + " else:\n", + " linux_logons=qry_prov.LinuxSyslog.list_logons_for_source_ip(**ipaddr_query_params())\n", + " failed_logons = (logon_events[logon_events['LogonResult'] == 'Failure'])\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Host Logons Timeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:49.256466Z", + "start_time": "2020-05-15T23:05:49.190460Z" + } + }, + "outputs": [], + "source": [ + "# set the origin time to the time of our alert\n", + "try:\n", + " origin_time = (related_alert.TimeGenerated \n", + " if recenter_wgt.value \n", + " else query_times.origin_time)\n", + "except NameError:\n", + " origin_time = query_times.origin_time\n", + " \n", + "logon_query_times = nbwidgets.QueryTime(\n", + " units=\"day\",\n", + " origin_time=origin_time,\n", + " before=5,\n", + " after=1,\n", + " max_before=20,\n", + " max_after=20,\n", + ")\n", + "logon_query_times.display()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:05:55.096129Z", + "start_time": "2020-05-15T23:05:52.661823Z" + } + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\":\n", + " host_logons = qry_prov.WindowsSecurity.list_host_logons(\n", + " logon_query_times, host_name=hostname\n", + " )\n", + "\n", + " if host_logons is not None and not host_logons.empty:\n", + " display(Markdown(\"### Logon timeline.\"))\n", + " tooltip_cols = [\n", + " \"TargetUserName\",\n", + " \"TargetDomainName\",\n", + " \"SubjectUserName\",\n", + " \"SubjectDomainName\",\n", + " \"LogonType\",\n", + " \"IpAddress\",\n", + " ]\n", + " nbdisplay.display_timeline(\n", + " data=host_logons,\n", + " group_by=\"TargetUserName\",\n", + " source_columns=tooltip_cols,\n", + " legend=\"right\", yaxis=True\n", + " )\n", + "\n", + " display(Markdown(\"### Counts of logon events by logon type.\"))\n", + " display(Markdown(\"Min counts for each logon type highlighted.\"))\n", + " logon_by_type = (\n", + " host_logons[[\"Account\", \"LogonType\", \"EventID\"]]\n", + " .astype({'LogonType': 'int32'})\n", + " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", + " left_on=\"LogonType\", right_index=True)\n", + " .drop(columns=\"LogonType\")\n", + " .groupby([\"Account\", \"LogonTypeDesc\"])\n", + " .count()\n", + " .unstack()\n", + " .rename(columns={\"EventID\": \"LogonCount\"})\n", + " .fillna(0)\n", + " .style\n", + " .background_gradient(cmap=\"viridis\", low=0.5, high=0)\n", + " .format(\"{0:0>3.0f}\")\n", + " )\n", + " display(logon_by_type)\n", + " else:\n", + " display(Markdown(\"No logon events found for host.\"))\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Failed Logons Timeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:06:01.493064Z", + "start_time": "2020-05-15T23:05:59.580819Z" + }, + "scrolled": true + }, + "outputs": [], + "source": [ + "if ipaddr_origin == \"Internal\":\n", + " failedLogons = qry_prov.WindowsSecurity.list_host_logon_failures(\n", + " logon_query_times, host_name=ip_entity.hostname\n", + " )\n", + " if failedLogons.empty:\n", + " print(\"No logon failures recorded for this host between \",\n", + " f\" {logon_query_times.start} and {logon_query_times.end}\"\n", + " )\n", + " else:\n", + " nbdisplay.display_timeline(\n", + " data=host_logons.query('TargetLogonId != \"0x3e7\"'),\n", + " overlay_data=failedLogons,\n", + " alert=related_alert,\n", + " title=\"Logons (blue=user-success, green=failed)\",\n", + " source_columns=tooltip_cols,\n", + " height=200,\n", + " )\n", + " display(failedLogons\n", + " .astype({'LogonType': 'int32'})\n", + " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", + " left_on=\"LogonType\", right_index=True)\n", + " [['Account', 'EventID', 'TimeGenerated',\n", + " 'Computer', 'SubjectUserName', 'SubjectDomainName',\n", + " 'TargetUserName', 'TargetDomainName',\n", + " 'LogonTypeDesc','IpAddress', 'WorkstationName'\n", + " ]])\n", + "else:\n", + " md(f'Analysis section Not Applicable since IP address owner is {ipaddr_origin}', styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-08-30T15:52:54.700099Z", + "start_time": "2019-08-30T15:52:54.661189Z" + } + }, + "source": [ + "[Contents](#toc)\n", + "## Network Connection Analysis\n", + "\n", + "**Hypothesis:** That an attacker is remotely communicating with the host in order to compromise the host or for outbound communication to C2 for data exfiltration purposes after compromising the host.\n", + "\n", + "This section provides an overview of network activity to and from the host during hunting time frame, the purpose of this is for the identification of anomalous network traffic. If you wish to investigate a specific IP in detail it is recommended that to use another instance of this notebook with each IP addresses.\n", + "\n", + "> Note: this query can return a lot of data for active hosts\n", + "> If your query times out, try reducing the time range, breaking the analysis\n", + "> into chunks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Network Check Communications with Other Hosts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:06:06.486183Z", + "start_time": "2020-05-15T23:06:06.429184Z" + } + }, + "outputs": [], + "source": [ + "ip_q_times = nbwidgets.QueryTime(\n", + " label=\"Set time bounds for network queries\",\n", + " units=\"hour\",\n", + " max_before=120,\n", + " before=5,\n", + " after=5,\n", + " max_after=60,\n", + " origin_time=logon_query_times.origin_time\n", + ")\n", + "ip_q_times.display()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Query Flows by IP Address" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:06:22.160247Z", + "start_time": "2020-05-15T23:06:10.292782Z" + } + }, + "outputs": [], + "source": [ + "if \"AzureNetworkAnalytics_CL\" not in available_datasets:\n", + " md_warn(\"No network flow data available.\")\n", + " md(\"Please skip the remainder of this section and go to [Time-Series-Anomalies](#Outbound-Data-transfer-Time-Series-Anomalies)\")\n", + " az_net_comms_df = None\n", + "else:\n", + " all_host_ips = (\n", + " ip_entity['private_ips'] + ip_entity['public_ips']\n", + " )\n", + " host_ips = [i.Address for i in all_host_ips]\n", + "\n", + " az_net_comms_df = qry_prov.Network.list_azure_network_flows_by_ip(\n", + " ip_q_times, ip_address_list=host_ips\n", + " )\n", + "\n", + " if isinstance(az_net_comms_df, pd.DataFrame) and not az_net_comms_df.empty:\n", + " az_net_comms_df['TotalAllowedFlows'] = az_net_comms_df['AllowedOutFlows'] + az_net_comms_df['AllowedInFlows']\n", + " nbdisplay.display_timeline(\n", + " data=az_net_comms_df,\n", + " group_by=\"L7Protocol\",\n", + " title=\"Network Flows by Protocol\",\n", + " time_column=\"FlowStartTime\",\n", + " source_columns=[\"FlowType\", \"AllExtIPs\", \"L7Protocol\", \"FlowDirection\"],\n", + " height=300,\n", + " legend=\"right\",\n", + " yaxis=True\n", + " )\n", + " nbdisplay.display_timeline(\n", + " data=az_net_comms_df,\n", + " group_by=\"FlowDirection\",\n", + " title=\"Network Flows by Direction\",\n", + " time_column=\"FlowStartTime\",\n", + " source_columns=[\"FlowType\", \"AllExtIPs\", \"L7Protocol\", \"FlowDirection\"],\n", + " height=300,\n", + " legend=\"right\",\n", + " yaxis=True\n", + " )\n", + " else:\n", + " md_warn(\"No network data for specified time range.\")\n", + " md(\"Please skip the remainder of this section and go to [Time-Series-Anomalies](#Outbound-Data-transfer-Time-Series-Anomalies)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:06:50.373391Z", + "start_time": "2020-05-15T23:06:50.084392Z" + } + }, + "outputs": [], + "source": [ + "try:\n", + " flow_plot = nbdisplay.display_timeline_values(\n", + " data=az_net_comms_df,\n", + " group_by=\"L7Protocol\",\n", + " source_columns=[\"FlowType\", \n", + " \"AllExtIPs\", \n", + " \"L7Protocol\", \n", + " \"FlowDirection\", \n", + " \"TotalAllowedFlows\"],\n", + " time_column=\"FlowStartTime\",\n", + " y=\"TotalAllowedFlows\",\n", + " legend=\"right\",\n", + " height=500,\n", + " kind=[\"vbar\", \"circle\"],\n", + " );\n", + "except NameError as err:\n", + " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:06:55.928554Z", + "start_time": "2020-05-15T23:06:55.790553Z" + } + }, + "outputs": [], + "source": [ + "try:\n", + " if az_net_comms_df is not None and not az_net_comms_df.empty:\n", + " cm = sns.light_palette(\"green\", as_cmap=True)\n", + "\n", + " cols = [\n", + " \"VMName\",\n", + " \"VMIPAddress\",\n", + " \"PublicIPs\",\n", + " \"SrcIP\",\n", + " \"DestIP\",\n", + " \"L4Protocol\",\n", + " \"L7Protocol\",\n", + " \"DestPort\",\n", + " \"FlowDirection\",\n", + " \"AllExtIPs\",\n", + " \"TotalAllowedFlows\",\n", + " ]\n", + " flow_index = az_net_comms_df[cols].copy()\n", + "\n", + " def get_source_ip(row):\n", + " if row.FlowDirection == \"O\":\n", + " return row.VMIPAddress if row.VMIPAddress else row.SrcIP\n", + " else:\n", + " return row.AllExtIPs if row.AllExtIPs else row.DestIP\n", + "\n", + " def get_dest_ip(row):\n", + " if row.FlowDirection == \"O\":\n", + " return row.AllExtIPs if row.AllExtIPs else row.DestIP\n", + " else:\n", + " return row.VMIPAddress if row.VMIPAddress else row.SrcIP\n", + " \n", + " flow_index[\"source\"] = flow_index.apply(get_source_ip, axis=1)\n", + " flow_index[\"dest\"] = flow_index.apply(get_dest_ip, axis=1)\n", + " display(flow_index)\n", + "\n", + " # Uncomment to view flow_index results\n", + " # with warnings.catch_warnings():\n", + " # warnings.simplefilter(\"ignore\")\n", + " # display(\n", + " # flow_index[\n", + " # [\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\", \"TotalAllowedFlows\"]\n", + " # ]\n", + " # .groupby([\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\"])\n", + " # .sum()\n", + " # .reset_index()\n", + " # .style.bar(subset=[\"TotalAllowedFlows\"], color=\"#d65f5f\")\n", + " # )\n", + "except NameError as err:\n", + " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "### Bulk whois lookup " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:08:00.744206Z", + "start_time": "2020-05-15T23:07:01.493951Z" + } + }, + "outputs": [], + "source": [ + "# Bulk WHOIS lookup function\n", + "from functools import lru_cache\n", + "from ipwhois import IPWhois\n", + "from ipaddress import ip_address\n", + "\n", + "try:\n", + " # Add ASN informatio from Whois\n", + " flows_df = (\n", + " flow_index[[\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\", \"TotalAllowedFlows\"]]\n", + " .groupby([\"source\", \"dest\", \"L7Protocol\", \"FlowDirection\"])\n", + " .sum()\n", + " .reset_index()\n", + " )\n", + "\n", + " num_ips = len(flows_df[\"source\"].unique()) + len(flows_df[\"dest\"].unique())\n", + " print(f\"Performing WhoIs lookups for {num_ips} IPs \", end=\"\")\n", + " #flows_df = flows_df.assign(DestASN=\"\", DestASNFull=\"\", SourceASN=\"\", SourceASNFull=\"\")\n", + " flows_df[\"DestASN\"] = flows_df.apply(lambda x: get_whois_info(x.dest, True), axis=1)\n", + " flows_df[\"SourceASN\"] = flows_df.apply(lambda x: get_whois_info(x.source, True), axis=1)\n", + " print(\"done\")\n", + "\n", + " # Split the tuple returned by get_whois_info into separate columns\n", + " flows_df[\"DestASNFull\"] = flows_df.apply(lambda x: x.DestASN[1], axis=1)\n", + " flows_df[\"DestASN\"] = flows_df.apply(lambda x: x.DestASN[0], axis=1)\n", + " flows_df[\"SourceASNFull\"] = flows_df.apply(lambda x: x.SourceASN[1], axis=1)\n", + " flows_df[\"SourceASN\"] = flows_df.apply(lambda x: x.SourceASN[0], axis=1)\n", + "\n", + " our_host_asns = [get_whois_info(ip.Address)[0] for ip in ip_entity.public_ips]\n", + " md(f\"Host {ip_entity.hostname} ASNs:\", \"bold\")\n", + " md(str(our_host_asns))\n", + "\n", + " flow_sum_df = flows_df.groupby([\"DestASN\", \"SourceASN\"]).agg(\n", + " TotalAllowedFlows=pd.NamedAgg(column=\"TotalAllowedFlows\", aggfunc=\"sum\"),\n", + " L7Protocols=pd.NamedAgg(column=\"L7Protocol\", aggfunc=lambda x: x.unique().tolist()),\n", + " source_ips=pd.NamedAgg(column=\"source\", aggfunc=lambda x: x.unique().tolist()),\n", + " dest_ips=pd.NamedAgg(column=\"dest\", aggfunc=lambda x: x.unique().tolist()),\n", + " ).reset_index()\n", + " display(flow_sum_df)\n", + "except NameError as err:\n", + " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Choose ASNs/IPs to Check for Threat Intel Reports\n", + "Choose from the list of Selected ASNs for the IPs you wish to check on.\n", + "The Source list is been pre-populated with all ASNs found in the network flow summary.\n", + "\n", + "As an example, we've populated the `Selected` list with the ASNs that have the lowest number of flows to and from the host. We also remove the ASN that matches the ASN of the host we are investigating.\n", + "\n", + "Please edit this list, using flow summary data above as a guide and leaving only ASNs that you are suspicious about. Typicially these would be ones with relatively low `TotalAllowedFlows` and possibly with unusual `L7Protocols`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:08:01.347207Z", + "start_time": "2020-05-15T23:08:01.287206Z" + } + }, + "outputs": [], + "source": [ + "try:\n", + " if isinstance(flow_sum_df, pd.DataFrame) and not flow_sum_df.empty:\n", + " all_asns = list(flow_sum_df[\"DestASN\"].unique()) + list(flow_sum_df[\"SourceASN\"].unique())\n", + " all_asns = set(all_asns) - set([\"private address\"])\n", + "\n", + " # Select the ASNs in the 25th percentile (lowest number of flows)\n", + " quant_25pc = flow_sum_df[\"TotalAllowedFlows\"].quantile(q=[0.25]).iat[0]\n", + " quant_25pc_df = flow_sum_df[flow_sum_df[\"TotalAllowedFlows\"] <= quant_25pc]\n", + " other_asns = list(quant_25pc_df[\"DestASN\"].unique()) + list(quant_25pc_df[\"SourceASN\"].unique())\n", + " other_asns = set(other_asns) - set(our_host_asns)\n", + " md(\"Choose IPs from Selected ASNs to look up for Threat Intel.\", \"bold\")\n", + " sel_asn = nbwidgets.SelectSubset(source_items=all_asns, default_selected=other_asns)\n", + "except NameError as err:\n", + " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:08:14.516746Z", + "start_time": "2020-05-15T23:08:01.935205Z" + } + }, + "outputs": [], + "source": [ + "try:\n", + " if isinstance(flow_sum_df, pd.DataFrame) and not flow_sum_df.empty:\n", + " ti_lookup = TILookup()\n", + " from itertools import chain\n", + " dest_ips = set(chain.from_iterable(flow_sum_df[flow_sum_df[\"DestASN\"].isin(sel_asn.selected_items)][\"dest_ips\"]))\n", + " src_ips = set(chain.from_iterable(flow_sum_df[flow_sum_df[\"SourceASN\"].isin(sel_asn.selected_items)][\"source_ips\"]))\n", + " selected_ips = dest_ips | src_ips\n", + " print(f\"{len(selected_ips)} unique IPs in selected ASNs\")\n", + "\n", + " # Add the IoCType to save cost of inferring each item\n", + " selected_ip_dict = {ip: \"ipv4\" for ip in selected_ips}\n", + " ti_results = ti_lookup.lookup_iocs(data=selected_ip_dict)\n", + "\n", + " print(f\"{len(ti_results)} results received.\")\n", + "\n", + " # ti_results_pos = ti_results[ti_results[\"Severity\"] > 0]\n", + " #####\n", + " # WARNING - faking results for illustration purposes\n", + " #####\n", + " ti_results_pos = ti_results.sample(n=2)\n", + "\n", + " print(f\"{len(ti_results_pos)} positive results found.\")\n", + "\n", + "\n", + " if not ti_results_pos.empty:\n", + " src_pos = flows_df.merge(ti_results_pos, left_on=\"source\", right_on=\"Ioc\")\n", + " dest_pos = flows_df.merge(ti_results_pos, left_on=\"dest\", right_on=\"Ioc\")\n", + " ti_ip_results = pd.concat([src_pos, dest_pos])\n", + " md_warn(\"Positive Threat Intel Results found for the following flows\")\n", + " md(\"Please examine these IP flows using the IP Explorer notebook.\", \"bold, large\")\n", + " display(ti_ip_results)\n", + "except NameError as err:\n", + " md(f\"Error Occured, Make sure to execute previous cells in notebook: {err}\",styles=[\"bold\",\"red\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ### GeoIP Map of External IPs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:08:16.023912Z", + "start_time": "2020-05-15T23:08:15.611915Z" + } + }, + "outputs": [], + "source": [ + "iplocation = GeoLiteLookup()\n", + "def format_ip_entity(row, ip_col):\n", + " ip_entity = entities.IpAddress(Address=row[ip_col])\n", + " iplocation.lookup_ip(ip_entity=ip_entity)\n", + " ip_entity.AdditionalData[\"protocol\"] = row.L7Protocol\n", + " if \"severity\" in row:\n", + " ip_entity.AdditionalData[\"threat severity\"] = row[\"severity\"]\n", + " if \"Details\" in row:\n", + " ip_entity.AdditionalData[\"threat details\"] = row[\"Details\"]\n", + " return ip_entity\n", + "\n", + "# from msticpy.nbtools.foliummap import FoliumMap\n", + "folium_map = FoliumMap()\n", + "if az_net_comms_df is None or az_net_comms_df.empty:\n", + " print(\"No network flow data available.\")\n", + "else:\n", + " # Get the flow records for all flows not in the TI results\n", + " selected_out = flows_df[flows_df[\"DestASN\"].isin(sel_asn.selected_items)]\n", + " selected_out = selected_out[~selected_out[\"dest\"].isin(ti_ip_results[\"Ioc\"])]\n", + " if selected_out.empty:\n", + " ips_out = []\n", + " else:\n", + " ips_out = list(selected_out.apply(lambda x: format_ip_entity(x, \"dest\"), axis=1))\n", + " \n", + " selected_in = flows_df[flows_df[\"SourceASN\"].isin(sel_asn.selected_items)]\n", + " selected_in = selected_in[~selected_in[\"source\"].isin(ti_ip_results[\"Ioc\"])]\n", + " if selected_in.empty:\n", + " ips_in = []\n", + " else:\n", + " ips_in = list(selected_in.apply(lambda x: format_ip_entity(x, \"source\"), axis=1))\n", + "\n", + " ips_threats = list(ti_ip_results.apply(lambda x: format_ip_entity(x, \"Ioc\"), axis=1))\n", + "\n", + " display(HTML(\"

External IP Addresses communicating with host

\"))\n", + " display(HTML(\"Numbered circles indicate multiple items - click to expand\"))\n", + " display(HTML(\"Location markers:
Blue = outbound, Purple = inbound, Green = Host, Red = Threats\"))\n", + "\n", + " icon_props = {\"color\": \"green\"}\n", + " for ips in ip_entity.public_ips:\n", + " ips.AdditionalData[\"host\"] = ip_entity.hostname\n", + " folium_map.add_ip_cluster(ip_entities=ip_entity.public_ips, **icon_props)\n", + " icon_props = {\"color\": \"blue\"}\n", + " folium_map.add_ip_cluster(ip_entities=ips_out, **icon_props)\n", + " icon_props = {\"color\": \"purple\"}\n", + " folium_map.add_ip_cluster(ip_entities=ips_in, **icon_props)\n", + " icon_props = {\"color\": \"red\"}\n", + " folium_map.add_ip_cluster(ip_entities=ips_threats, **icon_props)\n", + " \n", + " display(folium_map)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-09-05T18:03:37.980223Z", + "start_time": "2019-09-05T18:03:37.804856Z" + } + }, + "source": [ + "[Contents](#toc)\n", + "### Outbound Data transfer Time Series Anomalies" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section will look into the network datasources to check outbound data transfer trends. \n", + "You can also use time series analysis using below built-in KQL query example to analyze anamalous data transfer trends.below example shows sample dataset trends comparing with actual vs baseline traffic trends." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-05-15T23:08:42.937737Z", + "start_time": "2020-05-15T23:08:41.794266Z" + } + }, + "outputs": [], + "source": [ + "if \"VMConnection\" in table_index or \"CommonSecurityLog\" in table_index:\n", + " # KQL query for full text search of IP address and display all datatypes\n", + " dataxfer_stats = \"\"\"\n", + " union isfuzzy=true\n", + " (\n", + " CommonSecurityLog \n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end})\n", + " | where isnotempty(DestinationIP) and isnotempty(SourceIP)\n", + " | where SourceIP == \\'{ip_address}\\'\n", + " | extend SentBytesinKB = (SentBytes / 1024), ReceivedBytesinKB = (ReceivedBytes / 1024)\n", + " | summarize DailyCount = count(), ListOfDestPorts = make_set(DestinationPort), TotalSentBytesinKB = sum(SentBytesinKB), TotalReceivedBytesinKB = sum(ReceivedBytesinKB) by SourceIP, DestinationIP, DeviceVendor, bin(TimeGenerated,1d)\n", + " | project DeviceVendor, TimeGenerated, SourceIP, DestinationIP, ListOfDestPorts, TotalSentBytesinKB, TotalReceivedBytesinKB \n", + " ),\n", + " (\n", + " VMConnection \n", + " | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end}) \n", + " | where isnotempty(DestinationIp) and isnotempty(SourceIp)\n", + " | where SourceIp == \\'{ip_address}\\'\n", + " | extend DeviceVendor = \"VMConnection\", SourceIP = SourceIp, DestinationIP = DestinationIp\n", + " | extend SentBytesinKB = (BytesSent / 1024), ReceivedBytesinKB = (BytesReceived / 1024)\n", + " | summarize DailyCount = count(), ListOfDestPorts = make_set(DestinationPort), TotalSentBytesinKB = sum(SentBytesinKB),TotalReceivedBytesinKB = sum(ReceivedBytesinKB) by SourceIP, DestinationIP, DeviceVendor, bin(TimeGenerated,1d)\n", + " | project DeviceVendor, TimeGenerated, SourceIP, DestinationIP, ListOfDestPorts, TotalSentBytesinKB, TotalReceivedBytesinKB \n", + " )\n", + " \"\"\".format(**ipaddr_query_params())\n", + "\n", + " dataxfer_stats_df = qry_prov.exec_query(dataxfer_stats)\n", + "\n", + "#Display result as transposed matrix of datatypes availabel to query for the query period\n", + "if len(dataxfer_stats_df) > 0:\n", + " md(\n", + " 'Data transfer daily stats for IP ::', styles=[\"bold\",\"green\"]\n", + " )\n", + " display(dataxfer_stats_df)\n", + "else:\n", + " md_warn(\n", + " f'No Data transfer logs found for the query period'\n", + " )\n", + " #####\n", + " # WARNING - faking results for illustration purposes\n", + " #####\n", + "md(\n", + " 'Visualizing time series data transfer on dummy dataset for demonstration ::', styles=[\"bold\",\"green\"]\n", + " )\n", + "\n", + "# Generating graph based on dummy dataset in custom table representing Flow records outbound data transfer\n", + "timechartquery = \"\"\"\n", + "let TimeSeriesData = PaloAltoBytesSent_CL\n", + "| extend TimeGenerated = todatetime(EventTime_s), TotalBytesSent = todouble(TotalBytesSent_s) \n", + "| summarize TimeGenerated=make_list(TimeGenerated, 10000),TotalBytesSent=make_list(TotalBytesSent, 10000) by deviceVendor_s\n", + "| project TimeGenerated, TotalBytesSent;\n", + "TimeSeriesData\n", + "| extend (baseline,seasonal,trend,residual) = series_decompose(TotalBytesSent)\n", + "| mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), baseline to typeof(long), seasonal to typeof(long), trend to typeof(long), residual to typeof(long)\n", + "| project TimeGenerated, TotalBytesSent, baseline\n", + "| render timechart with (title=\"Palo Alto Outbound Data Transfer Time Series decomposition\")\n", + "\"\"\"\n", + "%kql -query timechartquery" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### List of Suspicious Activities/ Observables/Hunting bookmarks\n", + "- Suspicious alerts for the IP\n", + "- Anamalous Failed Logon trend on few days at 04:00 AM\n", + "- Anamalous spike in traffic logs on http\n", + "- Positive TI Hit from Open source feeds.\n", + "- Unusual data transfer deviating from normal baseline." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[Contents](#toc)\n", + "## Appendices" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Available DataFrames" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2020-04-02T10:00:41.436112Z", + "start_time": "2020-04-02T10:00:41.426605Z" + } + }, + "outputs": [], + "source": [ + "print('List of current DataFrames in Notebook')\n", + "print('-' * 50)\n", + "current_vars = list(locals().keys())\n", + "for var_name in current_vars:\n", + " if isinstance(locals()[var_name], pd.DataFrame) and not var_name.startswith('_'):\n", + " print(var_name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Saving Data to Excel\n", + "To save the contents of a pandas DataFrame to an Excel spreadsheet\n", + "use the following syntax\n", + "```\n", + "writer = pd.ExcelWriter('myWorksheet.xlsx')\n", + "my_data_frame.to_excel(writer,'Sheet1')\n", + "writer.save()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "### `msticpyconfig.yaml` configuration File\n", + "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3.6", + "language": "python", + "name": "python36" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + }, + "latex_envs": { + "LaTeX_envs_menu_present": true, + "autoclose": false, + "autocomplete": true, + "bibliofile": "biblio.bib", + "cite_by": "apalike", + "current_citInitial": 1, + "eqLabelWithNumbers": true, + "eqNumInitial": 1, + "hotkeys": { + "equation": "Ctrl-E", + "itemize": "Ctrl-I" + }, + "labels_anchors": false, + "latex_user_defs": false, + "report_style_numbering": false, + "user_envs_cfg": false + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": true, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": true, + "toc_position": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "299px" + }, + "toc_section_display": true, + "toc_window_display": true + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "position": { + "height": "400px", + "left": "1549px", + "right": "20px", + "top": "120px", + "width": "351px" + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/Entity Explorer - Linux Host.ipynb b/Entity Explorer - Linux Host.ipynb index 316682b8..ce2f688d 100644 --- a/Entity Explorer - Linux Host.ipynb +++ b/Entity Explorer - Linux Host.ipynb @@ -8,7 +8,7 @@ "
\n", "  Details...\n", "\n", - " **Notebook Version:** 1.0
\n", + " **Notebook Version:** 1.1
\n", " **Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", " **Required Packages**: kqlmagic, msticpy, pandas, pandas_bokeh, numpy, matplotlib, networkx, seaborn, datetime, ipywidgets, ipython, dnspython, ipwhois, folium, maxminddb_geolite2
\n", " **Platforms Supported**:\n", @@ -31,7 +31,7 @@ }, "source": [ "

Table of Contents

\n", - "" + "" ] }, { @@ -73,8 +73,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-15T23:58:35.616818Z", - "start_time": "2020-05-15T23:58:35.383819Z" + "end_time": "2020-06-24T01:51:59.386590Z", + "start_time": "2020-06-24T01:51:55.136591Z" } }, "outputs": [], @@ -86,7 +86,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -103,20 +103,39 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "extra_imports = [\n", " \"msticpy.nbtools, observationlist\",\n", " \"msticpy.nbtools.foliummap, get_map_center\",\n", + " \"msticpy.common.exceptions, MsticpyException\",\n", + " \"msticpy.sectools.syslog_utils, create_host_record\",\n", + " \"msticpy.sectools.syslog_utils, cluster_syslog_logons_df\",\n", + " \"msticpy.sectools.syslog_utils, risky_sudo_sessions\",\n", + " \"msticpy.sectools.ip_utils, convert_to_ip_entities\",\n", + " \"msticpy.sectools, auditdextract\",\n", + " \"msticpy.sectools.cmd_line, risky_cmd_line\",\n", " \"pyvis.network, Network\",\n", " \"re\",\n", + " \"math, pi\",\n", " \"ipwhois, IPWhois\",\n", - " \"pandas_bokeh\",\n", + " \"bokeh.plotting, show\",\n", + " \"bokeh.plotting, Row\",\n", + " \"bokeh.models, ColumnDataSource\",\n", + " \"bokeh.models, FactorRange\",\n", + " \"bokeh.transform, factor_cmap\",\n", + " \"bokeh.transform, cumsum\",\n", " \"bokeh.palettes, viridis\",\n", " \"dns, reversename\",\n", - " \"dns, resolver\"\n", + " \"dns, resolver\",\n", + " \"ipaddress, ip_address\",\n", + " \"functools, lru_cache\",\n", + " \"datetime,,dt\"\n", "]\n", "additional_packages = [\n", - " \"oauthlib\", \"pyvis\", \"python-whois\", \"pandas_bokeh\"\n", + " \"oauthlib\", \"pyvis\", \"python-whois\"\n", "]\n", "nbinit.init_notebook(\n", " namespace=globals(),\n", @@ -128,12 +147,7 @@ " \"layout\": widgets.Layout(width=\"95%\"),\n", " \"style\": {\"description_width\": \"initial\"},\n", "}\n", - "\n", - "from msticpy.sectools import auditdextract\n", - "from msticpy.sectools.cmd_line import *\n", - "from msticpy.sectools.ip_utils import convert_to_ip_entities\n", - "from msticpy.sectools.syslog_utils import *\n", - "from msticpy.sectools.syslog_utils import create_host_record, cluster_syslog_logons_df, risky_sudo_sessions\n" + "from bokeh.plotting import figure" ] }, { @@ -173,8 +187,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-15T23:58:49.319665Z", - "start_time": "2020-05-15T23:58:49.309664Z" + "end_time": "2020-06-24T01:51:59.434663Z", + "start_time": "2020-06-24T01:51:59.420592Z" } }, "outputs": [], @@ -185,12 +199,12 @@ "try:\n", " ws_id = ws_config['workspace_id']\n", " ten_id = ws_config['tenant_id']\n", - " display(HTML(\"Workspace details collected from config file\"))\n", + " md(\"Workspace details collected from config file\")\n", " config = True\n", "except:\n", - " display(HTML('Please go to your Log Analytics workspace, copy the workspace ID'\n", - " ' and/or tenant Id and paste here to enable connection to the workspace and querying of it..
'))\n", - " ws_id = mnbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", + " md('Please go to your Log Analytics workspace, copy the workspace ID'\n", + " ' and/or tenant Id and paste here to enable connection to the workspace and querying of it..
')\n", + " ws_id = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", " ten_id = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", @@ -202,8 +216,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-15T23:59:18.090694Z", - "start_time": "2020-05-15T23:58:52.515672Z" + "end_time": "2020-06-24T01:52:41.282988Z", + "start_time": "2020-06-24T01:52:00.925257Z" } }, "outputs": [], @@ -213,7 +227,7 @@ " ws_id = ws_id.value\n", " ten_id = ten_id.value\n", "qry_prov = QueryProvider('LogAnalytics')\n", - "qry_prov.connect(connection_str=ws_config.code_connect_str)\n" + "qry_prov.connect(connection_str=ws_config.code_connect_str)" ] }, { @@ -229,14 +243,14 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-15T23:59:18.217691Z", - "start_time": "2020-05-15T23:59:18.155693Z" + "end_time": "2020-06-24T01:52:41.392989Z", + "start_time": "2020-06-24T01:52:41.334990Z" } }, "outputs": [], "source": [ "query_times = nbwidgets.QueryTime(units='day',\n", - " max_before=20, max_after=1, before=3)\n", + " max_before=14, max_after=1, before=1)\n", "query_times.display()" ] }, @@ -251,53 +265,19 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:59:30.667719Z", - "start_time": "2020-05-15T23:59:30.645721Z" - } - }, - "outputs": [], - "source": [ - "host_text = widgets.Text(\n", - " description=\"Enter the Host name to search for:\", **WIDGET_DEFAULTS\n", - ")\n", - "display(host_text)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:59:47.945809Z", - "start_time": "2020-05-15T23:59:45.525517Z" - } - }, + "metadata": {}, "outputs": [], "source": [ - "hostname = None\n", - "items = []\n", - "hosts_query = f\"\"\" Syslog | where TimeGenerated between (datetime({query_times.start}) .. datetime({query_times.end})) \n", - " | where Computer contains \"{host_text.value}\" | distinct Computer | limit 490000\"\"\"\n", - "print(\"Collecting details on avaliable hosts...\")\n", - "hosts_df = qry_prov._query_provider.query(query=hosts_query)\n", - "if isinstance(hosts_df, pd.DataFrame) and not hosts_df.empty:\n", - " items = hosts_df[\"Computer\"].unique().tolist()\n", - "\n", - "if len(items) > 1:\n", - " print(f\"Multiple matches for '{host_text.value}'. Please select a host from the list.\")\n", - " choose_host = nbwidgets.SelectString(\n", - " item_list=items,\n", - " description=\"Select the host.\",\n", - " auto_display=True,\n", - " )\n", - " \n", - "elif not hosts_df.empty:\n", - " hostname = items[0]\n", - " md(f\"Unique host found: {hostname}\")\n", + "#Get a list of hosts with syslog data in our hunting timegframe to provide easy selection\n", + "syslog_query = f\"\"\"Syslog | where TimeGenerated between (datetime({query_times.start}) .. datetime({query_times.end})) | summarize by Computer\"\"\"\n", + "md(\"Collecting avaliable host details...\")\n", + "hosts_list = qry_prov._query_provider.query(query=syslog_query)\n", + "if isinstance(hosts_list, pd.DataFrame) and not hosts_list.empty:\n", + " hosts = hosts_list[\"Computer\"].unique().tolist()\n", + " host_text = nbwidgets.SelectString(description='Select host to investigate: ', \n", + " item_list=hosts, width='75%', auto_display=True)\n", "else:\n", - " md(f\"Host not found: {host_text.value}\")" + " display(md(\"There are no hosts with syslog data in this time period to investigate\"))" ] }, { @@ -311,91 +291,81 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T00:00:21.714769Z", - "start_time": "2020-05-15T23:59:56.711836Z" - } - }, + "metadata": {}, "outputs": [], "source": [ - "print(\"Collecting host details. This may take a few minutes...\")\n", - "if not hostname:\n", - " hostname = choose_host.value\n", + "hostname=host_text.value\n", + "az_net_df = None\n", "# Collect data on the host\n", - "syslog_query = f\"\"\" Syslog | where TimeGenerated between (datetime({query_times.start}) .. datetime({query_times.end})) \n", - " | where Computer contains \"{hostname}\" \"\"\"\n", - "all_syslog = qry_prov.exec_query(query=syslog_query)\n", - "syslog_data = all_syslog[all_syslog['Computer'] == f'{hostname}']\n", - "heartbeat_query = f\"\"\"Heartbeat | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end})| where Computer == '{hostname}' | top 1 by TimeGenerated desc nulls last\"\"\"\n", - "if \"AzureNetworkAnalytics_CL\" in qry_prov.schema:\n", - " aznet_query = f\"\"\"AzureNetworkAnalytics_CL | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end}) | where VirtualMachine_s has '{hostname}' | where ResourceType == 'NetworkInterface' | top 1 by TimeGenerated desc | project PrivateIPAddresses = PrivateIPAddresses_s, PublicIPAddresses = PublicIPAddresses_s\"\"\"\n", - " az_net_df = qry_prov.exec_query(query=aznet_query)\n", - "host_hb = qry_prov.exec_query(query=heartbeat_query)\n", - "\n", - "# Create host entity record, with Azure network data if any is avaliable\n", - "if isinstance(az_net_df, pd.DataFrame):\n", - " host_entity = create_host_record(\n", - " syslog_df=syslog_data, heartbeat_df=host_hb, az_net_df=az_net_df)\n", + "all_syslog_query = f\"Syslog | where TimeGenerated between (datetime({query_times.start}) .. datetime({query_times.end})) | where Computer =~ '{hostname}'\"\"\"\n", + "all_syslog_data = qry_prov.exec_query(all_syslog_query)\n", + "if isinstance(all_syslog_data, pd.DataFrame) and not all_syslog_data.empty:\n", + " heartbeat_query = f\"\"\"Heartbeat | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end})| where Computer == '{hostname}' | top 1 by TimeGenerated desc nulls last\"\"\"\n", + " if \"AzureNetworkAnalytics_CL\" in qry_prov.schema:\n", + " aznet_query = f\"\"\"AzureNetworkAnalytics_CL | where TimeGenerated >= datetime({query_times.start}) | where TimeGenerated <= datetime({query_times.end}) | where VirtualMachine_s has '{hostname}' | where ResourceType == 'NetworkInterface' | top 1 by TimeGenerated desc | project PrivateIPAddresses = PrivateIPAddresses_s, PublicIPAddresses = PublicIPAddresses_s\"\"\"\n", + " print(\"Getting network data...\")\n", + " az_net_df = qry_prov.exec_query(query=aznet_query)\n", + " print(\"Getting host data...\")\n", + " host_hb = qry_prov.exec_query(query=heartbeat_query)\n", + "\n", + " # Create host entity record, with Azure network data if any is avaliable\n", + " if az_net_df is not None and isinstance(az_net_df, pd.DataFrame) and not az_net_df.empty:\n", + " host_entity = create_host_record(syslog_df=all_syslog_data, heartbeat_df=host_hb, az_net_df=az_net_df)\n", + " else:\n", + " host_entity = create_host_record(syslog_df=all_syslog_data, heartbeat_df=host_hb)\n", + "\n", + " md(\n", + " \"Host Details
\"\n", + " f\"Hostname: {host_entity.computer}
\"\n", + " f\"OS: {host_entity.OSType} {host_entity.OSName}
\"\n", + " f\"IP Address: {host_entity.IPAddress.Address}
\"\n", + " f\"Location: {host_entity.IPAddress.Location.CountryName}
\"\n", + " f\"Installed Applications: {host_entity.Applications}
\"\n", + " )\n", "else:\n", - " host_entity = create_host_record(\n", - " syslog_df=syslog_data, heartbeat_df=host_hb)\n", - "\n", - "display(\n", - " Markdown(\n", - " \"***Host Details***\\n\\n\"\n", - " f\"**Hostname**: {host_entity.computer} \\n\\n\"\n", - " f\"**OS**: {host_entity.OSType} {host_entity.OSName}\\n\\n\"\n", - " f\"**IP Address**: {host_entity.IPAddress.Address}\\n\\n\"\n", - " f\"**Location**: {host_entity.IPAddress.Location.CountryName}\\n\\n\"\n", - " f\"**Installed Applications**: {host_entity.Applications}\\n\\n\"\n", - " )\n", - ")\n", - "rel_alert_select = None\n", - "sudo_events = None" + " md_warn(\"No Syslog data found, check hostname and timeframe.\")\n", + " md(\"The data query may be timing out, consider reducing the timeframe size.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Host Alerts\n", - "This section provides an overview of any security alerts in Azure Sentinel related to this host, this will help scope and guide our hunt." + "### Host Alerts & Bookmarks\n", + "This section provides an overview of any security alerts or Hunting Bookmarks in Azure Sentinel related to this host, this will help scope and guide our hunt." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T00:00:27.549794Z", - "start_time": "2020-05-16T00:00:25.664615Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "related_alerts = qry_prov.SecurityAlert.list_related_alerts(\n", " query_times, host_name=hostname)\n", - "\n", + "realted_bookmarks = qry_prov.AzureSentinel.list_bookmarks_for_entity(query_times, entity_id=hostname)\n", "if isinstance(related_alerts, pd.DataFrame) and not related_alerts.empty:\n", " host_alert_items = (related_alerts[['AlertName', 'TimeGenerated']]\n", " .groupby('AlertName').TimeGenerated.agg('count').to_dict())\n", "\n", " def print_related_alerts(alertDict, entityType, entityName):\n", " if len(alertDict) > 0:\n", - " md(f\"### Found {len(alertDict)} different alert types related to this {entityType} (\\'{entityName}\\')\")\n", + " md(f\"Found {len(alertDict)} different alert types related to this {entityType} (\\'{entityName}\\')\")\n", " for (k, v) in alertDict.items():\n", " md(f\"- {k}, Count of alerts: {v}\")\n", " else:\n", " md(f\"No alerts for {entityType} entity \\'{entityName}\\'\")\n", "\n", - "\n", - "# Display alerts on timeline to aid in visual grouping\n", " print_related_alerts(host_alert_items, 'host', host_entity.HostName)\n", - " x = nbdisplay.display_timeline(\n", + " nbdisplay.display_timeline(\n", " data=related_alerts, source_columns=[\"AlertName\"], title=\"Host alerts over time\", height=300, color=\"red\")\n", "else:\n", - " md('No related alerts found.')" + " md('No related alerts found.')\n", + " \n", + "if isinstance(realted_bookmarks, pd.DataFrame) and not realted_bookmarks.empty:\n", + " nbdisplay.display_timeline(data=realted_bookmarks, source_columns=[\"BookmarkName\"], height=200, color=\"orange\", title=\"Host bookmarks over time\",)\n", + "else:\n", + " md('No related bookmarks found.')" ] }, { @@ -403,8 +373,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:00:31.720767Z", - "start_time": "2020-05-16T00:00:31.664768Z" + "end_time": "2020-06-24T01:53:31.887372Z", + "start_time": "2020-06-24T01:53:31.826372Z" } }, "outputs": [], @@ -421,7 +391,7 @@ "if isinstance(related_alerts, pd.DataFrame) and not related_alerts.empty:\n", " related_alerts['CompromisedEntity'] = related_alerts['Computer']\n", " md('### Click on alert to view details.')\n", - " rel_alert_select = nbwidgets.AlertSelector(alerts=related_alerts,\n", + " rel_alert_select = nbwidgets.SelectAlert(alerts=related_alerts,\n", " action=show_full_alert)\n", " rel_alert_select.display()\n", "else:\n", @@ -441,8 +411,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:01:56.991980Z", - "start_time": "2020-05-16T00:01:56.936979Z" + "end_time": "2020-06-24T01:53:32.233372Z", + "start_time": "2020-06-24T01:53:32.172372Z" } }, "outputs": [], @@ -453,8 +423,8 @@ " start = rel_alert_select.selected_alert['TimeGenerated']\n", "\n", "# Set new investigation time windows based on the selected alert\n", - "invest_times = nbwidgets.QueryTime(units='hours',\n", - " max_before=24, max_after=12, before=6, origin_time=start)\n", + "invest_times = nbwidgets.QueryTime(\n", + " units='day', max_before=24, max_after=12, before=1, after=1, origin_time=start)\n", "invest_times.display()" ] }, @@ -472,6 +442,27 @@ "You can choose to start below with a hunt in host logon events or choose to jump to one of the other sections listed above. The order in which you choose to run each of these major sections doesn't matter, they are each self contained. You may also choose to rerun sections based on your findings from running other sections." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook uses external threat intelligence sources to enrich data. The next cell loads the TILookup class.\n", + "> **Note**: to use TILookup you will need configuration settings in your msticpyconfig.yaml\n", + ">
see [TIProviders documenation](https://msticpy.readthedocs.io/en/latest/TIProviders.html)\n", + ">
and [Configuring Notebook Environment notebook](./ConfiguringNotebookEnvironment.ipynb)\n", + ">
or [ConfiguringNotebookEnvironment (GitHub static view)](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tilookup = TILookup()\n", + "md(\"Threat intelligence provider loading complete.\")" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -485,89 +476,91 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T00:02:12.485265Z", - "start_time": "2020-05-16T00:02:10.553617Z" - } - }, + "metadata": {}, "outputs": [], "source": [ + "\n", "# Collect logon events for this, seperate them into sucessful and unsucessful and cluster sucessful one into sessions\n", - "logon_events = qry_prov.LinuxSyslog.user_logon(invest_times, host_name=hostname)\n", + "logon_events = qry_prov.LinuxSyslog.user_logon(start=invest_times.start, end=invest_times.end, host_name=hostname)\n", "remote_logons = None\n", "failed_logons = None\n", - "logon_sessions_df = None\n", + "\n", "if isinstance(logon_events, pd.DataFrame) and not logon_events.empty:\n", - " try:\n", - " remote_logons = (logon_events[logon_events['LogonResult'] == 'Success'])\n", - " failed_logons = (logon_events[logon_events['LogonResult'] == 'Failure'])\n", - " logon_sessions_df = cluster_syslog_logons_df(logon_events)\n", - " except:\n", - " print(\"No logon sessions in this timeframe\")\n", + " remote_logons = (logon_events[logon_events['LogonResult'] == 'Success'])\n", + " failed_logons = (logon_events[logon_events['LogonResult'] == 'Failure'])\n", "else:\n", " print(\"No logon events in this timeframe\")\n", "\n", "\n", - "if (remote_logons is not None and not remote_logons.empty) or (failed_logons is not None and not failed_logons.empty):\n", - " # Provide a timeline of sucessful and failed logon attempts to aid identification of potential brute force attacks\n", + "if not remote_logons.empty or not failed_logons.empty:\n", + "#Provide a timeline of sucessful and failed logon attempts to aid identification of potential brute force attacks\n", " display(Markdown('### Timeline of sucessful host logons.'))\n", - " tl_data = {\"Remote Logons\": {\"data\": remote_logons, \"source_columns\": ['User', 'ProcessName', 'SourceIP'], \"color\": \"Green\"},\n", - " \"Failed Logons\": {\"data\": failed_logons, \"source_columns\": ['User', 'ProcessName', 'SourceIP'], \"time_column\": \"TimeGenerated\", \"color\": \"Red\"}}\n", - " logon_timeline = nbdisplay.display_timeline(\n", - " data=tl_data, height=300, alert=rel_alert_select.selected_alert)\n", - " palette = viridis(5)\n", - " # Graph out failed/sucessful logons by account and by logon process\n", - " all_df = pd.DataFrame(dict(successful=remote_logons['ProcessName'].value_counts(\n", - " ), failed=failed_logons['ProcessName'].value_counts())).fillna(0)\n", - " fail_data = pd.value_counts(failed_logons['User'].values, sort=True).head(\n", - " 10).reset_index(name='value').rename(columns={'User': 'Count'})\n", - " fail_pie = None\n", - " sucess_pie = None\n", - " if not fail_data.empty:\n", - " fail_pie = fail_data.plot_bokeh.pie(x='index', y=\"value\", colormap=palette,\n", - " show_figure=False, title=\"Relative Frequencies of Failed Logons by Account\")\n", - " sucess_data = pd.value_counts(remote_logons['User'].values, sort=False).reset_index(\n", - " name='value').rename(columns={'User': 'Count'})\n", - " if not sucess_data.empty:\n", - " sucess_pie = sucess_data.plot_bokeh.pie(x='index', colormap=palette, y=\"value\",\n", - " show_figure=False, title=\"Relative Frequencies of Sucessful Logons by Account\")\n", + " tooltip_cols = ['User', 'ProcessName', 'SourceIP']\n", + " if rel_alert_select is not None:\n", + " logon_timeline = nbdisplay.display_timeline(data=remote_logons, overlay_data=failed_logons, source_columns=tooltip_cols, height=200, overlay_color=\"red\", alert = rel_alert_select.selected_alert)\n", + " else:\n", + " logon_timeline = nbdisplay.display_timeline(data=remote_logons, overlay_data=failed_logons, source_columns=tooltip_cols, height=200, overlay_color=\"red\")\n", + " display(Markdown('Key:

Sucessful logons

Failed Logon Attempts (via su)

')) \n", + "\n", + " all_df = pd.DataFrame(dict(successful= remote_logons['ProcessName'].value_counts(), failed = failed_logons['ProcessName'].value_counts())).fillna(0)\n", + " fail_data = pd.value_counts(failed_logons['User'].values, sort=True).head(10).reset_index(name='value').rename(columns={'User':'Count'})\n", + " fail_data['angle'] = fail_data['value']/fail_data['value'].sum() * 2*pi\n", + " fail_data['color'] = viridis(len(fail_data))\n", + " fp = figure(plot_height=350, plot_width=450, title=\"Relative Frequencies of Failed Logons by Account\", toolbar_location=None, tools=\"hover\", tooltips=\"@index: @value\")\n", + " fp.wedge(x=0, y=1, radius=0.5, start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'), line_color=\"white\", fill_color='color', legend='index', source=fail_data)\n", + "\n", + " sucess_data = pd.value_counts(remote_logons['User'].values, sort=False).reset_index(name='value').rename(columns={'User':'Count'})\n", + " sucess_data['angle'] = sucess_data['value']/sucess_data['value'].sum() * 2*pi\n", + " sucess_data['color'] = viridis(len(sucess_data))\n", + " sp = figure(plot_height=350, width=450, title=\"Relative Frequencies of Sucessful Logons by Account\", toolbar_location=None, tools=\"hover\", tooltips=\"@index: @value\")\n", + " sp.wedge(x=0, y=1, radius=0.5, start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'), line_color=\"white\", fill_color='color', legend='index', source=sucess_data)\n", + "\n", + " fp.axis.axis_label=None\n", + " fp.axis.visible=False\n", + " fp.grid.grid_line_color = None\n", + " sp.axis.axis_label=None\n", + " sp.axis.visible=False\n", + " sp.grid.grid_line_color = None\n", + "\n", + "\n", " processes = all_df.index.values.tolist()\n", - " fail_sucess_data = pd.DataFrame({'processes': processes,\n", - " 'sucess': all_df['successful'].values.tolist(),\n", - " 'failure': all_df['failed'].values.tolist()})\n", - "\n", - " process_bar = fail_sucess_data.plot_bokeh.bar(\n", - " x=\"processes\", colormap=palette, show_figure=False, title=\"Failed and Sucessful logon attempts by process\")\n", - " pandas_bokeh.plot_grid(\n", - " [[fail_pie, sucess_pie], [process_bar]], plot_width=450, plot_height=300)\n", - "\n", - " # Convert logon IPs to IP entities in order to get location\n", - " ip_entity = entityschema.IpAddress()\n", - " #Is there a better way to do this rather than reseting the list each time.\n", - " ip_list = []\n", - " for ip_logon in remote_logons['SourceIP']:\n", - " ip_list.extend(convert_to_ip_entities(ip_logon))\n", - " ip_fail_list = []\n", - " for ip_fail in failed_logons['SourceIP']:\n", - " ip_fail_list.extend(convert_to_ip_entities(ip_fail))\n", - "\n", - " # Get center location of all IP locaitons to set map default\n", + " results = all_df.columns.values.tolist()\n", + " fail_sucess_data = {'processes' :processes,\n", + " 'sucess' : all_df['successful'].values.tolist(),\n", + " 'failure': all_df['failed'].values.tolist()}\n", + "\n", + " palette = viridis(2)\n", + " x = [ (process, result) for process in processes for result in results ]\n", + " counts = sum(zip(fail_sucess_data['sucess'], fail_sucess_data['failure']), ()) \n", + " source = ColumnDataSource(data=dict(x=x, counts=counts))\n", + " b = figure(x_range=FactorRange(*x), plot_height=350, plot_width=450, title=\"Failed and Sucessful logon attempts by process\",\n", + " toolbar_location=None, tools=\"\", y_minor_ticks=2)\n", + " b.vbar(x='x', top='counts', width=0.9, source=source, line_color=\"white\",\n", + " fill_color=factor_cmap('x', palette=palette, factors=results, start=1, end=2))\n", + " b.y_range.start = 0\n", + " b.x_range.range_padding = 0.1\n", + " b.xaxis.major_label_orientation = 1\n", + " b.xgrid.grid_line_color = None\n", + "\n", + " show(Row(sp,fp,b))\n", + "\n", + " ip_list = [convert_to_ip_entities(i)[0] for i in remote_logons['SourceIP']]\n", + " ip_fail_list = [convert_to_ip_entities(i)[0] for i in failed_logons['SourceIP']]\n", + " \n", " location = get_map_center(ip_list + ip_fail_list)\n", - " folium_map = FoliumMap(location=location, zoom_start=4)\n", - "\n", - " # Map logon locations to allow for identification of anomolous locations\n", + " folium_map = FoliumMap(location = location, zoom_start=1.4)\n", + " #Map logon locations to allow for identification of anomolous locations\n", " if len(ip_fail_list) > 0:\n", - " display(HTML('

Map of Originating Location of Logon Attempts

'))\n", + " md('

Map of Originating Location of Logon Attempts

')\n", " icon_props = {'color': 'red'}\n", " folium_map.add_ip_cluster(ip_entities=ip_fail_list, **icon_props)\n", " if len(ip_list) > 0:\n", " icon_props = {'color': 'green'}\n", " folium_map.add_ip_cluster(ip_entities=ip_list, **icon_props)\n", - " display(folium_map.folium_map)\n", - " display(Markdown('

Warning: the folium mapping library '\n", + " display(folium_map.folium_map)\n", + " md('

Warning: the folium mapping library '\n", " 'does not display correctly in some browsers.


'\n", - " 'If you see a blank image please retry with a different browser.'))" + " 'If you see a blank image please retry with a different browser.') \n" ] }, { @@ -583,22 +576,24 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:02:19.836782Z", - "start_time": "2020-05-16T00:02:19.732781Z" + "end_time": "2020-06-24T01:53:38.073770Z", + "start_time": "2020-06-24T01:53:37.978770Z" } }, "outputs": [], "source": [ - "import datetime as dt\n", - "def to_utc(time):\n", - " ts = (time - np.datetime64('1970-01-01T00:00:00')) / np.timedelta64(1, 's')\n", - " time = dt.datetime.utcfromtimestamp(ts) \n", - " return time\n", + "logon_sessions_df = None\n", + "try:\n", + " print(\"Clustering logon sessions...\")\n", + " logon_sessions_df = cluster_syslog_logons_df(logon_events)\n", + "except Exception as err:\n", + " print(f\"Error clustering logons: {err}\")\n", + "\n", "if logon_sessions_df is not None:\n", " logon_sessions_df[\"Alerts during session?\"] = np.nan\n", " # check if any alerts occur during logon window.\n", - " logon_sessions_df['Start (UTC)'] = [(to_utc(time) - dt.timedelta(seconds=5)) for time in logon_sessions_df['Start']]\n", - " logon_sessions_df['End (UTC)'] = [(to_utc(time) + dt.timedelta(seconds=5)) for time in logon_sessions_df['End']]\n", + " logon_sessions_df['Start (UTC)'] = [(time - dt.timedelta(seconds=5)) for time in logon_sessions_df['Start']]\n", + " logon_sessions_df['End (UTC)'] = [(time + dt.timedelta(seconds=5)) for time in logon_sessions_df['End']]\n", "\n", " for TimeGenerated in related_alerts['TimeGenerated']:\n", " logon_sessions_df.loc[(TimeGenerated >= logon_sessions_df['Start (UTC)']) & (TimeGenerated <= logon_sessions_df['End (UTC)']), \"Alerts during session?\"] = \"Yes\"\n", @@ -631,14 +626,16 @@ " display(logon_sessions_df[['User','Start (UTC)', 'End (UTC)', 'Alerts during session?', 'Sucessful to failed logon ratio', 'Root?']]\n", " .style.applymap(color_cells).hide_index())\n", "\n", - " logon_items = logon_sessions_df[['User','Start (UTC)', 'End (UTC)']].to_string(header=False,\n", - " index=False,\n", - " index_names=False).split('\\n')\n", - " logon_sessions_df[\"Key\"] = logon_items\n", + " logon_items = (\n", + " logon_sessions_df[['User','Start (UTC)', 'End (UTC)']]\n", + " .to_string(header=False, index=False, index_names=False)\n", + " .split('\\n')\n", + " )\n", + " logon_sessions_df[\"Key\"] = logon_items \n", " logon_sessions_df.set_index('Key', inplace=True)\n", " logon_dict = logon_sessions_df[['User','Start (UTC)', 'End (UTC)']].to_dict('index')\n", "\n", - " logon_selection = nbwidgets.SelectString(description='Select logon session to investigate: ',\n", + " logon_selection = nbwidgets.SelectItem(description='Select logon session to investigate: ',\n", " item_dict=logon_dict , width='80%', auto_display=True)\n", "else:\n", " md(\"No logon sessions during this timeframe\")" @@ -656,17 +653,16 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:02:33.394888Z", - "start_time": "2020-05-16T00:02:24.256128Z" + "end_time": "2020-06-24T01:53:44.059818Z", + "start_time": "2020-06-24T01:53:40.909226Z" } }, "outputs": [], "source": [ "def view_syslog(selected_facility):\n", - " display(syslog_events.query('Facility == @selected_facility'))\n", + " return [syslog_events.query('Facility == @selected_facility')]\n", "\n", "# Produce a summary of user modification actions taken\n", - "def action_count(x):\n", " if \"Add\" in x:\n", " return len(add_events.replace(\"\", np.nan).dropna(subset=['User'])['User'].unique().tolist())\n", " elif \"Modify\" in x:\n", @@ -675,6 +671,10 @@ " return len(del_events.replace(\"\", np.nan).dropna(subset=['User'])['User'].unique().tolist())\n", " else:\n", " return \"\"\n", + "\n", + "crn_tl_data = {}\n", + "user_tl_data = {}\n", + "sudo_tl_data = {}\n", "sudo_sessions = None\n", "tooltip_cols = ['SyslogMessage']\n", "if logon_sessions_df is not None:\n", @@ -686,31 +686,24 @@ " start=session.StartTimeUtc, end=session.EndTimeUtc, host_name=session.Host)\n", " sudo_events = qry_prov.LinuxSyslog.sudo_activity(\n", " start=session.StartTimeUtc, end=session.EndTimeUtc, host_name=session.Host, user=session.Account)\n", - "\n", + " \n", " if isinstance(sudo_events, pd.DataFrame) and not sudo_events.empty:\n", - " sudo_events[['Command', 'CommandCall']].replace('', np.nan, inplace=True)\n", " try:\n", - " sudo_sessions = cluster_syslog_logons_df(logon_events=(sudo_events))\n", - " except:\n", + " sudo_sessions = cluster_syslog_logons_df(logon_events=sudo_events)\n", + " except MsticpyException:\n", " pass\n", "\n", " # Display summary of cron activity in session\n", " cron_events = qry_prov.LinuxSyslog.cron_activity(\n", " start=session.StartTimeUtc, end=session.EndTimeUtc, host_name=session.Host)\n", - " if not isinstance(cron_events, pd.DataFrame):\n", - " display(HTML(\n", - " f'

No Cron activity for {session.Host} between {session.StartTimeUtc} and {session.EndTimeUtc}

'))\n", - " crn_tl_data = {}\n", + " if not isinstance(cron_events, pd.DataFrame) or cron_events.empty:\n", + " md(f'

No Cron activity for {session.Host} between {session.StartTimeUtc} and {session.EndTimeUtc}

')\n", " else:\n", - "\n", " cron_events['CMD'].replace('', np.nan, inplace=True)\n", - "\n", " crn_tl_data = {\"Cron Exections\": {\"data\": cron_events[['TimeGenerated', 'CMD', 'CronUser', 'SyslogMessage']].dropna(), \"source_columns\": tooltip_cols, \"color\": \"Blue\"},\n", " \"Cron Edits\": {\"data\": cron_events.loc[cron_events['SyslogMessage'].str.contains('EDIT')], \"source_columns\": tooltip_cols, \"color\": \"Green\"}}\n", - "\n", - " display(HTML('

Most common commands run by cron:

'))\n", - " display(HTML(\n", - " 'This shows how often each cron job was exected within the specified time window'))\n", + " md('

Most common commands run by cron:

')\n", + " md('This shows how often each cron job was exected within the specified time window')\n", " cron_commands = (cron_events[['EventTime', 'CMD']]\n", " .groupby(['CMD']).count()\n", " .dropna()\n", @@ -723,10 +716,8 @@ " # Display summary of user and group creations, deletions and modifications during the session\n", " user_activity = qry_prov.LinuxSyslog.user_group_activity(\n", " start=session.StartTimeUtc, end=session.EndTimeUtc, host_name=session.Host)\n", - "\n", - " if not isinstance(user_activity, pd.DataFrame) and not use_activity.empty:\n", - " display(HTML(\n", - " f' No user or group moidifcations for {session.Host} between {session.StartTimeUtc} and {session.EndTimeUtc}'))\n", + " if not isinstance(user_activity, pd.DataFrame) or user_activity.empty:\n", + " md(f'

No user or group moidifcations for {session.Host} between {session.StartTimeUtc} and {session.EndTimeUtc}>

')\n", " else:\n", " add_events = user_activity[user_activity['UserGroupAction'].str.contains(\n", " 'Add')]\n", @@ -736,12 +727,11 @@ " 'Modify')]\n", " user_activity['Count'] = user_activity.groupby('UserGroupAction')['UserGroupAction'].transform('count')\n", " if add_events.empty and del_events.empty and mod_events.empty:\n", - " display(HTML('

Users and groups added or deleted:'))\n", - " display(HTML(\n", - " f'No users or groups were added or deleted on {host_entity.HostName} between {query_times.start} and {query_times.end}'))\n", + " md('

Users and groups added or deleted:')\n", + " md(f'No users or groups were added or deleted on {host_entity.HostName} between {query_times.start} and {query_times.end}')\n", " user_tl_data = {}\n", " else:\n", - " display(HTML(\"

Users added, modified or deleted

\"))\n", + " md(\"

Users added, modified or deleted

\")\n", " display(user_activity[['UserGroupAction','Count']].drop_duplicates().style.hide_index())\n", " account_actions = pd.DataFrame({\"User Additions\": [add_events.replace(\"\", np.nan).dropna(subset=['User'])['User'].unique().tolist()],\n", " \"User Modifications\": [mod_events.replace(\"\", np.nan).dropna(subset=['User'])['User'].unique().tolist()],\n", @@ -750,54 +740,60 @@ " user_tl_data = {\"User adds\": {\"data\": add_events, \"source_columns\": tooltip_cols, \"color\": \"Orange\"},\n", " \"User deletes\": {\"data\": del_events, \"source_columns\": tooltip_cols, \"color\": \"Red\"},\n", " \"User modfications\": {\"data\": mod_events, \"source_columns\": tooltip_cols, \"color\": \"Grey\"}}\n", + " \n", " # Display sudo activity during session\n", - " if sudo_sessions is None:\n", - " md(f\"No Sudo sessions for {session.Host} between {logon_selection.value.get('Start (UTC)')} and {logon_selection.value.get('End (UTC)')}\")\n", - " sudo_tl_data = {}\n", + " if not isinstance(sudo_sessions, pd.DataFrame) or sudo_sessions.empty:\n", + " md(f\"

No Sudo sessions for {session.Host} between {logon_selection.value.get('Start (UTC)')} and {logon_selection.value.get('End (UTC)')}

\")\n", + " sudo_tl_data = {}\n", + " else:\n", + " sudo_start = sudo_events[sudo_events[\"SyslogMessage\"].str.contains(\n", + " \"pam_unix.+session opened\")].rename(columns={\"Sudoer\": \"User\"})\n", + " sudo_tl_data = {\"Host logons\": {\"data\": remote_logons, \"source_columns\": tooltip_cols, \"color\": \"Cyan\"},\n", + " \"Sudo sessions\": {\"data\": sudo_start, \"source_columns\": tooltip_cols, \"color\": \"Purple\"}}\n", + " try:\n", + " risky_actions = cmd_line.risky_cmd_line(events=sudo_events, log_type=\"Syslog\")\n", + " suspicious_events = cmd_speed(\n", + " cmd_events=sudo_events, time=60, events=2, cmd_field=\"Command\")\n", + " except:\n", + " risky_actions = None\n", + " suspicious_events = None\n", + " if risky_actions is None and suspicious_events is None:\n", + " pass\n", " else:\n", - " sudo_start = sudo_events[sudo_events[\"SyslogMessage\"].str.contains(\n", - " \"pam_unix.+session opened\")].rename(columns={\"Sudoer\": \"User\"})\n", - " sudo_tl_data = {\"Host logons\": {\"data\": remote_logons, \"source_columns\": tooltip_cols, \"color\": \"Cyan\"},\n", - " \"Sudo sessions\": {\"data\": sudo_start, \"source_columns\": tooltip_cols, \"color\": \"Purple\"}}\n", - " try:\n", - " risky_actions = cmd_line.risky_cmd_line(events=sudo_events, log_type=\"Syslog\")\n", - " suspicious_events = cmd_speed(\n", - " cmd_events=sudo_events, time=60, events=2, cmd_field=\"Command\")\n", - " except:\n", - " risky_actions = None\n", - " suspicious_events = None\n", - " if risky_actions is None and suspicious_events is None:\n", - " pass\n", + " risky_sessions = risky_sudo_sessions(\n", + " risky_actions=risky_actions, sudo_sessions=sudo_sessions, suspicious_actions=suspicious_events)\n", + " for key in risky_sessions:\n", + " if key in sudo_sessions:\n", + " sudo_sessions[f\"{key} - {risky_sessions[key]}\"] = sudo_sessions.pop(\n", + " key)\n", + " \n", + " if isinstance(sudo_events, pd.DataFrame):\n", + " sudo_events_val = sudo_events[['EventTime', 'CommandCall']][sudo_events['CommandCall']!=\"\"].dropna(how='any', subset=['CommandCall'])\n", + " if sudo_events_val.empty:\n", + " md(f\"No sucessful sudo activity for {hostname} between {logon_selection.value.get('Start (UTC)')} and {logon_selection.value.get('End (UTC)')}\")\n", " else:\n", - " risky_sessions = risky_sudo_sessions(\n", - " risky_actions=risky_actions, sudo_sessions=sudo_sessions, suspicious_actions=suspicious_events)\n", - " for key in risky_sessions:\n", - " if key in sudo_sessions:\n", - " sudo_sessions[f\"{key} - {risky_sessions[key]}\"] = sudo_sessions.pop(\n", - " key)\n", - "\n", - " if sudo_events.empty:\n", - " md(f\"No sucessful sudo activity for {hostname} between {logon_selection.value.get('Start (UTC)')} and {logon_selection.value.get('End (UTC)')}\")\n", + " sudo_events.replace(\"\", np.nan, inplace=True)\n", + " md('

Frequency of sudo commands

')\n", + " md('This shows how many times each command has been run with sudo. /bin/bash is usally associated with the use of \"sudo -i\"')\n", + " sudo_commands = (sudo_events[['EventTime', 'CommandCall']]\n", + " .groupby(['CommandCall'])\n", + " .count()\n", + " .dropna()\n", + " .style\n", + " .set_table_attributes('width=900px, text-align=center')\n", + " .background_gradient(cmap='Reds', low=.5, high=1)\n", + " .format(\"{0:0>3.0f}\"))\n", + " display(sudo_commands)\n", " else:\n", - " sudo_events.replace(\"\", np.nan, inplace=True)\n", - " display(HTML('

Frequency of sudo commands

'))\n", - " display(HTML('This shows how many times each command has been run with sudo. /bin/bash is usally associated with the use of \"sudo -i\"'))\n", - " sudo_commands = (sudo_events[['EventTime', 'CommandCall']]\n", - " .groupby(['CommandCall'])\n", - " .count()\n", - " .dropna()\n", - " .style\n", - " .set_table_attributes('width=900px, text-align=center')\n", - " .background_gradient(cmap='Reds', low=.5, high=1)\n", - " .format(\"{0:0>3.0f}\"))\n", - " display(sudo_commands)\n", + " md(f\"No sucessful sudo activity for {hostname} between {logon_selection.value.get('Start (UTC)')} and {logon_selection.value.get('End (UTC)')}\") \n", "\n", " # Display a timeline of all activity during session\n", " crn_tl_data.update(user_tl_data)\n", " crn_tl_data.update(sudo_tl_data)\n", - " display(HTML('

Session Timeline.

'))\n", - " nbdisplay.display_timeline(\n", - " data=crn_tl_data, title='Session Timeline', height=300)\n", + " if crn_tl_data:\n", + " md('

Session Timeline.

')\n", + " nbdisplay.display_timeline(\n", + " data=crn_tl_data, title='Session Timeline', height=300)\n", "else:\n", " md(\"No logon sessions during this timeframe\")" ] @@ -815,13 +811,13 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:04:55.506355Z", - "start_time": "2020-05-16T00:04:52.200963Z" + "end_time": "2020-06-24T01:53:47.432915Z", + "start_time": "2020-06-24T01:53:45.628367Z" } }, "outputs": [], "source": [ - "if logon_sessions_df is not None:\n", + "if isinstance(logon_sessions_df, pd.DataFrame) and not logon_sessions_df.empty:\n", " #Return syslog data and present it to the use for investigation\n", " session_syslog = qry_prov.LinuxSyslog.all_syslog(\n", " start=session.StartTimeUtc, end=session.EndTimeUtc, host_name=session.Host)\n", @@ -831,15 +827,13 @@ "\n", "\n", " def view_sudo(selected_cmd):\n", - " display(sudo_events.query('CommandCall == @selected_cmd')[\n", - " ['TimeGenerated', 'SyslogMessage', 'Sudoer', 'SudoTo', 'Command', 'CommandCall']])\n", + " return [sudo_events.query('CommandCall == @selected_cmd')[\n", + " ['TimeGenerated', 'SyslogMessage', 'Sudoer', 'SudoTo', 'Command', 'CommandCall']]]\n", "\n", " # Show syslog messages associated with selected sudo command\n", - " display(HTML(\"

View all messages assocated with a sudo command

\"))\n", + " md(\"

View all messages associated with a sudo command

\")\n", " items = sudo_events['CommandCall'].dropna().unique().tolist()\n", - " cmd_w = widgets.Dropdown(\n", - " options=items, description='Select sudo command facility to examine', disabled=False, **WIDGET_DEFAULTS)\n", - " display(widgets.interactive(view_sudo, selected_cmd=cmd_w))\n", + " display(nbwidgets.SelectItem(item_list=items, action=view_sudo))\n", "else:\n", " md(\"No logon sessions during this timeframe\")" ] @@ -849,19 +843,17 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:04:56.376353Z", - "start_time": "2020-05-16T00:04:56.284354Z" + "end_time": "2020-06-24T01:53:48.221915Z", + "start_time": "2020-06-24T01:53:48.175914Z" } }, "outputs": [], "source": [ - "if logon_sessions_df is not None:\n", + "if isinstance(logon_sessions_df, pd.DataFrame) and not logon_sessions_df.empty:\n", " # Display syslog messages from the session witht he facility selected\n", " items = syslog_events['Facility'].dropna().unique().tolist()\n", - " display(HTML(\"

View all messages assocated with a syslog facility

\"))\n", - " sess_w = widgets.Dropdown(\n", - " options=items, description='Select syslog facility to examine', disabled=False, **WIDGET_DEFAULTS)\n", - " display(widgets.interactive(view_syslog, selected_facility=sess_w))\n", + " md(\"

View all messages associated with a syslog facility

\")\n", + " display(nbwidgets.SelectItem(item_list=items, action=view_syslog))\n", "else:\n", " md(\"No logon sessions during this timeframe\")" ] @@ -878,13 +870,13 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:06.090478Z", - "start_time": "2020-05-16T00:04:57.481284Z" + "end_time": "2020-06-24T01:53:51.672525Z", + "start_time": "2020-06-24T01:53:50.175953Z" } }, "outputs": [], "source": [ - "if logon_sessions_df is not None:\n", + "if isinstance(logon_sessions_df, pd.DataFrame) and not logon_sessions_df.empty:\n", " display(HTML(\"

Process Trees from session

\"))\n", " print(\"Building process tree, this may take some time...\")\n", " # Find the table with auditd data in\n", @@ -934,8 +926,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:14.828475Z", - "start_time": "2020-05-16T00:05:14.794474Z" + "end_time": "2020-06-24T01:53:55.462637Z", + "start_time": "2020-06-24T01:53:55.422637Z" } }, "outputs": [], @@ -948,45 +940,26 @@ " sudo_sessions.set_index('Key', inplace=True)\n", " sudo_dict = sudo_sessions[['User','Start', 'End']].to_dict('index')\n", "\n", - " sudo_selection = nbwidgets.SelectString(description='Select sudo session to investigate: ',\n", + " sudo_selection = nbwidgets.SelectItem(description='Select sudo session to investigate: ',\n", " item_dict=sudo_dict, width='100%', height='300px', auto_display=True)\n", "else:\n", " sudo_selection = None\n", " md(\"No logon sessions during this timeframe\")" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Load TILookup class\n", - "> **Note**: to use TILookup you will need configuration settings in your msticpyconfig.yaml\n", - ">
see [TIProviders documenation](https://msticpy.readthedocs.io/en/latest/TIProviders.html)\n", - ">
and [Configuring Notebook Environment notebook](./ConfiguringNotebookEnvironment.ipynb)\n", - ">
or [ConfiguringNotebookEnvironment (GitHub static view)](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tilookup = TILookup()" - ] - }, { "cell_type": "code", "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:25.237213Z", - "start_time": "2020-05-16T00:05:21.813842Z" + "end_time": "2020-06-24T01:57:23.902023Z", + "start_time": "2020-06-24T01:57:21.856481Z" } }, "outputs": [], "source": [ "#Collect data associated with the sudo session selected\n", + "sudo_events = None\n", "from msticpy.sectools.tiproviders.ti_provider_base import TISeverity\n", "\n", "def ti_check_sev(severity, threshold):\n", @@ -1034,7 +1007,7 @@ " display(sudo_events[sudo_events['SyslogMessage'].str.contains(\n", " ioc)][['TimeGenerated', 'SyslogMessage']])\n", " else:\n", - " md(\"No IoC patterns found in Syslog Messages.\")\n", + " md(\"No IoC patterns found in Syslog Messages.\")\n", " else:\n", " md('No sudo messages for this session')\n", "\n", @@ -1074,8 +1047,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:40.283882Z", - "start_time": "2020-05-16T00:05:38.659404Z" + "end_time": "2020-06-24T01:57:32.366086Z", + "start_time": "2020-06-24T01:57:31.372985Z" } }, "outputs": [], @@ -1093,7 +1066,7 @@ "\n", "# Pick Users\n", "if not logon_events.empty:\n", - " user_select = nbwidgets.SelectString(description='Select user to investigate: ',\n", + " user_select = nbwidgets.SelectItem(description='Select user to investigate: ',\n", " item_list=all_users, width='75%', auto_display=True)\n", "else:\n", " md(\"There was no user activity in the timeframe specified.\")\n", @@ -1105,8 +1078,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:43.703387Z", - "start_time": "2020-05-16T00:05:41.635697Z" + "end_time": "2020-06-24T01:57:35.805460Z", + "start_time": "2020-06-24T01:57:33.955397Z" } }, "outputs": [], @@ -1114,18 +1087,18 @@ "folium_user_map = FoliumMap()\n", "\n", "def view_sudo(cmd):\n", - " display(user_sudo_hold.query('CommandCall == @cmd')[\n", - " ['TimeGenerated', 'HostName', 'Command', 'CommandCall', 'SyslogMessage']])\n", + " return [user_sudo_hold.query('CommandCall == @cmd')[\n", + " ['TimeGenerated', 'HostName', 'Command', 'CommandCall', 'SyslogMessage']]]\n", "user_sudo_hold = None\n", "if user_select is not None:\n", " # Get all syslog relating to these users\n", " username = user_select.value\n", - " user_events = all_syslog[all_syslog['SyslogMessage'].str.contains(username)]\n", + " user_events = all_syslog_data[all_syslog_data['SyslogMessage'].str.contains(username)]\n", " logon_sessions = cluster_syslog_logons_df(logon_events)\n", "\n", " # Display all logons associated with the user\n", - " display(HTML(f\"

User Logon Activity for {username}

\"))\n", - " user_logon_events = logon_events.loc[logon_events['User'] == username]\n", + " md(f\"

User Logon Activity for {username}

\")\n", + " user_logon_events = logon_events[logon_events['User'] == username]\n", " try:\n", " user_logon_sessions = cluster_syslog_logons_df(user_logon_events)\n", " except:\n", @@ -1141,7 +1114,7 @@ " for _, row in logon_sessions_df.iterrows():\n", " end = row['End']\n", " user_sudo_events = qry_prov.LinuxSyslog.sudo_activity(start=user_remote_logons.sort_values(\n", - " by='TimeGenerated')['TimeGenerated'].head(1).values[0], end=end, host_name=hostname, user=username)\n", + " by='TimeGenerated')['TimeGenerated'].iloc[0], end=end, host_name=hostname, user=username)\n", " else: \n", " user_sudo_events = None\n", "\n", @@ -1162,75 +1135,66 @@ "\n", " nbdisplay.display_timeline(\n", " data=user_tl_data, title=\"User logon timeline\", height=300)\n", + " \n", + " all_user_df = pd.DataFrame(dict(successful= user_remote_logons['ProcessName'].value_counts(), failed = user_failed_logons['ProcessName'].value_counts())).fillna(0)\n", + " processes = all_user_df.index.values.tolist()\n", + " results = all_user_df.columns.values.tolist()\n", + " user_fail_sucess_data = {'processes' :processes,\n", + " 'sucess' : all_user_df['successful'].values.tolist(),\n", + " 'failure': all_user_df['failed'].values.tolist()}\n", "\n", " palette = viridis(2)\n", - " # Graph out failed/sucessful logons by account and by logon process\n", - " all_user_df = pd.DataFrame(dict(successful=user_remote_logons['ProcessName'].value_counts(\n", - " ), failed=user_failed_logons['ProcessName'].value_counts())).fillna(0).T\n", - "\n", - " user_processes = all_user_df.columns.values.tolist()\n", - "\n", - " fail_sucess_user_data = pd.DataFrame({'processes': user_processes,\n", - " 'sucess': all_user_df.loc['successful'].values.tolist(),\n", - " 'failure': all_user_df.loc['failed'].astype(int).values.tolist()})\n", - "\n", - " user_process_bar = fail_sucess_user_data.plot_bokeh.bar(\n", - " x=\"processes\", colormap=palette, show_figure=False, title=\"Failed and Sucessful logon attempts by process\")\n", - " user_logons = pd.DataFrame({\"Sucessful Logons\" : [int(all_user_df.loc['successful'].sum())],\n", - " \"Failed Logons\" : [int(all_user_df.loc['failed'].sum())]}).T\n", - "\n", - " user_ratio_pie =user_logons.plot_bokeh.pie(colormap = palette,\n", - " show_figure = False, title = \"Relative Frequencies of Sucessful Logons by Account\")\n", - "\n", - " pandas_bokeh.plot_grid([[user_ratio_pie, user_process_bar], \n", - " []], plot_width = 450, plot_height = 300)\n", - "\n", - "\n", - " # Convert logon IPs to IP entities in order to get location\n", - " ip_entity = entityschema.IpAddress()\n", + " x = [ (process, result) for process in processes for result in results ]\n", + " counts = sum(zip(user_fail_sucess_data['sucess'], fail_sucess_data['failure']), ()) \n", + " source = ColumnDataSource(data=dict(x=x, counts=counts))\n", + " b = figure(x_range=FactorRange(*x), plot_height=350, plot_width=450, title=\"Failed and Sucessful logon attempts by process\",\n", + " toolbar_location=None, tools=\"\", y_minor_ticks=2)\n", + " b.vbar(x='x', top='counts', width=0.9, source=source, line_color=\"white\",\n", + " fill_color=factor_cmap('x', palette=palette, factors=results, start=1, end=2))\n", + " b.y_range.start = 0\n", + " b.x_range.range_padding = 0.1\n", + " b.xaxis.major_label_orientation = 1\n", + " b.xgrid.grid_line_color = None\n", + " user_logons = pd.DataFrame({\"Sucessful Logons\" : [int(all_user_df['successful'].sum())],\n", + " \"Failed Logons\" : [int(all_user_df['failed'].sum())]}).T\n", + " user_logon_data = pd.value_counts(user_logon_events['LogonResult'].values, sort=True).head(10).reset_index(name='value').rename(columns={'User':'Count'})\n", + " user_logon_data = user_logon_data[user_logon_data['index']!=\"Unknown\"].copy()\n", + " user_logon_data['angle'] = user_logon_data['value']/user_logon_data['value'].sum() * 2*pi\n", + " user_logon_data['color'] = viridis(len(user_logon_data))\n", + " p = figure(plot_height=350, plot_width=450, title=\"Relative Frequencies of Failed Logons by Account\", toolbar_location=None, tools=\"hover\", tooltips=\"@index: @value\")\n", + " p.axis.visible = False\n", + " p.xgrid.visible = False\n", + " p.ygrid.visible = False\n", + " p.wedge(x=0, y=1, radius=0.5, start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'), line_color=\"white\", fill_color='color', legend='index', source=user_logon_data)\n", + " show(Row(p,b)) \n", " \n", - " user_ip_list = []\n", - " for ip_logon in user_remote_logons['SourceIP']:\n", - " user_ip_list.extend(convert_to_ip_entities(ip_logon))\n", - " user_ip_fail_list = []\n", - " for ip_logon in user_failed_logons['SourceIP']:\n", - " user_ip_fail_list.extend(convert_to_ip_entities(ip_logon))\n", - " \n", - " folium_user_map=FoliumMap(location=location, zoom_start=3)\n", - " if not user_ip_list and not user_ip_fail_list:\n", - " print(\"No user events\")\n", - " elif not user_ip_list and user_ip_fail_list:\n", - " icon_props={'color': 'red'}\n", - " folium_user_map.add_ip_cluster(ip_entities=user_ip_fail_list, **icon_props)\n", - " elif not user_ip_fail_list and user_ip_list:\n", - " icon_props = {'color': 'green'}\n", - " folium_user_map.add_ip_cluster(ip_entities=user_ip_list, **icon_props)\n", - " else:\n", + " user_ip_list = [convert_to_ip_entities(i)[0] for i in user_remote_logons['SourceIP']]\n", + " user_ip_fail_list = [convert_to_ip_entities(i)[0] for i in user_failed_logons['SourceIP']]\n", + " \n", + " user_location = get_map_center(ip_list + ip_fail_list)\n", + " user_folium_map = FoliumMap(location = location, zoom_start=1.4)\n", + " #Map logon locations to allow for identification of anomolous locations\n", + " if len(ip_fail_list) > 0:\n", + " md('

Map of Originating Location of Logon Attempts

')\n", " icon_props = {'color': 'red'}\n", - " folium_user_map.add_ip_cluster(ip_entities=user_ip_fail_list, **icon_props)\n", + " user_folium_map.add_ip_cluster(ip_entities=user_ip_fail_list, **icon_props)\n", + " if len(ip_list) > 0:\n", " icon_props = {'color': 'green'}\n", - " folium_user_map.add_ip_cluster(ip_entities=user_ip_list, **icon_props)\n", - "\n", - " folium_user_map.center_map()\n", - " display(HTML('

Map of Originating Location of Logon Attempts

'))\n", - " display(folium_user_map)\n", - " display(Markdown('

Warning: the folium mapping library '\n", + " user_folium_map.add_ip_cluster(ip_entities=user_ip_list, **icon_props)\n", + " display(user_folium_map.folium_map)\n", + " md('

Warning: the folium mapping library '\n", " 'does not display correctly in some browsers.


'\n", - " 'If you see a blank image please retry with a different browser.'))\n", - "\n", - "\n", - "\n", + " 'If you see a blank image please retry with a different browser.') \n", + " \n", " #Display sudo activity of the user \n", " if not isinstance(user_sudo_events, pd.DataFrame) or user_sudo_events.empty:\n", - " display(HTML(f\"No sucessful sudo activity for {username}\"))\n", + " md(f\"

No sucessful sudo activity for {username}

\")\n", " else:\n", " user_sudo_hold = user_sudo_events\n", " user_sudo_commands = (user_sudo_events[['EventTime', 'CommandCall']].replace('', np.nan).groupby(['CommandCall']).count().dropna().style.set_table_attributes('width=900px, text-align=center').background_gradient(cmap='Reds', low=.5, high=1).format(\"{0:0>3.0f}\"))\n", " display(user_sudo_commands)\n", - " display(HTML(\"Select a sudo command to investigate in more detail\"))\n", - " cmd = widgets.Dropdown(options=user_sudo_events['CommandCall'].replace(\n", - " '', np.nan).dropna().unique().tolist(), description='Cmd:', disabled=False)\n", - " display(widgets.interactive(view_sudo, cmd=cmd))\n", + " md(\"Select a sudo command to investigate in more detail\")\n", + " display(nbwidgets.SelectItem(item_list=items, action=view_sudo))\n", "else:\n", " md(\"No user session selected\")" ] @@ -1240,15 +1204,15 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:50.586733Z", - "start_time": "2020-05-16T00:05:50.564733Z" + "end_time": "2020-06-24T01:57:41.495503Z", + "start_time": "2020-06-24T01:57:41.474501Z" } }, "outputs": [], "source": [ "# If the user has sudo activity extract and IOCs from the logs and look them up in TI feeds\n", - "if user_sudo_hold is not None or user_sudo_hold is not isinstance(user_sudo_hold, pd.DataFrame) or user_sudo_hold.empty:\n", - " print(f\"No sudo messages data\")\n", + "if not isinstance(user_sudo_hold, pd.DataFrame) or user_sudo_hold.empty:\n", + " md(f\"No sudo messages data\")\n", "else:\n", " # Extract IOCs\n", " ioc_extractor = iocextract.IoCExtract()\n", @@ -1260,7 +1224,7 @@ " ioc_types=['ipv4', 'ipv6', 'dns', 'url', 'md5_hash', 'sha1_hash', 'sha256_hash'])\n", " if len(ioc_df) > 0:\n", " ioc_count = len(ioc_df[[\"IoCType\", \"Observable\"]].drop_duplicates())\n", - " display(HTML(f\"Found {ioc_count} IOCs\"))\n", + " md(f\"Found {ioc_count} IOCs\")\n", " ti_resps = tilookup.lookup_iocs(data=ioc_df[[\"IoCType\", \"Observable\"]].drop_duplicates(\n", " ).reset_index(), obs_col='Observable', ioc_type_col='IoCType')\n", " i = 0\n", @@ -1272,13 +1236,13 @@ " i += 1\n", " else:\n", " i += 1\n", - " display(HTML(f\"Found {len(ti_hits)} IoCs in Threat Intelligence\"))\n", + " md(f\"Found {len(ti_hits)} IoCs in Threat Intelligence\")\n", " for ioc in ti_hits:\n", - " display(HTML(f\"Messages containing IoC found in TI feed: {ioc}\"))\n", + " md(f\"Messages containing IoC found in TI feed: {ioc}\")\n", " display(user_sudo_hold[user_sudo_hold['SyslogMessage'].str.contains(\n", " ioc)][['TimeGenerated', 'SyslogMessage']])\n", " else:\n", - " display(HTML(\"No IoC patterns found in Syslog Message.\"))" + " md(\"No IoC patterns found in Syslog Message.\")" ] }, { @@ -1308,14 +1272,14 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:05:57.509366Z", - "start_time": "2020-05-16T00:05:57.455366Z" + "end_time": "2020-06-24T01:57:45.323865Z", + "start_time": "2020-06-24T01:57:45.274865Z" } }, "outputs": [], "source": [ "# Get list of Applications\n", - "apps = all_syslog['ProcessName'].replace('', np.nan).dropna().unique().tolist()\n", + "apps = all_syslog_data['ProcessName'].replace('', np.nan).dropna().unique().tolist()\n", "system_apps = ['sudo', 'CRON', 'systemd-resolved', 'snapd',\n", " '50-motd-news', 'systemd-logind', 'dbus-deamon', 'crontab']\n", "if len(host_entity.Applications) > 0:\n", @@ -1323,7 +1287,7 @@ " installed_apps.extend(x for x in apps if x not in system_apps)\n", "\n", " # Pick Applications\n", - " app_select = nbwidgets.SelectString(description='Select sudo session to investigate: ',\n", + " app_select = nbwidgets.SelectItem(description='Select sudo session to investigate: ',\n", " item_list=installed_apps, width='75%', auto_display=True)\n", "else:\n", " display(HTML(\"No applications other than stand OS applications present\"))" @@ -1334,78 +1298,35 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:06:05.972032Z", - "start_time": "2020-05-16T00:06:05.838032Z" + "end_time": "2020-06-24T01:57:51.258753Z", + "start_time": "2020-06-24T01:57:51.149753Z" } }, "outputs": [], "source": [ - "from bokeh.models import ColumnDataSource, RangeTool\n", - "from bokeh.plotting import figure, show, output_notebook\n", - "from bokeh.layouts import column\n", - "output_notebook()\n", "# Get all syslog relating to these Applications\n", "app = app_select.value\n", - "app_data = all_syslog.loc[all_syslog['ProcessName'] == app]\n", + "app_data = all_syslog_data[all_syslog_data['ProcessName'] == app].copy()\n", "\n", "# App log volume over time\n", "if isinstance(app_data, pd.DataFrame) and not app_data.empty:\n", " app_data_volume = app_data.set_index(\n", " \"TimeGenerated\").resample('5T').count()\n", - " source = ColumnDataSource(\n", - " data=dict(date=app_data_volume.index, count=app_data_volume['SyslogMessage']))\n", - " p = figure(plot_height=300, plot_width=900, tools=\"xpan\", toolbar_location=None,\n", - " x_axis_type=\"datetime\", x_axis_location=\"above\", y_minor_ticks=2,\n", - " title=\"Application syslog volume over time\",\n", - " background_fill_color=\"#efefef\", x_range=(app_data_volume.index[int(len(app_data_volume.index)*.33)], app_data_volume.index[int(len(app_data_volume.index)*.66)]))\n", - " p.line('date', 'count', source=source)\n", - " p.yaxis.axis_label = 'Message volume'\n", - " select = figure(title=\"Drag the middle and edges of the selection box to change the range above\",\n", - " plot_height=130, plot_width=900, y_range=p.y_range,\n", - " x_axis_type=\"datetime\", y_axis_type=None,\n", - " tools=\"\", toolbar_location=None, background_fill_color=\"#efefef\")\n", - " range_tool = RangeTool(x_range=p.x_range)\n", - " range_tool.overlay.fill_color = \"navy\"\n", - " range_tool.overlay.fill_alpha = 0.2\n", - " select.line('date', 'count', source=source)\n", - " select.ygrid.grid_line_color = None\n", - " select.add_tools(range_tool)\n", - " select.toolbar.active_multi = range_tool\n", - " show(column(p, select))\n", + " app_data_volume.reset_index(level=0, inplace=True)\n", + " app_data_volume.rename(columns={\"TenantId\" : \"NoOfLogMessages\"}, inplace=True)\n", + " nbdisplay.display_timeline_values(data=app_data_volume, y='NoOfLogMessages', source_columns=['NoOfLogMessages'], title=f\"{app} log volume over time\") \n", + " \n", " app_high_sev = app_data[app_data['SeverityLevel'].isin(\n", " ['emerg', 'alert', 'crit', 'err', 'warning'])]\n", - " if app_high_sev.empty:\n", - " print(f\"No high severity syslog messages for {app}\")\n", - " else:\n", - " app_high_sev = app_high_sev.set_index(\n", + " if isinstance(app_high_sev, pd.DataFrame) and not app_high_sev.empty:\n", + " app_hs_volume = app_high_sev.set_index(\n", " \"TimeGenerated\").resample('5T').count()\n", - " hs_source = ColumnDataSource(\n", - " data=dict(date=app_high_sev.index, count=app_high_sev['SyslogMessage']))\n", - " hs_p = figure(plot_height=300, plot_width=900, tools=\"xpan\", toolbar_location=None,\n", - " x_axis_type=\"datetime\", x_axis_location=\"above\", y_minor_ticks=2,\n", - " title=\"High Severity application syslog volume over time\",\n", - " background_fill_color=\"#FCF1CB\", x_range=(app_high_sev.index[int(len(app_high_sev.index)*.33)], app_high_sev.index[int(len(app_high_sev.index)*.66)]), y_range=(0, app_data_volume['SyslogMessage'].max()))\n", - " hs_p.line('date', 'count', source=hs_source, line_color='red')\n", - " hs_p.yaxis.axis_label = 'Message volume'\n", - " hs_select = figure(title=\"Drag the middle and edges of the selection box to change the range above\",\n", - " plot_height=130, plot_width=900, y_range=hs_p.y_range,\n", - " x_axis_type=\"datetime\", y_axis_type=None,\n", - " tools=\"\", toolbar_location=None, background_fill_color=\"#FCF1CB\")\n", - " hs_range_tool = RangeTool(x_range=hs_p.x_range)\n", - " hs_range_tool.overlay.fill_color = \"orange\"\n", - " hs_range_tool.overlay.fill_alpha = 0.2\n", - " hs_select.line('date', 'count', source=hs_source, line_color='red')\n", - " hs_select.ygrid.grid_line_color = None\n", - " hs_select.add_tools(hs_range_tool)\n", - " hs_select.toolbar.active_multi = hs_range_tool\n", - " show(column(hs_p, hs_select))\n", - "else:\n", - " display(HTML(\"No data for this application\"))\n", - "# Check for mallicious stuff\n", + " app_hs_volume.reset_index(level=0, inplace=True)\n", + " app_hs_volume.rename(columns={\"TenantId\" : \"NoOfLogMessages\"}, inplace=True)\n", + " nbdisplay.display_timeline_values(data=app_hs_volume, y='NoOfLogMessages', source_columns=['NoOfLogMessages'], title=f\"{app} high severity log volume over time\") \n", + "\n", "risky_messages = risky_cmd_line(events=app_data, log_type=\"Syslog\", cmd_field=\"SyslogMessage\")\n", - "if not risky_messages:\n", - " pass\n", - "else:\n", + "if risky_messages:\n", " print(risky_messages)" ] }, @@ -1422,8 +1343,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:06:27.934589Z", - "start_time": "2020-05-16T00:06:27.885591Z" + "end_time": "2020-06-24T01:59:29.756566Z", + "start_time": "2020-06-24T01:59:29.702565Z" } }, "outputs": [], @@ -1444,8 +1365,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:07:32.371549Z", - "start_time": "2020-05-16T00:06:33.042022Z" + "end_time": "2020-06-24T02:01:09.922496Z", + "start_time": "2020-06-24T02:00:19.315827Z" } }, "outputs": [], @@ -1453,6 +1374,7 @@ "audit_table = None\n", "app_audit_data = None\n", "app = app_select.value\n", + "process_tree_data = None\n", "regex = '.*audit.*\\_cl?'\n", "# Find the table with auditd data in and collect the data\n", "matches = ((re.match(regex, key, re.IGNORECASE)) for key in qry_prov.schema)\n", @@ -1485,12 +1407,6 @@ " )\n", " response = (input(\"Y/N\") or \"N\")\n", " \n", - "# app_audit_query = f\"\"\"{audit_table} \n", - "# | where TimeGenerated >= datetime({proc_invest_times.start}) \n", - "# | where TimeGenerated <= datetime({proc_invest_times.end}) \n", - "# | where Computer == '{hostname}'\n", - "# | where RawData contains \"sshd\"\n", - "# \"\"\"\n", " if (\n", " (count_check['count_'].iloc[0] < 100000)\n", " or (count_check['count_'].iloc[0] > 100000\n", @@ -1506,7 +1422,7 @@ " data=audit_data\n", " )\n", " \n", - " process_tree = auditdextract.generate_process_tree(audit_data=audit_events)\n", + " process_tree_data = auditdextract.generate_process_tree(audit_data=audit_events)\n", " plot_lim = 1000\n", " if len(process_tree) > plot_lim:\n", " md_warn(f\"More than {plot_lim} processes to plot, limiting to top {plot_lim}.\")\n", @@ -1515,11 +1431,13 @@ " process_tree.mp_process_tree.plot(legend_col=\"exe\")\n", " size = audit_events.size\n", " print(f\"Collected {size} rows of data\")\n", + " else:\n", + " md(\"No audit events avalaible\")\n", " else:\n", " print(\"Resize query window\")\n", " \n", "else:\n", - " display(HTML(\"No audit events avalaible\"))" + " md(\"No audit events avalaible\")" ] }, { @@ -1527,37 +1445,27 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:07:41.084063Z", - "start_time": "2020-05-16T00:07:40.662062Z" + "end_time": "2020-06-24T02:01:43.252644Z", + "start_time": "2020-06-24T02:01:42.969634Z" } }, "outputs": [], "source": [ - "display(HTML(f\"

Process tree for {app}

\"))\n", - "#Generate process tree with auditd data around the selected process\n", - "# from msticpy.sectools import auditdextract\n", - "# if isinstance(audit_events, pd.DataFrame) and not audit_events.empty:\n", - "# audit_events = auditdextract.extract_events_to_df(\n", - "# data=app_audit_data, input_column='RawData')\n", - "# if not audit_events[audit_events[\"exe\"].str.contains(app, na=False)].empty:\n", - "# procs = auditdextract.cluster_auditd_processes(audit_data=audit_events, app=app)\n", - "# display(Markdown(f'{len(procs)} process events'))\n", - "# process_tree = auditdextract.generate_process_tree(audit_data = audit_events, processes = procs)\n", - "# nbdisplay.display_process_tree(process_tree)\n", - "\n", - "# else:\n", - "# display(f\"No process tree data avaliable for {app}\")\n", - "# process_tree = None\n", - "if not process_tree[process_tree[\"exe\"].str.contains(app, na=False)].empty: \n", - " app_roots = process_tree[process_tree[\"exe\"].str.contains(app)].apply(lambda x: ptree.get_root(process_tree, x), axis=1)\n", - " trees = []\n", - " for root in app_roots[\"source_index\"].unique():\n", - " trees.append(process_tree[process_tree[\"path\"].str.startswith(root)])\n", - " app_proc_trees = pd.concat(trees)\n", - " app_proc_trees.mp_process_tree.plot(legend_col=\"exe\", show_table=True)\n", + "md(f\"

Process tree for {app}

\")\n", + "if process_tree_data is not None:\n", + " process_tree_df = process_tree_data[process_tree_data[\"exe\"].str.contains(app, na=False)].copy()\n", + " if not process_tree_df.empty: \n", + " app_roots = process_tree_data.apply(lambda x: ptree.get_root(process_tree_data, x), axis=1)\n", + " trees = []\n", + " for root in app_roots[\"source_index\"].unique():\n", + " trees.append(process_tree_data[process_tree_data[\"path\"].str.startswith(root)])\n", + " app_proc_trees = pd.concat(trees)\n", + " app_proc_trees.mp_process_tree.plot(legend_col=\"exe\", show_table=True)\n", + " else:\n", + " display(f\"No process tree data avaliable for {app}\")\n", + " process_tree = None\n", "else:\n", - " display(f\"No process tree data avaliable for {app}\")\n", - " process_tree = None" + " md(\"No data avaliable to build process tree\")" ] }, { @@ -1573,8 +1481,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:08:25.051755Z", - "start_time": "2020-05-16T00:08:03.604431Z" + "end_time": "2020-06-24T02:01:50.058394Z", + "start_time": "2020-06-24T02:01:49.715903Z" } }, "outputs": [], @@ -1589,7 +1497,7 @@ " ioc_types=['ipv4', 'ipv6', 'dns', 'url',\n", " 'md5_hash', 'sha1_hash', 'sha256_hash'])\n", "\n", - "if process_tree is not None and not process_tree.empty:\n", + "if process_tree_data is not None and not process_tree_data.empty:\n", " app_process_tree = app_proc_trees.dropna(subset=['cmdline'])\n", " audit_ioc_df = ioc_extractor.extract(data=app_process_tree,\n", " columns=['cmdline'],\n", @@ -1620,7 +1528,7 @@ " display(app_data[app_data['SyslogMessage'].str.contains(\n", " ioc)][['TimeGenerated', 'SyslogMessage']])\n", "else:\n", - " display(Markdown(\"### No IoC patterns found in Syslog Message.\"))" + " md(\"

No IoC patterns found in Syslog Message.

\")" ] }, { @@ -1653,8 +1561,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:08:58.079873Z", - "start_time": "2020-05-16T00:08:41.302405Z" + "end_time": "2020-06-24T02:02:21.843587Z", + "start_time": "2020-06-24T02:02:11.835821Z" } }, "outputs": [], @@ -1663,7 +1571,7 @@ "ioc_extractor = iocextract.IoCExtract()\n", "os_family = host_entity.OSType if host_entity.OSType else 'Linux'\n", "print('Finding IP Addresses this may take a few minutes.......')\n", - "syslog_ips = ioc_extractor.extract(data=syslog_data,\n", + "syslog_ips = ioc_extractor.extract(data=all_syslog_data,\n", " columns=['SyslogMessage'],\n", " os_family=os_family,\n", " ioc_types=['ipv4', 'ipv6'])\n", @@ -1692,7 +1600,7 @@ " IPs = syslog_ips[['IoCType', 'Observable']].drop_duplicates('Observable')\n", " display(f\"Found {len(IPs)} IP Addresses assoicated with the host\")\n", "else:\n", - " display(Markdown(\"### No IoC patterns found in Syslog Message.\"))\n", + " md(\"### No IoC patterns found in Syslog Message.\")\n", " \n", "if az_ips is not None:\n", " ips = az_ips['PublicIps'].drop_duplicates(\n", @@ -1723,7 +1631,7 @@ " 'FlowType', 'AllExtIPs', 'L7Protocol', 'FlowDirection'],\n", " height=300)\n", "else:\n", - " print('No Azure network data for specified time range.')" + " md('

No Azure network data for specified time range.

')" ] }, { @@ -1740,17 +1648,12 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:09:14.771199Z", - "start_time": "2020-05-16T00:09:04.783620Z" + "end_time": "2020-06-24T02:02:28.305211Z", + "start_time": "2020-06-24T02:02:27.707241Z" } }, "outputs": [], "source": [ - "\n", - "from functools import lru_cache\n", - "from ipwhois import IPWhois\n", - "from ipaddress import ip_address\n", - "\n", "#Lookup each IP in whois data and extract the ASN\n", "@lru_cache(maxsize=1024)\n", "def whois_desc(ip_lookup, progress=False):\n", @@ -1793,14 +1696,15 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:09:26.062183Z", - "start_time": "2020-05-16T00:09:26.045183Z" + "end_time": "2020-06-24T02:03:09.018331Z", + "start_time": "2020-06-24T02:03:08.996333Z" } }, "outputs": [], "source": [ "# For every IP associated with the selected ASN look them up in TI feeds\n", "ip_invest_list = None\n", + "ip_selection = None\n", "for ASN in selection.value:\n", " if ip_invest_list is None:\n", " ip_invest_list = (IP_ASN[IP_ASN[\"ASN\"] == ASN]['IPs'].tolist())\n", @@ -1815,7 +1719,7 @@ " ti_hits = []\n", " while i < len(ti_resps):\n", " if ti_resps['Details'][i]['pulse_count'] > 0:\n", - " ti_hits.append(ti_resps['IoC'][i])\n", + " ti_hits.append(ti_resps['Ioc'][i])\n", " i += 1\n", " else:\n", " i += 1\n", @@ -1826,9 +1730,8 @@ " #Show IPs found in TI feeds for further investigation \n", " if len(ioc_ip_list) > 0: \n", " display(HTML(\"Select an IP whcih appeared in TI to investigate further\"))\n", - " ip_selection = nbwidgets.SelectString(description='Select IP Address to investigate: ', item_list = ioc_ip_list, width='95%', auto_display=True)\n", - " else: \n", - " ip_selection = None\n", + " ip_selection = nbwidgets.SelectItem(description='Select IP Address to investigate: ', item_list = ioc_ip_list, width='95%', auto_display=True)\n", + " \n", "else:\n", " md(\"No IPs to investigate\")" ] @@ -1838,8 +1741,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:09:28.758531Z", - "start_time": "2020-05-16T00:09:28.740532Z" + "end_time": "2020-06-24T02:03:11.613331Z", + "start_time": "2020-06-24T02:03:11.600332Z" } }, "outputs": [], @@ -1847,13 +1750,13 @@ "# Get all syslog for the IPs\n", "if ip_selection is not None:\n", " display(HTML(\"Syslog data associated with this IP Address\"))\n", - " sys_hits = all_syslog[all_syslog['SyslogMessage'].str.contains(\n", + " sys_hits = all_syslog_data[all_syslog_data['SyslogMessage'].str.contains(\n", " ip_selection.value)]\n", " display(sys_hits)\n", " os_family = host_entity.OSType if host_entity.OSType else 'Linux'\n", "\n", " display(HTML(\"TI result for this IP Address\"))\n", - " display(ti_resps[ti_resps['IoC'] == ip_selection.value])\n", + " display(ti_resps[ti_resps['Ioc'] == ip_selection.value])\n", "else:\n", " md(\"No IP address selected\")" ] diff --git a/Entity Explorer - Windows Host.ipynb b/Entity Explorer - Windows Host.ipynb index b5f03c2d..edd29f8f 100644 --- a/Entity Explorer - Windows Host.ipynb +++ b/Entity Explorer - Windows Host.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ " # Windows Host Explorer\n", - " <details>\n", + "
\n", "  Details...\n", "\n", " **Notebook Version:** 1.0
\n", @@ -19,7 +19,7 @@ " **Data Sources Required**:\n", " - Log Analytics - SecurityAlert, SecurityEvent (EventIDs 4688 and 4624/25), AzureNetworkAnalytics_CL, Heartbeat\n", " - (Optional) - VirusTotal, AlienVault OTX, IBM XForce, Open Page Rank, (all require accounts and API keys)\n", - " </details>\n", + "
\n", "\n", " Brings together a series of queries and visualizations to help you determine the security state of the Windows host or virtual machine that you are investigating.\n" ] @@ -63,12 +63,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:27:18.623464Z", - "start_time": "2020-05-15T23:27:15.156160Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", @@ -78,7 +73,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -95,6 +90,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "nbinit.init_notebook(\n", " namespace=globals(),\n", @@ -107,7 +105,7 @@ "metadata": {}, "source": [ " ## Get WorkspaceId and Authenticate to Azure Sentinel\n", - " <details>\n", + "
\n", " Details...\n", " If you are using user/device authentication, run the following cell.\n", " - Click the 'Copy code to clipboard and authenticate' button.\n", @@ -127,18 +125,13 @@ " Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", " On successful authentication you should see a ```popup schema``` button.\n", " To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - " </details>" + "
" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:27:22.847608Z", - "start_time": "2020-05-15T23:27:22.839609Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "#See if we have an Azure Sentinel Workspace defined in our config file, if not let the user specify Workspace and Tenant IDs\n", @@ -166,12 +159,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:28:39.796803Z", - "start_time": "2020-05-15T23:27:27.080209Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if config is False:\n", @@ -185,12 +173,7 @@ }, { "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-31T23:37:18.211230Z", - "start_time": "2019-10-31T23:37:18.204259Z" - } - }, + "metadata": {}, "source": [ "### Authentication and Configuration Problems\n", "\n", @@ -222,12 +205,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:28:41.610484Z", - "start_time": "2020-05-15T23:28:41.598485Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "host_text = widgets.Text(\n", @@ -239,12 +217,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:28:46.198826Z", - "start_time": "2020-05-15T23:28:46.144827Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "query_times = nbwidgets.QueryTime(units=\"day\", max_before=20, before=5, max_after=1)\n", @@ -254,12 +227,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:28:58.158859Z", - "start_time": "2020-05-15T23:28:55.922817Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Get single event - try process creation\n", @@ -271,7 +239,7 @@ ")\n", "if len(matching_hosts_df) > 1:\n", " print(f\"Multiple matches for '{host_text.value}'. Please select a host from the list.\")\n", - " choose_host = nbwidgets.SelectString(\n", + " choose_host = nbwidgets.SelectItem(\n", " item_list=list(matching_hosts_df[\"Computer\"].values),\n", " description=\"Select the host.\",\n", " auto_display=True,\n", @@ -286,12 +254,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:12.506439Z", - "start_time": "2020-05-15T23:29:01.493356Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if not host_name:\n", @@ -393,12 +356,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:12.712441Z", - "start_time": "2020-05-15T23:29:12.667444Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ra_query_times = nbwidgets.QueryTime(\n", @@ -414,12 +372,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:15.961693Z", - "start_time": "2020-05-15T23:29:14.578094Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "\n", @@ -467,18 +420,13 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:19.615661Z", - "start_time": "2020-05-15T23:29:19.546661Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "def disp_full_alert(alert):\n", " global related_alert\n", " related_alert = SecurityAlert(alert)\n", - " nbdisplay.display_alert(related_alert, show_entities=True)\n", + " return nbdisplay.format_alert(related_alert, show_entities=True)\n", "\n", "recenter_wgt = widgets.Checkbox(\n", " value=True,\n", @@ -490,7 +438,7 @@ " related_alerts[\"CompromisedEntity\"] = related_alerts[\"Computer\"]\n", " display(Markdown(\"### Click on alert to view details.\"))\n", " display(recenter_wgt)\n", - " rel_alert_select = nbwidgets.AlertSelector(\n", + " rel_alert_select = nbwidgets.SelectAlert(\n", " alerts=related_alerts,\n", " action=disp_full_alert,\n", " )\n", @@ -510,12 +458,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:25.409590Z", - "start_time": "2020-05-15T23:29:25.359585Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to the time of our alert\n", @@ -543,12 +486,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:29.150066Z", - "start_time": "2020-05-15T23:29:27.337759Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "host_logons = qry_prov.WindowsSecurity.list_host_logons(\n", @@ -605,12 +543,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:32.494051Z", - "start_time": "2020-05-15T23:29:31.042487Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "failedLogons = qry_prov.WindowsSecurity.list_host_logon_failures(\n", @@ -651,12 +584,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:35.995808Z", - "start_time": "2020-05-15T23:29:35.834809Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if not failedLogons.empty:\n", @@ -695,12 +623,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:47.340949Z", - "start_time": "2020-05-15T23:29:38.308340Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "md(f\"Collecting Windows Event Logs for {host_entity.HostName}, this may take a few minutes...\")\n", @@ -752,23 +675,18 @@ "For events that you want to look at in more detail you can parse out the full EventData field (containing all fields of the original event). The `parse_event_data` function below does that - transforming the EventData XML into a dictionary of property/value pairs). The `expand_event_properties` function takes this dictionary and transforms into columns in the output DataFrame.\n", "\n", "
\n", - "<details>\n", + "
\n", "  More details...\n", "You can do this for multiple event types in a single pass but, dependng on the schema of each event you may end up with a lot of sparsely populated columns. E.g. suppose EventID 1 has EventData fields A, B and C and EventID 2 has fields A, D, E. If you parse both IDs you'll will end up with a DataFrame with columns A, B, C, D and E with contents populated only for the rows that with corresponding data.\n", "\n", "We recommend that you process batches of related event types (e.g. all user account change events) that have similar sets of fields to keep the output DataFrame manageable.\n", - "</details>" + "
" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:29:58.968325Z", - "start_time": "2020-05-15T23:29:58.080325Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Function to convert EventData XML into dictionary and\n", @@ -836,12 +754,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:30:02.077311Z", - "start_time": "2020-05-15T23:30:01.842315Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Get a full list of Windows Security Events\n", @@ -882,12 +795,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-11-01T19:38:42.155265Z", - "start_time": "2019-11-01T19:38:42.104295Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# populate actual events IDs to select from\n", @@ -906,12 +814,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-11-01T19:38:48.430726Z", - "start_time": "2019-11-01T19:38:48.412764Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "col_names = ['TimeGenerated', 'Account', 'AccountType',\n", @@ -945,12 +848,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:30:15.514844Z", - "start_time": "2020-05-15T23:30:14.697818Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from msticpy.sectools.eventcluster import (\n", @@ -999,12 +897,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:30:17.480349Z", - "start_time": "2020-05-15T23:30:17.426340Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "import re\n", @@ -1032,17 +925,11 @@ " return host_logons.query(\"TargetUserName == @acct and LogonType == @logon_type\")\n", "\n", "\n", - "# Create an Output widget to show the Logon Details\n", - "w_output = widgets.Output(layout={\"border\": \"1px solid black\"})\n", - "\n", - "\n", "def show_logon(idx):\n", - " w_output.clear_output()\n", - " with w_output:\n", - " nbdisplay.display_logon_data(pd.DataFrame(clus_logons.loc[idx]).T)\n", + " return nbdisplay.format_logon(pd.DataFrame(clus_logons.loc[idx]).T)\n", "\n", "\n", - "logon_wgt = nbwidgets.SelectString(\n", + "logon_wgt = nbwidgets.SelectItem(\n", " description=\"Select logon cluster to examine\",\n", " item_dict=dist_logons,\n", " action=show_logon,\n", @@ -1050,11 +937,7 @@ " width=\"100%\",\n", " auto_display=True,\n", ")\n", - "display(w_output)\n", - "# Display the first item on first view\n", - "top_item = next(iter(dist_logons.values()))\n", - "with w_output:\n", - " nbdisplay.display_logon_data(pd.DataFrame(clus_logons.loc[top_item]).T)\n" + "\n" ] }, { @@ -1071,12 +954,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:30:31.264572Z", - "start_time": "2020-05-15T23:30:31.213572Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to start at the first logon in our set\n", @@ -1108,12 +986,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:30:56.847458Z", - "start_time": "2020-05-15T23:30:36.409635Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from msticpy.sectools.eventcluster import dbcluster_events, add_process_features\n", @@ -1195,12 +1068,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:02.979872Z", - "start_time": "2020-05-15T23:31:02.675871Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Display process timeline for 75% percentile rarest scores\n", @@ -1234,12 +1102,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:09.344452Z", - "start_time": "2020-05-15T23:31:09.280062Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "def view_logon_sess(logon_id=\"\"):\n", @@ -1254,7 +1117,7 @@ " sess_procs = procs_with_cluster.query(\"SubjectLogonId == @logon_id\")[\n", " [\"NewProcessName\", \"CommandLine\", \"SubjectLogonId\", \"ClusterSize\"]\n", " ].drop_duplicates()\n", - " display(sess_procs)\n", + " return sess_procs\n", "\n", "sessions = list(process_rarity\n", " .sort_values(\"Rarity\", ascending=False)\n", @@ -1267,7 +1130,7 @@ " **WIDGET_DEFAULTS,\n", ")\n", "display(all_procs)\n", - "logon_wgt = nbwidgets.SelectString(\n", + "logon_wgt = nbwidgets.SelectItem(\n", " description=\"Select logon session to examine\",\n", " item_dict={label: val for label, val in sessions},\n", " height=\"300px\",\n", @@ -1295,15 +1158,10 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-19T01:18:41.70951Z", - "start_time": "2019-10-19T01:18:41.686523Z" - } - }, + "metadata": {}, "outputs": [], "source": [ - "logon_wgt2 = nbwidgets.SelectString(\n", + "logon_wgt2 = nbwidgets.SelectItem(\n", " description=\"Select logon cluster to examine\",\n", " item_dict=dist_logons,\n", " height=\"200px\",\n", @@ -1328,12 +1186,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-19T01:18:55.502417Z", - "start_time": "2019-10-19T01:18:55.450467Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "selected_logon_cluster = get_selected_logon_cluster(logon_wgt2.value)\n", @@ -1453,12 +1306,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-09-12T22:34:30.885408Z", - "start_time": "2019-09-12T22:34:30.882411Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Use this to search all process commandlines\n", @@ -1474,12 +1322,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:26.280399Z", - "start_time": "2020-05-15T23:31:25.106400Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "selected_tgt_logon = selected_logon[\"TargetUserName\"].iat[0]\n", @@ -1514,7 +1357,7 @@ "metadata": {}, "source": [ " ## If any Base64 encoded strings, decode and search for IoCs in the results.\n", - " <details>\n", + "
\n", "  Details...\n", " This section looks for base64 encoded strings within the data - this is a common way of hiding attacker intent. It attempts to decode any strings that look like base64. Additionally, if the base64 decode operation returns any items that look like a base64 encoded string or file, a gzipped binary sequence, a zipped or tar archive, it will attempt to extract the contents before searching for potentially interesting IoC observables within the decoded data.\n", "\n", @@ -1535,18 +1378,13 @@ " - printable_bytes - printable version of input_bytes as a string of \\xNN values\n", " - src_index - the index of the row in the input dataframe from which the data came.\n", " - full_decoded_string - the full decoded string with any decoded replacements. This is only really useful for top-level items, since nested items will only show the 'full' string representing the child fragment.\n", - " </details>" + "
" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:27.893142Z", - "start_time": "2020-05-15T23:31:27.880144Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "\n", @@ -1610,12 +1448,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:36.912323Z", - "start_time": "2020-05-15T23:31:30.600559Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ti_lookup = TILookup()\n", @@ -1631,12 +1464,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:31:47.445775Z", - "start_time": "2020-05-15T23:31:37.910324Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "iocs_to_check = (ioc_df[ioc_df[\"Observable\"].isin(ioc_ss.selected_items)]\n", @@ -1656,12 +1484,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:32:07.424166Z", - "start_time": "2020-05-15T23:32:07.379166Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ip_q_times = nbwidgets.QueryTime(\n", @@ -1687,12 +1510,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:32:15.750114Z", - "start_time": "2020-05-15T23:32:09.049085Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if \"AzureNetworkAnalytics_CL\" not in table_index:\n", @@ -1740,12 +1558,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:32:17.381114Z", - "start_time": "2020-05-15T23:32:17.067117Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if az_net_comms_df is not None and not az_net_comms_df.empty:\n", @@ -1779,12 +1592,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:32:18.693115Z", - "start_time": "2020-05-15T23:32:18.586117Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if az_net_comms_df is not None and not az_net_comms_df.empty:\n", @@ -1840,12 +1648,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:33:03.907521Z", - "start_time": "2020-05-15T23:32:26.377680Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# WHOIS lookup function\n", @@ -1932,13 +1735,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:33:05.240520Z", - "start_time": "2020-05-15T23:33:05.179518Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "all_asns = list(flow_sum_df[\"DestASN\"].unique()) + list(flow_sum_df[\"SourceASN\"].unique())\n", @@ -1956,13 +1753,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-15T23:33:20.587377Z", - "start_time": "2020-05-15T23:33:06.602518Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "from itertools import chain\n", @@ -2007,12 +1798,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-10-19T01:25:26.845038Z", - "start_time": "2019-10-19T01:25:26.778039Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "def format_ip_entity(row, ip_col):\n", @@ -2124,6 +1910,7 @@ "metadata": { "file_extension": ".py", "hide_input": false, + "history": [], "kernelspec": { "display_name": "Python 3.6", "language": "python", @@ -2181,6 +1968,7 @@ "toc_section_display": true, "toc_window_display": true }, + "uuid": "752d7f6a-d842-43cc-b46d-4d9e9a2c1160", "varInspector": { "cols": { "lenName": 16, @@ -2217,14 +2005,7 @@ ], "window_display": false }, - "version": 3, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } + "version": 3 }, "nbformat": 4, "nbformat_minor": 4 diff --git a/Getting Started with Azure Sentinel Notebooks.ipynb b/Getting Started with Azure Sentinel Notebooks.ipynb deleted file mode 100644 index 01e2c737..00000000 --- a/Getting Started with Azure Sentinel Notebooks.ipynb +++ /dev/null @@ -1,953 +0,0 @@ -{ - "cells": [ - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "# Getting Started with Azure Notebooks and Azure Sentinel\n", - "**Notebook Version:** 1.0
\n", - " **Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", - " **Required Packages**:
\n", - " **Platforms Supported**:\n", - " - Azure Notebooks Free Compute\n", - " - Azure Notebooks DSVM\n", - " - OS Independent\n", - "\n", - "**Data Sources Required**:\n", - " - Log Analytics - SiginLogs (Optional)\n", - " - VirusTotal\n", - " - MaxMind\n", - " \n", - " \n", - "This notebook takes you through the basics needed to get started with Azure Notebooks and Azure Sentinel, and how to perform the basic actions of data acquisition, data enrichment, data analysis, and data visualization. These actions are the building blocks of threat hunting with notebooks and are useful to understand before running more complex notebooks. This notebook only lightly covers each topic but includes 'learn more' sections to provide you with the resource to deep dive into each of these topics. \n", - "\n", - "This notebook assumes that you are running this in an Azure Notebooks environment, however it will work in other Jupyter environments.\n", - "\n", - "**Note:**\n", - "This notebooks uses SigninLogs from your Azure Sentinel Workspace. If you are not yet collecting SigninLogs configure this connector in the Azure Sentinel portal before running this notebook.\n", - "This notebook also uses the VirusTotal API for data enrichment, for this you will require an API key which can be obtained by signing up for a free [VirusTotal community account](\"https://www.virustotal.com/gui/join-us\")\n" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## What is a Jupyter notebook?\n", - "You are currently reading a Jupyter notebook. [Jupyter](http://jupyter.org/) is an interactive development and data manipulation environment presented in a browser. Using Jupyter you can create documents, called Notebooks. These documents are made up of cells that contain interactive code, alongside that code's output, and other items such as text and images (what you are looking at now is a cell of Markdown text).\n", - "\n", - "The name, Jupyter, comes from the core supported programming languages that it supports: Julia, Python, and R. Whilst you can use any of these languages we are going to use Python in this notebook, in addition the notebooks that come with Azure Sentinel are all written in Python. Whilst there are pros, and cons to each language Python is a well-established language that has a large number of materials and libraries well suited for data analysis and security investigation, making it ideal for our needs.\n", - "\n", - "### Learn more:\n", - " - The [Infosec Jupyter Book](\"https://infosecjupyterbook.com/introduction.html\") has more details on the technical working of Jupyter.\n", - " - [The Jupyter Project documentation](\"https://jupyter.org/documentation\")\n", - "\n", - "---\n", - "## How to use a Jupyter notebook?\n", - "To use a Jupyter notebook you need a Jupyter server that will render the notebook and execute the code within it. This can take the form of a local [Jupyter installation](https://pypi.org/project/jupyter/), or a remotely hosted version such as [Azure Notebooks](https://notebooks.azure.com/). If you are reading this it is highly likely that you already have a Jupyter server that this notebook is using.\n", - "You can learn more about installing and running your own Jupyter server [here](https://realpython.com/jupyter-notebook-introduction/).\n", - "\n", - "### Using Azure Notebooks\n", - "If you accessed this notebook from Azure Sentinel, you are probably using Azure Notebooks to run this notebook. Azure Notebooks runs in the same way that a local Jupyter server with, except with the additional feature of integrated project management and file storage. When you open a notebook in Azure Notebooks the user interface is nearly identical to a standard Jupyter notebook experience.\n", - "\n", - "Before you can start running code in a notebook you need to make sure that it is connected to a Jupyter server and you have the correct type of kernel configured. For this notebook we are going to be using Python 3.6, hopefully Azure Notebooks has already loaded this kernel for you - you can check this by looking at the top left corner of the screen where you should see the currently connected kernel. \n", - "\n", - "![KernelIssue](./images/nb_img1.png)\n", - "\n", - "If this does not read Python 3.6 you can select the correct kernel by selecting Kernel > Change kernel from the top menu and clicking Python 3.6.\n", - "\n", - "> **Note**: the notebook works with Python 3.6, 3.7 or later. If you are using this notebook in Azure ML or another Jupyter environment you can choose any kernel that supports Python 3.6 or later\n", - "\n", - "![KernelPicker](./images/nb_img2.png)\n", - "\n", - "Once you have done this you should be ready to move onto a code cell.\n", - "> **Tip**: You can identify which cells are code by selecting them and looking at the drop down box at the center of the top menu. It will either read 'Code' (for interactive code cells), 'Markdown' (for Markdown text cells like this one), or RawNBConvert (these are just raw data and not interpreted by Jupyter - they can be used by tools that process notebook files, such as *nbconvert* to render the data into HTML or LaTeX). \n", - "\n", - "If you click on the cell below you should see this box change to 'Code'.\n", - "\n", - "### Learn More:\n", - "More details on Azure Notebooks can be found in the [Azure Notebooks documentation](https://docs.microsoft.com/en-us/azure/notebooks/) and the [Azure Sentinel documentation](https://docs.microsoft.com/en-us/azure/sentinel/notebooks).\n", - "\n", - "---\n", - "## Running code\n", - "Once you have selected a code cell you can run it by clicking the run button at the menu bar at the top, or by pressing Ctrl+Enter.\n" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# This is our first code cell, it contains basic Python code.\n", - "# You can run a code cell by selecting it and clicking the Run button in the top menu, or by pressing Shift + Enter.\n", - "# Once you run a code cell any output from that code will be displayed directly below it.\n", - "print(\"Congratulations you just ran this code cell\")\n", - "y = 2+2\n", - "print(\"2 + 2 =\", y)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Variables set within a code cell persist between cells meaning you can chain cells together" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "y + 2" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn More : \n", - " - The [Infosec Jupyter Book](\"https://infosecjupyterbook.com/\") provides an infosec specific intro to Python.\n", - " - [Real Pyhton](\"https://realpython.com/\") is a comprehensive set of Python learnings and tutorials.\n", - "
\n", - "
" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Now that you understand the basics we can move onto more complex code.\n", - "\n", - "---\n", - "## Setting up the environment\n", - "Code cells behave in the same way your code would in other environments, so you need to remember about common coding practices such as variable initialization and library imports. \n", - "Before we execute more complex code we need to make sure the required packages are installed and libraries imported. At the top of many of the Azure Sentinel notebooks you will see large cells that will check kernel versions and then install and import all the libraries we are going to be using in the notebook, make sure you run this before running other cells in the notebook.\n", - "If you are running notebooks locally or via dedicated compute in Azure Notebooks library installs will persist but this is not the case with Azure Notebooks free tier, so you will need to install each time you run. Even if running in a static environment imports are required for each run so make sure you run this cell regardless." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from pathlib import Path\n", - "import os\n", - "import sys\n", - "import warnings\n", - "from IPython.display import display, HTML, Markdown\n", - "\n", - "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", - "\n", - "display(HTML(\"

Starting Notebook setup...

\"))\n", - "# If you did not clone the entire Azure-Sentinel-Notebooks repo you may not have this file\n", - "if Path(\"./utils/nb_check.py\").is_file():\n", - " from utils.nb_check import check_python_ver, check_mp_ver\n", - "\n", - " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", - " try:\n", - " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", - " except ImportError:\n", - " !pip install --upgrade msticpy\n", - " if \"msticpy\" in sys.modules:\n", - " importlib.reload(sys.modules[\"msticpy\"])\n", - " else:\n", - " import msticpy\n", - " check_mp_ver(MSTICPY_REQ_VERSION)\n", - " \n", - "from msticpy.nbtools import nbinit\n", - "nbinit.init_notebook(\n", - " namespace=globals(),\n", - " extra_imports=[\"ipwhois, IPWhois, pyyaml\"]\n", - ")" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Configuration\n", - "Once we have set up our Jupyter environment with the libraries that we'll use in the notebook, we need to make sure we have some configuration in place. Some of the notebook components need addtional configuration to connect to external services (e.g. API keys to retrieve Threat Intelligence data). This includes configuration for connection to our Azure Sentinel workspace, as well as some threat intelligence providers we will use later.\n", - "The easiest way to handle the configuration for these services is to store them in a msticpyconfig file (`msticpyconfig.yaml`). More details on msticpyconfig can be found here: https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html\n", - "\n", - "### Learn more: \n", - "- In this notebook we will setup the basic config we need to get started. If you need a more complete walk-through we have a separate notebook to help you: https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb\n", - "
\n", - "
" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "The Azure-Sentinel-Notebooks GitHub repo contains an template msticpyconfig file ready to be populated. If you have run this notebook before you may have a msticpyconfig file already populated, the cell below allows you to checks if this file. If your config file does not contain details under Azure Sentinel > Workspaces, or TIProviders the following cells will populate these for you.
\n", - "If you want to see an example of what a populated msticpyconfig file should look like a samples is included in the repo as msticpyconfig-sample.yaml." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "%pfile msticpyconfig.yaml" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "If you do not have and msticpyconfig file we can populate one for you. Before you do this you will need a few things.\n", - "\n", - "The first is the Workspace ID and Tenant ID of the Azure Sentinel Workspace you wish to connect to.\n", - "\n", - " - You can get the workspace ID by opening Azure Sentinel in the [Azure Portal](\"https://portal.azure.com\") and selecting Settings > Workspace Settings. Your Workspace ID is displayed near the top of this page.\n", - "\n", - "- You can get your tenant ID (also referred to organization or directory ID) via [Azure Active Directory](\"https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id\")\n", - "\n", - "We are going to use [VirusTotal](\"https://www.virustotal.com\") to enrich our Azure Sentinel data. For this you will need a VirusTotal API key, one of these can be obtained for free (as a personnal key) via the [VirusTotal](\"https://developers.virustotal.com/v3.0/reference#getting-started\") website.\n", - "We are using VirusTotal for this notebook but we also support a range of other threat intelligence providers: https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html\n", - "

\n", - "In addition we are going to plot IP address locations on a map, in order to do this we are going to use [MaxMind](\"https://www.maxmind.com\") to geolocate IP addresses which requires an API key. You can sign up for a free account and API key at https://www.maxmind.com/en/geolite2/signup. \n", - "

\n", - "Once you have these required items run the cell below and you will prompted to enter these elements:" - ] - }, - { - "metadata": { - "trusted": true, - "scrolled": true - }, - "cell_type": "code", - "source": [ - "ws_id = nbwidgets.GetEnvironmentKey(env_var='WORKSPACE_ID',\n", - " prompt='Please enter your Log Analytics Workspace Id:', auto_display=True)\n", - "ten_id = nbwidgets.GetEnvironmentKey(env_var='TENANT_ID',\n", - " prompt='Please enter your Log Analytics Tenant Id:', auto_display=True)\n", - "vt_key = nbwidgets.GetEnvironmentKey(env_var='VT_KEY',\n", - " prompt='Please enter your VirusTotal API Key:', auto_display=True)\n", - "mm_key = nbwidgets.GetEnvironmentKey(env_var='MM_KEY',\n", - " prompt='Please enter your MaxMind API Key:', auto_display=True)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - " The cell below will now populate a msticpyconfig file with these values:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "import yaml\n", - "with open(\"msticpyconfig.yaml\") as config:\n", - " data = yaml.load(config, Loader=yaml.Loader)\n", - "data['AzureSentinel']\n", - "\n", - "workspace = {\"Default\":{\"WorkspaceId\": ws_id.value, \"TenantId\": ten_id.value}}\n", - "ti = {\"VirusTotal\":{\"Args\": {\"AuthKey\" : vt_key.value}, \"Primary\" : True, \"Provider\": \"VirusTotal\"}}\n", - "other_prov = {\"GeoIPLite\" : {\"Args\" : {\"AuthKey\" : mm_key.value, \"DBFolder\" : \"~/msticpy\"}, \"Provider\" : \"GeoLiteLookup\"}}\n", - "data['AzureSentinel']['Workspaces'] = workspace\n", - "data['TIProviders'] = ti\n", - "data['OtherProviders'] = other_prov\n", - "\n", - "with open(\"msticpyconfig.yaml\", 'w') as config:\n", - " yaml.dump(data, config)\n", - " \n", - "print(\"msticpyconfig.yaml updated\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "We can now validate our configuration is correct." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from msticpy.common.pkg_config import refresh_config, validate_config\n", - "refresh_config()\n", - "validate_config()" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "> **Note** you may see warnings for missing providers when running this cell.\n", - "> This is not an issue as we will not be using all providers in this notebook\n", - "> so long as you get thie message \"No errors found.\" you are OK to proceed.\n" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Getting Data\n", - "Now that we have configured the details necessary to connect to Azure Sentinel we can go ahead and get some data. We will do this with `QueryProvider()` from MSTICpy. \n", - "You can use the `QueryProvider` class to connect to different data sources such as MDATP, the Security Graph API, and the one we will use here, Azure Sentinel. \n", - "\n", - "### Learn more:\n", - " - More details on configuring and using QueryProviders can be found in the [MSTICpy Documentation](\"https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#instantiating-a-query-provider\").\n", - "

" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "For now, we are going to set up a QueryProvider for Azure Sentinel, pass it the details for our workspace that we just stored in the msticpyconfig file, and connect. The connection process will ask us to authenticate to our Azure Sentinel workspace via [device authorization](\"https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code\") with our Azure credentials. You can do this by clicking the device login code button that appears as the output of the next cell, or by navigating to https://microsoft.com/devicelogin and manually entering the code. Note that this authentication persists with the kernel you are using with the notebook, so if you restart the kernel you will need to re-authenticate.\n" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Initalize a QueryProvider for Azure Sentinel\n", - "qry_prov = QueryProvider(\"LogAnalytics\")\n", - "\n", - "# Get the Azure Sentinel workspace details from msticpyconfig\n", - "try:\n", - " ws_config = WorkspaceConfig()\n", - " md(\"Workspace details collected from config file\")\n", - "except:\n", - " raise(\"No workspace settings are configured, please run the cells above to configure these.\")\n", - " \n", - "# Connect to Azure Sentinel with our QueryProvider and config details\n", - "# ws_config.code_connect_str is a feature of MSTICpy that creates the required connection string from details in our msticpyconfig\n", - "qry_prov.connect(connection_str=ws_config.code_connect_str)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Now that we have connected we can query Azure Sentinel for data, but before we do that we need to understand what data is avalaible to query. The QueryProvider object provides a way to get a list of tables as well as tables and table columns:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Get list of tables in our Workspace\n", - "display(qry_prov.schema_tables [:5]) # We are outputting only the first 5 tables for brevity\n", - "# Get list of tables and thier columns\n", - "qry_prov.schema['SigninLogs'] # We are only displaying the columns for SigninLogs for brevity" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "MSTICpy includes a number of built in queries that you can run.
\n", - "You can list available queries with .list_queries() and get specific details about a query by calling it with \"?\" as a parameter" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Get a list of avaliable queries\n", - "qry_prov.list_queries()" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Get details about a query\n", - "qry_prov.Azure.list_all_signins_geo(\"?\")" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "You can then run the query by calling it with the required parameters:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from datetime import datetime, timedelta\n", - "# set our query end time as now\n", - "end = datetime.now()\n", - "# set our query start time as 1 hour ago\n", - "start = end - timedelta(hours=1)\n", - "# run query with specified start and end times\n", - "logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", - "# display first 5 rows of any results\n", - "logons_df.head() # If you have no data you will just see the column headings displayed" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Another way to run queries is to pass a string format of a KQL query to the query provider, this will run the query against the workspace connected to above, and will return the data in a [Pandas DataFrame](\"https://pandas.pydata.org/\"). We will look at working with Pandas in a bit more detail later." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Define our query\n", - "test_query = \"\"\"\n", - "SigninLogs\n", - "| where TimeGenerated > ago(7d)\n", - "| take 10\n", - "\"\"\"\n", - "\n", - "# Pass that query to our QueryProvider\n", - "test_df = qry_prov.exec_query(test_query)\n", - "\n", - "# Check that we have some data\n", - "if isinstance(test_df, pd.DataFrame) and not test_df.empty:\n", - " # .head() returns the first 5 rows of our results DataFrame\n", - " display(test_df.head())\n", - "# If where is no data load some sample data to use instead\n", - "else:\n", - " md(\"You don't appear to have any SigninLogs - we will load sample data for you to use.\")\n", - " qry_prov = QueryProvider(\"LocalData\", data_paths=[\"nbdemo/data/\"], query_paths=[\"nbdemo/data/\"])\n", - " logons_df = qry_prov.Azure.list_all_signins_geo()\n", - " display(logons_df.head())" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn more:\n", - " - You can learn more about the MSTICpy pre-defined queries in the [MSTICpy Documentation](\"https://msticpy.readthedocs.io/en/latest/data_acquisition/DataProviders.html#running-an-pre-defined-queryl\")" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Pandas\n", - "Our query results are returned in the form of a Pandas DataFrame. DataFrames are a core component of the Azure Sentinel notebooks and of MSTICpy and is used for both input and output formats.\n", - "Pandas DataFrames are incredibly versitile data structures with a lot of useful features, we will cover a small number of them here and we recommend that you check out the Learn more section to learn more about Pandas features.\n", - "
\n", - "
\n", - "### Displaying a DataFrame:\n", - "The first thing we want to do is display our DataFrame. You can either just run it or explicity display it by calling `display(df)`." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# For this section we are going to create a DataFrame from data we have saved in a csv file\n", - "df = pd.read_csv(\"https://raw.githubusercontent.com/microsoft/msticpy/master/tests/testdata/host_logons.csv\", index_col=[0] )\n", - "# Display our DataFrame\n", - "df # or display(df)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "> **Note** if the dataframe variable (`df` in the example above) is the last statement in a \n", - "> code cell, Jupyter will automatically display it without using the `display()` function. \n", - "> However, if you want to display a DataFrame in the middle of \n", - "> other code in a cell you must use the `display()` function.\n", - "\n", - "You may not want to display the whole DataFrame and instead display only a selection of items. There are numerous ways to do this and the cell below shows some of the most widely used functions." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Display the first 2 rows using head(): \", \"bold\")\n", - "display(df.head(2)) # we don't need to call display here but just for illustration" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Display the 3rd row using iloc[]: \", \"bold\")\n", - "df.iloc[3]" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Show the column names in the DataFrame \", \"bold\")\n", - "df.columns" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Display just the TimeGenerated and TenantId columnns: \", \"bold\")\n", - "df[[\"TimeGenerated\", \"TenantId\"]]" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "We can also choose to select a subsection of our DataFrame based on the contents of the DataFrame:\n", - "\n", - "> **Tip**: the syntax in these examples is using a technique called *boolean indexing*. \n", - ">
`df[]`\n", - "> returns all rows in the dataframe where the boolean expression is True\n", - ">
In the first example we telling pandas to return all rows where the column value of\n", - "> 'TargetUserName' matches 'MSTICAdmin'" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Display only rows where TargetUserName value is 'MSTICAdmin': \", \"bold\")\n", - "df[df['TargetUserName']==\"MSTICAdmin\"]" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "md(\"Display rows where TargetUserName is either MSTICAdmin or adm1nistratror:\", \"bold\")\n", - "display(df[df['TargetUserName'].isin(['adm1nistrator', 'MSTICAdmin'])])" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Our DataFrame call also be extended to add new columns with additional data if reqired:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "df[\"NewCol\"] = \"Look at my new data!\"\n", - "display(df[[\"TenantId\",\"Account\", \"TimeGenerated\", \"NewCol\"]].head(2))" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn more:\n", - "There is a lot more you can do with Pandas, the links below provide some useful resources:\n", - " - [Getting starting with Pandas](\"https://pandas.pydata.org/pandas-docs/stable/getting_started/index.html\")\n", - " - [Infosec Jupyerbook intro to Pandas](\"https://infosecjupyterbook.com/notebooks/tutorials/03_intro_to_pandas.html\")\n", - " - [A great list of Pandas hints and tricks](\"https://www.dataschool.io/python-pandas-tips-and-tricks/\")" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Enriching data\n", - "\n", - "Now that we have seen how to query for data, and do some basic manipulation we can look at enriching this data with additional data sources. For this we are going to use an external threat intelligence provider to give us some more details about an IP address we have in our dataset using the [MSTICpy TIProvider](\"https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html\") feature." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from datetime import datetime, timedelta\n", - "# Check if we have logon data already and if not get some\n", - "if not isinstance(logons_df, pd.DataFrame) or logons_df.empty:\n", - " # set our query end time as now\n", - " end = datetime.now()\n", - " # set our query start time as 1 hour ago\n", - " start = end - timedelta(days=1)\n", - " # run query with specified start and end times\n", - " logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", - " \n", - "# Create our TI provider\n", - "ti = TILookup()\n", - "# Get the first logon IP address from our dataset\n", - "ip = logons_df.iloc[1]['IPAddress']\n", - "# Look up the IP in VirusTotal\n", - "ti_resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", - "\n", - "# Format our results as a DataFrame\n", - "ti_resp = ti.result_to_df(ti_resp)\n", - "display(ti_resp)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Using the [Pandas apply()](\"https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html\") feature we can get results for all the IP addresses in our data set and add the lookup severity score as a new column in our DataFrame for easier reference." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "# Take the IP address in each row, look it up against TI and return the seveirty score\n", - "def lookup_res(row):\n", - " ip = row['IPAddress']\n", - " resp = ti.lookup_ioc(ip, providers=[\"VirusTotal\"])\n", - " resp = ti.result_to_df(resp)\n", - " return resp[\"Severity\"].iloc[0]\n", - "\n", - "# Take the first 3 rows of data and copy they into a new DataFrame\n", - "enrich_logons_df = logons_df.iloc[:3].copy()\n", - "# Create a new column called TIRisk and populate that with the TI severity score of the IP Address in that row\n", - "enrich_logons_df['TIRisk'] = enrich_logons_df.apply(lookup_res, axis=1)\n", - "# Display a subset of columns from our DataFrame\n", - "enrich_logons_df[[\"TimeGenerated\", \"ResultType\", \"UserPrincipalName\", \"IPAddress\", \"TIRisk\"]]" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn more:\n", - "MSTICpy includes further threat intelligence capabilities as well as other data enrichment options. More details on these can be found in the [documentation](\"https://msticpy.readthedocs.io/en/latest/DataEnrichment.html\")." - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Analyzing data\n", - "With the data we have collected we may wish to perform some analysis on it in order to better understand it. MSTICpy includes a number of features to help with this, and there are a vast array of other data analysis capabilities available via Python ranging from simple processes to complex ML models. We will start here by keeping it simple and look at how we can decode some Base64 encoded command line strings we have in order to allow us to understand their content." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from msticpy.sectools import base64unpack as b64\n", - "# Take our encoded Powershell Command\n", - "b64_cmd = \"powershell.exe -encodedCommand SW52b2tlLVdlYlJlcXVlc3QgaHR0cHM6Ly9jb250b3NvLmNvbS9tYWx3YXJlIC1PdXRGaWxlIEM6XG1hbHdhcmUuZXhl\"\n", - "# Unpack the Base64 encoded elements\n", - "unpack_txt = b64.unpack(input_string=b64_cmd)\n", - "# Display our results and transform for easier reading\n", - "unpack_txt[1].T" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "We can also use MSTICpy to extract Indicators of Compromise (IoCs) from a dataset, this makes it easy to extract and match on a set of IoCs within our data. In the example below we take a US Cybersecurity & Infrastructure Security Agency (CISA) report and extract all domains listed in the report:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "import requests\n", - "# Set up our IoCExtract oject\n", - "ioc_extractor = iocextract.IoCExtract()\n", - "# Download our threat report\n", - "data = requests.get(\"https://www.us-cert.gov/sites/default/files/publications/AA20-099A_WHITE.stix.xml\")\n", - "# Extract domains listed in our report\n", - "iocs = ioc_extractor.extract(data.text, ioc_types=\"dns\")['dns']\n", - "# Display the first 5 iocs found in our report\n", - "list(iocs)[:5]" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn more:\n", - "There are a wide range of options when it comes to data analysis in notebooks using Python. Here are some useful resources to get you started:\n", - " - [MSITCpy DataAnalysis documentation](\"https://msticpy.readthedocs.io/en/latest/DataAnalysis.html\")\n", - " - Scikit-Learn is a popular Python ML data analysis library, which has a useful [tutorial](\"https://scikit-learn.org/stable/tutorial/basic/tutorial.html\")" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Visualizing data\n", - "Visualizing data can provide an excellent way to analyse data, identify patterns and anomalies. Python has a wide range of data visualization capabilities each of which have thier own benefits and drawbacks. We will look at some basic capabilities as well as the in-build visualizations in MSTICpy.\n", - "


\n", - "**Basic Graphs**
\n", - "Pandas and Matplotlib provide the easiest and simplest way to produce simple plots of data:" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "vis_q = \"\"\"\n", - "SigninLogs\n", - "| where TimeGenerated > ago(7d)\n", - "| sample 5\"\"\"\n", - "\n", - "# Try and query for data but if using sample data load that instead\n", - "try:\n", - " vis_data = qry_prov.exec_query(vis_q)\n", - "except FileNotFoundError:\n", - " vis_data = logons_df\n", - "\n", - "# Check we have some data in our results and if not use previously used dataset\n", - "if not isinstance(vis_data, pd.DataFrame) or vis_data.empty:\n", - " vis_data = logons_df\n", - "\n", - "# Plot up to the first 5 IP addresses\n", - "vis_data.head()['IPAddress'].value_counts().plot.bar(title=\"IP prevelence\", legend=False)\n" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "pie_df = vis_data.copy()\n", - " # If we have lots of data just plot the first 5 rows\n", - "pie_df.head()['IPAddress'].value_counts().plot.pie(legend=True)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "[Bokeh](\"https://bokeh.org/\") is a powerful visualization library that allows you to create complex, interactive visualizations. MSTICpy includes a number of pre-built visualizations using Bokeh including a timeline feature that can be used to represent events over time. You can interact with the timeline by zooming and panning, using the range selector, as well as hovering over data points to see more details." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from datetime import datetime, timedelta\n", - "# Check if we have logon data already and if not get some\n", - "if not isinstance(logons_df, pd.DataFrame) or logons_df.empty:\n", - " # set our query end time as now\n", - " end = datetime.now()\n", - " # set our query start time as 1 hour ago\n", - " start = end - timedelta(days=1)\n", - " # run query with specified start and end times\n", - " logons_df = qry_prov.Azure.list_all_signins_geo(start=start, end=end)\n", - " \n", - "display(timeline.display_timeline(logons_df.head(10), source_columns=[\"TimeGenerated\", \"ResultType\", \"UserPrincipalName\", \"IPAddress\"], group_by=\"AppDisplayName\"))" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "MSTICpy also includes a feature to allow you to map locations, this can be particularily useful when looking at the distribution of remote network connections or other events. Below we plot the locations of remote logons observed in our Azure AD data." - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [ - "from msticpy.sectools.ip_utils import convert_to_ip_entities\n", - "from msticpy.nbtools.foliummap import FoliumMap, get_map_center\n", - "\n", - "# Convert our IP addresses in string format into an ip address entity\n", - "ip_entity = entityschema.IpAddress()\n", - "ip_list = [convert_to_ip_entities(i)[0] for i in logons_df['IPAddress'].head(10)]\n", - " \n", - "# Get center location of all IP locaitons to center the map on\n", - "location = get_map_center(ip_list)\n", - "logon_map = FoliumMap(location=location, zoom_start=4)\n", - "\n", - "# Add location markers to our map and dsiplay it\n", - "if len(ip_list) > 0:\n", - " logon_map.add_ip_cluster(ip_entities=ip_list)\n", - "display(logon_map.folium_map)" - ], - "execution_count": null, - "outputs": [] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "### Learn more:\n", - " - The [Infosec Jupyterbook](\"https://infosecjupyterbook.com/\") includes a section on data visualization.\n", - " - [Bokeh Library Documentation](\"https://bokeh.org/\")\n", - " - [Matplotlib tutorial](\"https://matplotlib.org/3.2.0/tutorials/index.html\")\n", - " - [Seaborn visualization library tutorial](\"https://seaborn.pydata.org/tutorial.html\")" - ] - }, - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "---\n", - "## Conclusion\n", - "This notebook has showed you the basics of using notebooks and Azure Sentinel for security investigaitons. There are many more things possible using notebooks and it is stronly encouraged to read the material we have referenced in the learn more sections in this notebook. You can also explore the other Azure Sentinel notebooks in order to take advantage of the pre-built hunting logic, and understand other analysis techniques that are possible.
\n", - "### Appendix:\n", - " - [Jupyter Notebooks: An Introduction](\"https://realpython.com/jupyter-notebook-introduction/\")\n", - " - [Threat Hunting in the cloud with Azure Notebooks](\"https://medium.com/@maarten.goet/threat-hunting-in-the-cloud-with-azure-notebooks-supercharge-your-hunting-skills-using-jupyter-8d69218e7ca0\")\n", - " - [MSTICpy documentation](\"https://msticpy.readthedocs.io/\")\n", - " - [Azure Sentinel Notebooks documentation](\"https://docs.microsoft.com/en-us/azure/sentinel/notebooks\")\n", - " - [The Infosec Jupyterbook](\"https://infosecjupyterbook.com/introduction.html\")\n", - " - [Linux Host Explorer Notebook walkthrough](\"https://techcommunity.microsoft.com/t5/azure-sentinel/explorer-notebook-series-the-linux-host-explorer/ba-p/1138273\")\n", - " - [Why use Jupyter for Security Investigations](\"https://techcommunity.microsoft.com/t5/azure-sentinel/why-use-jupyter-for-security-investigations/ba-p/475729\")\n", - " - [Security Investigtions with Azure Sentinel & Notebooks](\"https://techcommunity.microsoft.com/t5/azure-sentinel/security-investigation-with-azure-sentinel-and-jupyter-notebooks/ba-p/432921\")\n", - " - [Pandas Documentation](\"https://pandas.pydata.org/pandas-docs/stable/user_guide/index.html\")\n", - " - [Bokeh Documentation](\"https://docs.bokeh.org/en/latest/\")" - ] - }, - { - "metadata": { - "trusted": true - }, - "cell_type": "code", - "source": [], - "execution_count": null, - "outputs": [] - } - ], - "metadata": { - "kernelspec": { - "name": "python36", - "display_name": "Python 3.6", - "language": "python" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - }, - "language_info": { - "mimetype": "text/x-python", - "nbconvert_exporter": "python", - "name": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6", - "file_extension": ".py", - "codemirror_mode": { - "version": 3, - "name": "ipython" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} \ No newline at end of file diff --git a/Guided Hunting - Anomalous Office365 Exchange Sessions.ipynb b/Guided Hunting - Anomalous Office365 Exchange Sessions.ipynb index 89b10bdd..935acf4e 100644 --- a/Guided Hunting - Anomalous Office365 Exchange Sessions.ipynb +++ b/Guided Hunting - Anomalous Office365 Exchange Sessions.ipynb @@ -90,7 +90,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -527,9 +527,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3.6", "language": "python", - "name": "python3" + "name": "python36" }, "language_info": { "codemirror_mode": { @@ -541,7 +541,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.0" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/Guided Hunting - Office365-Exploring.ipynb b/Guided Hunting - Office365-Exploring.ipynb index 93af9d2b..018ee7b9 100644 --- a/Guided Hunting - Office365-Exploring.ipynb +++ b/Guided Hunting - Office365-Exploring.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ "# Title: Office 365 Explorer\n", - "<details>\n", + "
\n", "  Details...\n", "**Notebook Version:** 1.0
\n", "**Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", @@ -18,7 +18,7 @@ "**Data Sources Required**:\n", "- Log Analytics - OfficeActivity, IPLocation, Azure Network Analytics\n", "\n", - "</details>\n", + "
\n", "\n", "Brings together a series of queries and visualizations to help you investigate the security status of Office 365 subscription and individual user activities.\n", "- The first section focuses on Tenant-Wide data queries and analysis\n", @@ -30,34 +30,12 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "toc": true + }, "source": [ - "\n", - "# Table of Contents\n", - "- [Setup and Authenticate](#setup)\n", - "- [Office 365 Activity](#o365)\n", - " - [Tenant-wide Information](#tenant_info)\n", - " - [AAD Operations - Account Modifications](#aad_ops)\n", - " - [Logon Anomalies](#logon_anomalies)\n", - " - [Activity Summary](#activity_summary)\n", - " - [Variability of IP Address for users](#ip_variability)\n", - " - [Accounts with multiple IPs and Geolocations](#acct_multi_geo)\n", - " - [User Logons with > N IP Address](#acct_multi_ips)\n", - " - [Operation Types by Location and IP](#ip_op_matrix)\n", - " - [Geolocation Map of Client IPs](#geo_map_tenant)\n", - " - [Distinct User Agent Strings in Use](#distinct_uas)\n", - " - [Graphical Activity Timeline](#op_timeline)\n", - " - [Users With largest Activity Type Count](#user_activity_counts)\n", - " - [Office User Investigation](#o365_user_inv)\n", - " - [Activity Summary](#user_act_summary)\n", - " - [Operation Breakdown for User](#user_op_count)\n", - " - [IP Count for Different User Operations](#user_ip_counts)\n", - " - [Activity Timeline](#user_act_timeline)\n", - " - [User IP GeoMap](#user_geomap)\n", - " - [Check for User IPs in Azure Network Flow Data](#ips_in_azure)\n", - " - [Rare Combinations of Country/UserAgent/Operation Type](#o365_cluster)\n", - "- [Appendices](#appendices)\n", - " - [Saving data to Excel](#appendices)\n" + "

Table of Contents

\n", + "" ] }, { @@ -65,7 +43,7 @@ "metadata": {}, "source": [ "---\n", - "### Notebook initialization\n", + "## Notebook initialization\n", "The next cell:\n", "- Checks for the correct Python version\n", "- Checks versions and optionally installs required packages\n", @@ -91,8 +69,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:58:22.794036Z", - "start_time": "2020-05-16T00:58:18.510870Z" + "end_time": "2020-06-26T23:59:19.478614Z", + "start_time": "2020-06-26T23:59:17.250092Z" } }, "outputs": [], @@ -104,7 +82,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -121,6 +99,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "extra_imports = [\n", " \"dns, reversename\",\n", @@ -148,7 +129,7 @@ }, "source": [ "### Get WorkspaceId and Authenticate to Log Analytics \n", - "<details>\n", + "
\n", "  Details...\n", "If you are using user/device authentication, run the following cell. \n", "- Click the 'Copy code to clipboard and authenticate' button.\n", @@ -168,7 +149,7 @@ "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", "On successful authentication you should see a ```popup schema``` button.\n", "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - "</details>" + "
" ] }, { @@ -176,23 +157,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:57:50.958560Z", - "start_time": "2020-05-16T00:57:50.945560Z" - } - }, - "outputs": [], - "source": [ - "# To list configured workspaces run WorkspaceConfig.list_workspaces()\n", - "# WorkspaceConfig.list_workspaces()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T01:07:40.087301Z", - "start_time": "2020-05-16T00:58:30.041315Z" + "end_time": "2020-06-26T23:59:54.546697Z", + "start_time": "2020-06-26T23:59:27.512604Z" }, "tags": [ "todo" @@ -207,19 +173,31 @@ "table_index = qry_prov.schema_tables" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Configuration\n", + "\n", + "#### `msticpyconfig.yaml` configuration File\n", + "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", + "\n", + "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "[Contents](#contents)\n", - "# Office 365 Activity" + "## Office 365 Activity" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Log Analytics Queries" + "### Log Analytics Queries and query time window" ] }, { @@ -227,8 +205,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T00:13:44.706841Z", - "start_time": "2020-05-16T00:13:44.691841Z" + "end_time": "2020-06-27T00:32:09.095417Z", + "start_time": "2020-06-27T00:32:09.027664Z" } }, "outputs": [], @@ -237,7 +215,9 @@ " display(Markdown('

Warning. Office Data not available.


'\n", " 'Either Office 365 data has not been imported into the workspace or'\n", " ' the OfficeActivity table is empty.
'\n", - " 'This workbook is not useable with the current workspace.'))" + " 'This workbook is not useable with the current workspace.'))\n", + "else:\n", + " md('Office Activity table has records available for hunting')" ] }, { @@ -245,8 +225,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:49:50.212900Z", - "start_time": "2020-05-16T01:49:50.137902Z" + "end_time": "2020-06-27T00:00:02.384265Z", + "start_time": "2020-06-27T00:00:02.302204Z" } }, "outputs": [], @@ -263,8 +243,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:52:48.738859Z", - "start_time": "2020-05-16T01:52:48.733859Z" + "end_time": "2020-06-27T00:00:07.416773Z", + "start_time": "2020-06-27T00:00:07.406273Z" } }, "outputs": [], @@ -309,8 +289,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:52:58.127604Z", - "start_time": "2020-05-16T01:52:52.998622Z" + "end_time": "2020-06-27T00:00:19.867160Z", + "start_time": "2020-06-27T00:00:17.317726Z" } }, "outputs": [], @@ -342,8 +322,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:17:05.322665Z", - "start_time": "2020-05-16T01:17:05.298668Z" + "end_time": "2020-06-27T00:00:27.023842Z", + "start_time": "2020-06-27T00:00:26.990387Z" } }, "outputs": [], @@ -362,21 +342,21 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:21:12.670387Z", - "start_time": "2020-05-16T01:21:03.701387Z" + "end_time": "2020-06-27T00:02:38.220632Z", + "start_time": "2020-06-27T00:02:38.196469Z" } }, "outputs": [], "source": [ "import math\n", "multi_ip_users = unique_ip_op_ua[unique_ip_op_ua[\"ClientIPCount\"] > 1]\n", - "if len(unique_ip_op_ua) > 0:\n", + "if len(multi_ip_users) > 0:\n", " height = max(math.log10(len(multi_ip_users.UserId.unique())) * 10, 8)\n", " aspect = 10 / height\n", " user_ip_op = sns.catplot(x=\"ClientIPCount\", y=\"UserId\", hue='Operation', data=multi_ip_users, height=height, aspect=aspect)\n", " md('Variability of IP Address Usage by user')\n", "else:\n", - " md('No IP Addresses')" + " md('No users with multiple IP addresses')" ] }, { @@ -392,8 +372,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:26:11.150486Z", - "start_time": "2020-05-16T01:26:10.871487Z" + "end_time": "2020-06-27T00:02:48.941771Z", + "start_time": "2020-06-27T00:02:44.876978Z" } }, "outputs": [], @@ -455,8 +435,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:26:26.039785Z", - "start_time": "2020-05-16T01:26:26.025787Z" + "end_time": "2020-06-27T00:02:53.533522Z", + "start_time": "2020-06-27T00:02:53.520578Z" } }, "outputs": [], @@ -478,8 +458,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:27:22.043604Z", - "start_time": "2020-05-16T01:26:39.421506Z" + "end_time": "2020-06-27T00:03:01.285844Z", + "start_time": "2020-06-27T00:02:58.782751Z" } }, "outputs": [], @@ -560,8 +540,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:27:44.018242Z", - "start_time": "2020-05-16T01:27:42.735241Z" + "end_time": "2020-06-27T00:03:05.569063Z", + "start_time": "2020-06-27T00:03:05.057550Z" } }, "outputs": [], @@ -606,8 +586,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:27:55.044908Z", - "start_time": "2020-05-16T01:27:51.212910Z" + "end_time": "2020-06-27T00:03:11.829518Z", + "start_time": "2020-06-27T00:03:09.986019Z" } }, "outputs": [], @@ -634,8 +614,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:35:06.031920Z", - "start_time": "2020-05-16T01:35:05.353920Z" + "end_time": "2020-06-27T00:03:15.304316Z", + "start_time": "2020-06-27T00:03:15.175812Z" } }, "outputs": [], @@ -654,10 +634,13 @@ " 'MailboxLogin']\n", " office_ops = office_ops[office_ops.Operation.isin(limit_op_types)]\n", " \n", - " sns.catplot(data=office_ops, y='Account', x='OperationCount', \n", - " hue='Operation', aspect=2)\n", - " display(office_ops.pivot_table('OperationCount', index=['Account'], \n", - " columns='Operation')) #.style.bar(color='orange', align='mid'))" + " if len(office_ops) > 0:\n", + " sns.catplot(data=office_ops, y='Account', x='OperationCount', \n", + " hue='Operation', aspect=2)\n", + " display(office_ops.pivot_table('OperationCount', index=['Account'], \n", + " columns='Operation')) #.style.bar(color='orange', align='mid'))\n", + " else:\n", + " md('no categorical data to plot')" ] }, { @@ -665,8 +648,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:35:21.811305Z", - "start_time": "2020-05-16T01:35:21.567307Z" + "end_time": "2020-06-27T00:05:58.742811Z", + "start_time": "2020-06-27T00:05:58.694824Z" } }, "outputs": [], @@ -695,8 +678,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:35:36.322501Z", - "start_time": "2020-05-16T01:35:36.272495Z" + "end_time": "2020-06-27T00:06:02.119250Z", + "start_time": "2020-06-27T00:06:02.032121Z" } }, "outputs": [], @@ -711,15 +694,15 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:35:42.273892Z", - "start_time": "2020-05-16T01:35:42.112892Z" + "end_time": "2020-06-27T00:06:03.023848Z", + "start_time": "2020-06-27T00:06:02.983335Z" } }, "outputs": [], "source": [ "distinct_users = office_ops_df[['UserId']].sort_values('UserId')['UserId'].str.lower().drop_duplicates().tolist()\n", "distinct_users\n", - "user_select = nbwidgets.SelectString(description='Select User Id', item_list=distinct_users, auto_display=True)\n", + "user_select = nbwidgets.SelectItem(description='Select User Id', item_list=distinct_users, auto_display=True)\n", " # (items=distinct_users)" ] }, @@ -736,8 +719,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:36:57.613215Z", - "start_time": "2020-05-16T01:36:28.890742Z" + "end_time": "2020-06-27T00:06:10.366715Z", + "start_time": "2020-06-27T00:06:08.663607Z" } }, "outputs": [], @@ -776,8 +759,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:37:09.274863Z", - "start_time": "2020-05-16T01:37:07.067302Z" + "end_time": "2020-06-27T00:06:15.221357Z", + "start_time": "2020-06-27T00:06:14.703434Z" } }, "outputs": [], @@ -804,8 +787,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:37:24.368273Z", - "start_time": "2020-05-16T01:37:24.066273Z" + "end_time": "2020-06-27T00:06:18.926471Z", + "start_time": "2020-06-27T00:06:18.687117Z" } }, "outputs": [], @@ -832,8 +815,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:39:53.519748Z", - "start_time": "2020-05-16T01:39:53.048747Z" + "end_time": "2020-06-27T00:06:23.188010Z", + "start_time": "2020-06-27T00:06:22.814167Z" } }, "outputs": [], @@ -859,8 +842,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:39:59.097697Z", - "start_time": "2020-05-16T01:39:58.876675Z" + "end_time": "2020-06-27T00:06:27.528551Z", + "start_time": "2020-06-27T00:06:27.470982Z" } }, "outputs": [], @@ -905,8 +888,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:46:23.169433Z", - "start_time": "2020-05-16T01:46:23.150433Z" + "end_time": "2020-06-27T00:06:33.023802Z", + "start_time": "2020-06-27T00:06:33.019298Z" } }, "outputs": [], @@ -925,8 +908,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:44:23.645211Z", - "start_time": "2020-05-16T01:44:20.392588Z" + "end_time": "2020-06-27T00:06:39.582431Z", + "start_time": "2020-06-27T00:06:37.614815Z" } }, "outputs": [], @@ -1022,8 +1005,8 @@ "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2020-05-16T01:47:49.358082Z", - "start_time": "2020-05-16T01:47:14.392043Z" + "end_time": "2020-06-27T00:07:07.310221Z", + "start_time": "2020-06-27T00:07:07.124636Z" } }, "outputs": [], @@ -1083,18 +1066,6 @@ "# Appendices" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration\n", - "\n", - "### `msticpyconfig.yaml` configuration File\n", - "You can configure primary and secondary TI providers and any required parameters in the `msticpyconfig.yaml` file. This is read from the current directory or you can set an environment variable (`MSTICPYCONFIG`) pointing to its location.\n", - "\n", - "To configure this file see the [ConfigureNotebookEnvironment notebook](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1188,14 +1159,14 @@ "number_sections": false, "sideBar": true, "skip_h1_title": true, - "title_cell": "Table of Contents2", + "title_cell": "Table of Contents", "title_sidebar": "Contents", - "toc_cell": false, + "toc_cell": true, "toc_position": { "height": "calc(100% - 180px)", "left": "10px", "top": "150px", - "width": "351px" + "width": "230.17px" }, "toc_section_display": true, "toc_window_display": true diff --git a/Guided Investigation - Anomaly Lookup.ipynb b/Guided Investigation - Anomaly Lookup.ipynb index d00b9ff8..22c40a31 100644 --- a/Guided Investigation - Anomaly Lookup.ipynb +++ b/Guided Investigation - Anomaly Lookup.ipynb @@ -154,7 +154,7 @@ "nbconvert_exporter": "python", "name": "python", "pygments_lexer": "ipython3", - "version": "3.6.6", + "version": "3.6.7", "file_extension": ".py", "codemirror_mode": { "version": 3, diff --git a/Guided Investigation - MDATP Webshell Alerts.ipynb b/Guided Investigation - MDATP Webshell Alerts.ipynb index b60a8cda..ffde034e 100644 --- a/Guided Investigation - MDATP Webshell Alerts.ipynb +++ b/Guided Investigation - MDATP Webshell Alerts.ipynb @@ -75,7 +75,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -90,8 +90,11 @@ " importlib.reload(sys.modules[\"msticpy\"])\n", " else:\n", " import msticpy\n", - " check_mp_ver(MSTICPY_REQ_VERSION)\n", + " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "nbinit.init_notebook(\n", " namespace=globals(),\n", @@ -272,9 +275,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "command_investigation_query = f'''\n", @@ -478,9 +479,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "attackerip = iis_data['AttackerIP']\n", @@ -721,9 +720,9 @@ ], "metadata": { "kernelspec": { - "display_name": "webshell_investigation", + "display_name": "Python 3.6", "language": "python", - "name": "webshell_investigation" + "name": "python36" }, "language_info": { "codemirror_mode": { @@ -735,7 +734,14 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.6.7" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } } }, "nbformat": 4, diff --git a/Guided Investigation - Process-Alerts.ipynb b/Guided Investigation - Process-Alerts.ipynb index fdcf6362..64ab2c4a 100644 --- a/Guided Investigation - Process-Alerts.ipynb +++ b/Guided Investigation - Process-Alerts.ipynb @@ -5,7 +5,7 @@ "metadata": {}, "source": [ "# Alert Investigation - Windows Process Alerts\n", - "<details>\n", + "
\n", "  Details...\n", "**Notebook Version:** 1.1
\n", "\n", @@ -18,7 +18,7 @@ " - OTX (https://otx.alienvault.com/)\n", " - VirusTotal (https://www.virustotal.com/)\n", " - XForce (https://www.ibm.com/security/xforce)\n", - "</details>\n", + "
\n", "\n", "This notebook is intended for triage and investigation of security alerts related to process execution. It is specifically targeted at alerts triggered by suspicious process activity on Windows hosts. " ] @@ -73,12 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:10:54.830029Z", - "start_time": "2020-05-16T02:10:50.255047Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", @@ -88,7 +83,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -105,6 +100,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "\n", "nbinit.init_notebook(namespace=globals());" @@ -116,7 +114,7 @@ "source": [ "[Contents](#toc)\n", "### Get WorkspaceId and Authenticate to Log Analytics \n", - "<details>\n", + "
\n", "  Details...\n", "If you are using user/device authentication, run the following cell. \n", "- Click the 'Copy code to clipboard and authenticate' button.\n", @@ -136,7 +134,7 @@ "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", "On successful authentication you should see a ```popup schema``` button.\n", "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - "</details>" + "
" ] }, { @@ -152,12 +150,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:11:20.062429Z", - "start_time": "2020-05-16T02:10:57.597153Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Authentication\n", @@ -180,12 +173,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:11:21.408605Z", - "start_time": "2020-05-16T02:11:21.347607Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "alert_q_times = nbwidgets.QueryTime(\n", @@ -197,12 +185,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:07.827669Z", - "start_time": "2020-05-16T02:14:02.907988Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "alert_list = qry_prov.SecurityAlert.list_alerts(\n", @@ -237,16 +220,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:19.661858Z", - "start_time": "2020-05-16T02:14:19.614856Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "get_alert = None\n", - "alert_select = nbwidgets.AlertSelector(alerts=alert_list, action=nbdisplay.display_alert)\n", + "alert_select = nbwidgets.SelectAlert(alerts=alert_list, action=nbdisplay.format_alert)\n", "alert_select.display()" ] }, @@ -266,12 +244,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:38.285057Z", - "start_time": "2020-05-16T02:14:38.264058Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Extract entities and properties into a SecurityAlert class\n", @@ -295,12 +268,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:41.613471Z", - "start_time": "2020-05-16T02:14:41.251019Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Draw the graph using Networkx/Matplotlib\n", @@ -327,12 +295,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:46.099473Z", - "start_time": "2020-05-16T02:14:46.032476Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to the time of our alert\n", @@ -344,12 +307,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:50.654284Z", - "start_time": "2020-05-16T02:14:48.146513Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if not security_alert.primary_host:\n", @@ -407,12 +365,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:14:57.587177Z", - "start_time": "2020-05-16T02:14:57.110178Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Draw a graph of this (add to entity graph)\n", @@ -438,23 +391,18 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:15:03.233694Z", - "start_time": "2020-05-16T02:15:03.182692Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "def disp_full_alert(alert):\n", " global related_alert\n", " related_alert = SecurityAlert(alert)\n", - " nbdisplay.display_alert(related_alert, show_entities=True)\n", + " return nbdisplay.format_alert(related_alert, show_entities=True)\n", "\n", "if related_alerts is not None and not related_alerts.empty:\n", " related_alerts['CompromisedEntity'] = related_alerts['Computer']\n", " print('Selected alert is available as \\'related_alert\\' variable.')\n", - " rel_alert_select = nbwidgets.AlertSelector(alerts=related_alerts, action=disp_full_alert)\n", + " rel_alert_select = nbwidgets.SelectAlert(alerts=related_alerts, action=disp_full_alert)\n", " rel_alert_select.display()\n", "else:\n", " md('No related alerts found.', styles=[\"bold\",\"green\"])" @@ -493,13 +441,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:15:11.683710Z", - "start_time": "2020-05-16T02:15:11.632710Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to the time of our alert\n", @@ -510,12 +452,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:18:46.782959Z", - "start_time": "2020-05-16T02:18:43.483058Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if security_alert.data_family.name != \"WindowsSecurity\":\n", @@ -588,13 +525,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:19:16.391952Z", - "start_time": "2020-05-16T02:19:16.269953Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# Show timeline of events\n", @@ -636,12 +567,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:19:46.286319Z", - "start_time": "2020-05-16T02:19:44.261867Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from msticpy.sectools.eventcluster import dbcluster_events, add_process_features\n", @@ -700,12 +626,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:19:52.943265Z", - "start_time": "2020-05-16T02:19:51.149746Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Looking at the variability of commandlines and process image paths\n", @@ -727,13 +648,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:00.562015Z", - "start_time": "2020-05-16T02:19:57.711493Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "if 'clus_events' in locals() and not clus_events.empty:\n", @@ -748,12 +663,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:03.856018Z", - "start_time": "2020-05-16T02:20:03.843018Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Look at clusters for individual process names\n", @@ -777,12 +687,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:06.531933Z", - "start_time": "2020-05-16T02:20:06.352934Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Show timeline of events - clustered events\n", @@ -807,12 +712,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:12.962663Z", - "start_time": "2020-05-16T02:20:11.763218Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "process = security_alert.primary_process\n", @@ -848,12 +748,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:15.419954Z", - "start_time": "2020-05-16T02:20:15.359955Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "ioc_extractor = IoCExtract()\n", @@ -897,12 +792,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:19.183556Z", - "start_time": "2020-05-16T02:20:19.165556Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if source_processes is not None:\n", @@ -945,12 +835,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:20:32.758293Z", - "start_time": "2020-05-16T02:20:22.825297Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from msticpy.sectools.tiproviders.ti_provider_base import TISeverity\n", @@ -984,13 +869,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:17.223752Z", - "start_time": "2020-05-16T02:21:17.162753Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to the time of our alert\n", @@ -1003,12 +882,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:22.127025Z", - "start_time": "2020-05-16T02:21:19.300028Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# This query needs a commandline parameter which isn't supplied\n", @@ -1067,13 +941,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:24.995564Z", - "start_time": "2020-05-16T02:21:24.938564Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "# set the origin time to the time of our alert\n", @@ -1097,12 +965,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:30.636724Z", - "start_time": "2020-05-16T02:21:28.813548Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "logon_id = security_alert.get_logon_id()\n", @@ -1131,12 +994,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:33.574131Z", - "start_time": "2020-05-16T02:21:31.528726Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from msticpy.sectools.eventcluster import dbcluster_events, add_process_features, _string_score\n", @@ -1176,76 +1034,24 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:39.050530Z", - "start_time": "2020-05-16T02:21:39.008528Z" - } - }, + "metadata": {}, "outputs": [], "source": [ - "select_logon_type = widgets.Select(options=nbdisplay._WIN_LOGON_TYPE_MAP.values(), layout=widgets.Layout(height=\"200px\"))\n", - "num_items = widgets.IntSlider(min=1, max=200, value=10, description=\"# logons\")\n", - "df_output1 = widgets.Output()\n", - "df_output2 = widgets.Output()\n", - "\n", - "def display_logons(host_logons, order_column, number_shown, output, title, ascending=True):\n", - " pivot_df = (\n", - " host_logons[[\"Account\", \"LogonType\", \"EventID\"]]\n", - " .astype({'LogonType': 'int32'})\n", - " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", - " left_on=\"LogonType\", right_index=True)\n", - " .drop(columns=\"LogonType\")\n", - " .groupby([\"Account\", \"LogonTypeDesc\"])\n", - " .count()\n", - " .unstack()\n", - " .rename(columns={\"EventID\": \"LogonCount\"})\n", - " .fillna(0)\n", - " )\n", - " \n", - " with output:\n", - " if ('LogonCount', order_column) in pivot_df.columns:\n", - " md(title)\n", - " display(\n", - " pivot_df\n", - " [pivot_df[(\"LogonCount\", order_column)] > 0]\n", - " .sort_values((\"LogonCount\", order_column), ascending=ascending)\n", - " .head(number_shown)\n", - " .style\n", - " .background_gradient(cmap=\"viridis\", low=0.5, high=0)\n", - " .format(\"{0:0>3.0f}\")\n", - " )\n", - " else:\n", - " md(f\"No logons of type {order_column}\")\n", - "\n", - " \n", - "def show_logons(evt):\n", - " del evt\n", - " logon_type = select_logon_type.value\n", - " list_size = num_items.value\n", - " df_output1.clear_output()\n", - " df_output2.clear_output()\n", - " display_logons(\n", - " host_logons,\n", - " order_column=logon_type, \n", - " number_shown=list_size, \n", - " output=df_output1, \n", - " title=\"Most Frequent Logons\",\n", - " ascending=False,\n", - " )\n", - " display_logons(\n", - " host_logons,\n", - " order_column=logon_type, \n", - " number_shown=list_size, \n", - " output=df_output2, \n", - " title=\"Rarest Logons\",\n", - " ascending=True\n", - " )\n", - " \n", - "select_logon_type.observe(show_logons, names=\"value\")\n", - "ctrls = widgets.HBox([select_logon_type, num_items])\n", - "outputs = widgets.HBox([df_output1, df_output2])\n", - "display(widgets.VBox([ctrls, outputs]))" + "display(\n", + " host_logons[[\"Account\", \"LogonType\", \"EventID\"]]\n", + " .astype({'LogonType': 'int32'})\n", + " .merge(right=pd.Series(data=nbdisplay._WIN_LOGON_TYPE_MAP, name=\"LogonTypeDesc\"),\n", + " left_on=\"LogonType\", right_index=True)\n", + " .drop(columns=\"LogonType\")\n", + " .groupby([\"Account\", \"LogonTypeDesc\"])\n", + " .count()\n", + " .unstack()\n", + " .rename(columns={\"EventID\": \"LogonCount\"})\n", + " .fillna(0)\n", + " .style\n", + " .background_gradient(cmap=\"viridis\", low=0.5, high=0)\n", + " .format(\"{0:0>3.0f}\")\n", + ")" ] }, { @@ -1262,12 +1068,7 @@ "cell_type": "code", "execution_count": null, "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:21:52.253650Z", - "start_time": "2020-05-16T02:21:50.510974Z" - }, - "hidden": true, - "scrolled": true + "hidden": true }, "outputs": [], "source": [ @@ -1282,9 +1083,20 @@ " \n", "\n", "if failedLogons is None or failedLogons.empty:\n", - " md(f'No logon failures recorded for this host between {security_alert.StartTimeUtc} and {security_alert.EndTimeUtc}', styles=[\"bold\",\"blue\"])\n", + " md(\n", + " f\"\"\"\n", + " No logon failures recorded for this host between\n", + " {security_alert.StartTimeUtc} and {security_alert.EndTimeUtc}.\n", + " \"\"\",\n", + " styles=[\"bold\",\"blue\"]\n", + " )\n", "else:\n", - " md('Failed Logons observed for the host:')\n", + " md(\n", + " f\"\"\"Failed Logons observed for the host between \n", + " {security_alert.StartTimeUtc} and {security_alert.EndTimeUtc} :\n", + " \"\"\",\n", + " styles=[\"bold\"]\n", + " )\n", " display(failedLogons)" ] }, @@ -1301,12 +1113,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-13T20:45:29.434121Z", - "start_time": "2020-02-13T20:45:29.427150Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "print('List of current DataFrames in Notebook')\n", @@ -1363,11 +1170,11 @@ ], "metadata": { "hide_input": false, + "history": [], "kernel_info": { "name": "python3" }, "kernelspec": { - "display_name": "Python 3.6", "language": "python", "name": "python36" }, @@ -1381,7 +1188,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.10" }, "latex_envs": { "LaTeX_envs_menu_present": true, @@ -1422,6 +1229,7 @@ "toc_section_display": true, "toc_window_display": true }, + "uuid": "3e8f75f2-2a40-4170-b5f5-c5cec2a483c0", "varInspector": { "cols": { "lenName": 16, @@ -1457,13 +1265,6 @@ "_Feature" ], "window_display": false - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } } }, "nbformat": 4, diff --git a/Guided Triage - Alerts.ipynb b/Guided Triage - Alerts.ipynb index d7934ba0..e2e84870 100644 --- a/Guided Triage - Alerts.ipynb +++ b/Guided Triage - Alerts.ipynb @@ -7,16 +7,16 @@ }, "source": [ "

Table of Contents

\n", - "" + "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Example Alert Triage Notebook\n", + "# Alert Triage Notebook\n", "\n", - "**Notebook Version:** 1.0
\n", + "**Notebook Version:** 1.1
\n", "**Python Version:** Python 3.6 (including Python 3.6 - AzureML)
\n", "**Data Sources Required:** SecurityAlerts
\n", "\n", @@ -32,9 +32,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Notebook Setup\n", + "---\n", + "### Notebook initialization\n", + "The next cell:\n", + "- Checks for the correct Python version\n", + "- Checks versions and optionally installs required packages\n", + "- Imports the required packages into the notebook\n", + "- Sets a number of configuration options.\n", "\n", - "If this is your first time running this Notebook please run the cell below before proceeding to ensure you have the required packages installed correctly. " + "This should complete without errors. If you encounter errors or warnings look at the following two notebooks:\n", + "- [TroubleShootingNotebooks](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/TroubleShootingNotebooks.ipynb)\n", + "- [ConfiguringNotebookEnvironment](https://github.com/Azure/Azure-Sentinel-Notebooks/blob/master/ConfiguringNotebookEnvironment.ipynb)\n", + "\n", + "If you are running in the Azure Sentinel Notebooks environment (Azure Notebooks or Azure ML) you can run live versions of these notebooks:\n", + "- [Run TroubleShootingNotebooks](./TroubleShootingNotebooks.ipynb)\n", + "- [Run ConfiguringNotebookEnvironment](./ConfiguringNotebookEnvironment.ipynb)\n", + "\n", + "You may also need to do some additional configuration to successfully use functions such as Threat Intelligence service lookup and Geo IP lookup. \n", + "There are more details about this in the `ConfiguringNotebookEnvironment` notebook and in these documents:\n", + "- [msticpy configuration](https://msticpy.readthedocs.io/en/latest/getting_started/msticpyconfig.html)\n", + "- [Threat intelligence provider configuration](https://msticpy.readthedocs.io/en/latest/data_acquisition/TIProviders.html#configuration-file)" ] }, { @@ -48,27 +65,54 @@ }, "outputs": [], "source": [ - "#Check that the Notebook kernel is Pytyhon 3.6\n", + "from pathlib import Path\n", + "import os\n", "import sys\n", - "MIN_REQ_PYTHON = (3,6)\n", - "if sys.version_info < MIN_REQ_PYTHON:\n", - " print('Check the Kernel->Change Kernel menu and ensure that Python 3.6')\n", - " print('or later is selected as the active kernel.')\n", - " sys.exit(\"Python %s.%s or later is required.\\n\" % MIN_REQ_PYTHON)\n", + "import warnings\n", + "from IPython.display import display, HTML, Markdown\n", + "\n", + "REQ_PYTHON_VER=(3, 6)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", + "\n", + "display(HTML(\"

Starting Notebook setup...

\"))\n", + "if Path(\"./utils/nb_check.py\").is_file():\n", + " from utils.nb_check import check_python_ver, check_mp_ver\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\n", + " try:\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\n", + " except ImportError:\n", + " !pip install --upgrade msticpy\n", + " if \"msticpy\" in sys.modules:\n", + " importlib.reload(sys.modules[\"msticpy\"])\n", + " else:\n", + " import msticpy\n", + " check_mp_ver(REQ_MSTICPY_VER)\n", + " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", + "from msticpy.nbtools import nbinit\n", + "extra_imports = [\n", + " \"whois\", \n", + " \"datetime,,dt\",\n", + " \"msticpy.nbtools.foliummap, get_center_ip_entities\",\n", + " \"msticpy.nbtools.observationlist, Observations\",\n", + " \"msticpy.nbtools.observationlist, Observation\",\n", + " \"msticpy.sectools.ip_utils, convert_to_ip_entities\"\n", + "]\n", + "nbinit.init_notebook(\n", + " namespace=globals(),\n", + " additional_packages=[\"IPWhois\", \"tldextract\", \"python-whois\"],\n", + " extra_imports=extra_imports,\n", "\n", - "# Install required packages for this Notebook\n", - "!pip install msticpy --upgrade --user\n", - "!pip install python-whois --user --upgrade\n", - "!pip install tqdm --upgrade --user\n", - "!pip install IPWhois --upgrade --user\n", - "!pip install tldextract --upgrade --user" + "pd.options.mode.chained_assignment = None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Import the required packages and initialize a set of required entities and properties:" + "Initialize TI and Observation list" ] }, { @@ -83,33 +127,10 @@ }, "outputs": [], "source": [ - "#Import required packages\n", - "print('Importing python packages....')\n", - "import whois\n", - "import numpy as np\n", - "import datetime as dt\n", - "import ipywidgets as widgets\n", - "import pandas as pd\n", - "print('Importing msticpy packages...')\n", - "from msticpy.sectools import *\n", - "from msticpy.nbtools import *\n", - "pd.set_option('display.max_rows', 100)\n", - "pd.set_option('display.max_columns', 50)\n", - "pd.set_option('display.max_colwidth', 100)\n", - "%env KQLMAGIC_LOAD_MODE=silent\n", - "WIDGET_DEFAULTS = {'layout': widgets.Layout(width=\"900px\"),\n", - " 'style': {'description_width': 'initial'}}\n", - "from msticpy.nbtools.foliummap import FoliumMap, get_center_ip_entities\n", - "from msticpy.nbtools.observationlist import Observations, Observation\n", + "# Initialize observations and TI modules\n", "summary = Observations()\n", - "from msticpy.data.data_providers import QueryProvider\n", "ti = TILookup()\n", - "from msticpy.nbtools.utility import md, md_warn\n", - "pd.options.mode.chained_assignment = None\n", - "from msticpy.nbtools.wsconfig import WorkspaceConfig\n", - "from msticpy.sectools.ip_utils import convert_to_ip_entities\n", - "\n", - "print('Imports complete')" + "print('Observation summary and TILookup loaded.')" ] }, { @@ -425,9 +446,10 @@ "#Display full alert details when selected\n", "def show_full_alert(selected_alert):\n", " global security_alert, alert_ip_entities\n", + " output = []\n", " security_alert = SecurityAlert(\n", " rel_alert_select.selected_alert)\n", - " nbdisplay.display_alert(security_alert, show_entities=True) \n", + " output.append(nbdisplay.format_alert(security_alert, show_entities=True))\n", " ioc_list = []\n", " if security_alert['Entities'] is not None:\n", " for entity in security_alert['Entities']:\n", @@ -437,7 +459,7 @@ " ioc_list.append(entity['Url'])\n", " if len(ioc_list) > 0:\n", " ti_data = ti.lookup_iocs(data=ioc_list, providers=ti_prov_use)\n", - " display(ti_data[['Ioc','IocType','Provider','Result','Severity','Details']].reset_index().style.applymap(color_cells).hide_index())\n", + " output.append(ti_data[['Ioc','IocType','Provider','Result','Severity','Details']].reset_index().style.applymap(color_cells).hide_index())\n", " ti_ips = ti_data[ti_data['IocType'] == 'ipv4']\n", " # If we have IP entities try and plot these on a map\n", " if not ti_ips.empty:\n", @@ -445,18 +467,21 @@ " center = get_center_ip_entities(ip_ents)\n", " ip_map = FoliumMap(location=center, zoom_start=4)\n", " ip_map.add_ip_cluster(ip_ents, color='red')\n", - " display(ip_map)\n", + " output.append(ip_map)\n", + " else:\n", + " output.append(\"\")\n", " else:\n", - " md(\"No IoCs\")\n", + " output.append(\"No IoCs\")\n", " else:\n", - " md(\"No IoCs\")\n", + " output.append(\"No Entities with IoCs\")\n", + " return output\n", " \n", "# Show selected alert when selected\n", "if isinstance(alerts, pd.DataFrame) and not alerts.empty:\n", " ti_data = None\n", " md('Click on alert to view details.', \"large\")\n", - " rel_alert_select = nbwidgets.AlertSelector(alerts=selected_alert_type,\n", - " action=show_full_alert)\n", + " rel_alert_select = nbwidgets.SelectAlert(alerts=selected_alert_type,\n", + " action=show_full_alert)\n", " rel_alert_select.display()\n", " # Add alert details to summary.\n", " if ti_data is not None:\n", @@ -609,7 +634,7 @@ ], "metadata": { "hide_input": false, -"kernelspec": { + "kernelspec": { "display_name": "Python 3.6", "language": "python", "name": "python36" diff --git a/Notebook Template.ipynb b/Notebook Template.ipynb index d7eb9da1..4eb9bd7a 100644 --- a/Notebook Template.ipynb +++ b/Notebook Template.ipynb @@ -63,12 +63,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:00:38.505687Z", - "start_time": "2020-05-16T02:00:31.727307Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", @@ -78,7 +73,7 @@ "from IPython.display import display, HTML, Markdown\n", "\n", "REQ_PYTHON_VER=(3, 6)\n", - "REQ_MSTICPY_VER=(0, 5, 0)\n", + "REQ_MSTICPY_VER=(0, 6, 0)\n", "\n", "display(HTML(\"

Starting Notebook setup...

\"))\n", "if Path(\"./utils/nb_check.py\").is_file():\n", @@ -95,6 +90,9 @@ " import msticpy\n", " check_mp_ver(REQ_MSTICPY_VER)\n", " \n", + "\n", + "# If not using Azure Notebooks, install msticpy with\n", + "# !pip install msticpy\n", "from msticpy.nbtools import nbinit\n", "nbinit.init_notebook(\n", " namespace=globals(),\n", @@ -108,7 +106,7 @@ "source": [ "[Contents](#toc)\n", "### Get WorkspaceId and Authenticate to Log Analytics \n", - "<details>\n", + "
\n", " Details...\n", "If you are using user/device authentication, run the following cell. \n", "- Click the 'Copy code to clipboard and authenticate' button.\n", @@ -128,7 +126,7 @@ "Note: you may occasionally see a JavaScript error displayed at the end of the authentication - you can safely ignore this.
\n", "On successful authentication you should see a ```popup schema``` button.\n", "To find your Workspace Id go to [Log Analytics](https://ms.portal.azure.com/#blade/HubsExtension/Resources/resourceType/Microsoft.OperationalInsights%2Fworkspaces). Look at the workspace properties to find the ID.\n", - "</details>" + "
" ] }, { @@ -148,12 +146,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:01:11.022700Z", - "start_time": "2020-05-16T02:00:49.394760Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "# Authentication\n", @@ -166,12 +159,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:03:12.112983Z", - "start_time": "2020-05-16T02:03:12.055984Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "query_scope = nbwidgets.QueryTime(auto_display=True)" @@ -187,12 +175,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-05-16T02:03:25.227614Z", - "start_time": "2020-05-16T02:03:21.291120Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "qry_prov.SecurityAlert.list_alerts(query_scope)" @@ -277,13 +260,6 @@ "_Feature" ], "window_display": false - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } } }, "nbformat": 4, diff --git a/Sample-Notebooks/Example - Guided Hunting - Office365-Exploring.ipynb b/Sample-Notebooks/Example - Guided Hunting - Office365-Exploring.ipynb index 07b38b3f..2fae3078 100644 --- a/Sample-Notebooks/Example - Guided Hunting - Office365-Exploring.ipynb +++ b/Sample-Notebooks/Example - Guided Hunting - Office365-Exploring.ipynb @@ -7721,7 +7721,7 @@ "source": [ "distinct_users = office_ops_df[['UserId']].sort_values('UserId')['UserId'].str.lower().drop_duplicates().tolist()\n", "distinct_users\n", - "user_select = mas.SelectString(description='Select User Id', item_list=distinct_users, auto_display=True)\n", + "user_select = mas.SelectItem(description='Select User Id', item_list=distinct_users, auto_display=True)\n", " # (items=distinct_users)" ] }, diff --git a/Sample-Notebooks/Example - Guided Investigation - Process-Alerts.ipynb b/Sample-Notebooks/Example - Guided Investigation - Process-Alerts.ipynb index 8e94e5f3..23b1ea93 100644 --- a/Sample-Notebooks/Example - Guided Investigation - Process-Alerts.ipynb +++ b/Sample-Notebooks/Example - Guided Investigation - Process-Alerts.ipynb @@ -1074,7 +1074,7 @@ } ], "source": [ - "alert_select = mas.AlertSelector(alerts=alert_list, action=nbdisp.display_alert)\n", + "alert_select = mas.SelectAlert(alerts=alert_list, action=nbdisp.display_alert)\n", "alert_select.display()" ] }, @@ -1989,7 +1989,7 @@ "if related_alerts is not None and not related_alerts.empty:\n", " related_alerts['CompromisedEntity'] = related_alerts['Computer']\n", " print('Selected alert is available as \\'related_alert\\' variable.')\n", - " rel_alert_select = mas.AlertSelector(alerts=related_alerts, action=disp_full_alert)\n", + " rel_alert_select = mas.SelectAlert(alerts=related_alerts, action=disp_full_alert)\n", " rel_alert_select.display()\n", "else:\n", " display(Markdown('No related alerts found.'))" diff --git a/Sample-Notebooks/Example - Step-by-Step Linux-Windows-Office Investigation.ipynb b/Sample-Notebooks/Example - Step-by-Step Linux-Windows-Office Investigation.ipynb index abc0f263..ae1bfe0c 100644 --- a/Sample-Notebooks/Example - Step-by-Step Linux-Windows-Office Investigation.ipynb +++ b/Sample-Notebooks/Example - Step-by-Step Linux-Windows-Office Investigation.ipynb @@ -1244,7 +1244,7 @@ " global security_alert\n", " security_alert = nbtools.SecurityAlert(alert_select.selected_alert)\n", " nbtools.disp.display_alert(security_alert, show_entities=True)\n", - "alert_select = nbtools.AlertSelector(alerts=alert_list, action=show_full_alert)\n", + "alert_select = nbtools.SelectAlert(alerts=alert_list, action=show_full_alert)\n", "alert_select.display()" ] }, @@ -8635,7 +8635,7 @@ " return host_logons.query('TargetUserName == @acct and LogonType == @logon_type'\n", " ' and TargetLogonId == @logon_id')\n", " \n", - "logon_wgt = nbtools.SelectString(description='Select logon cluster to examine', \n", + "logon_wgt = nbtools.SelectItem(description='Select logon cluster to examine', \n", " item_list=items, height='200px', width='100%', auto_display=True)" ] }, @@ -11874,4 +11874,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/SigmaRuleImporter.ipynb b/SigmaRuleImporter.ipynb index 3e0ac4bd..2864c332 100644 --- a/SigmaRuleImporter.ipynb +++ b/SigmaRuleImporter.ipynb @@ -1,589 +1,644 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Import and convert Neo23x0 Sigma scripts\n", - "ianhelle@microsoft.com\n", - "\n", - "This notebook is a is a quick and dirty Sigma to Log Analytics converter.\n", - "It uses the modules from sigmac package to do the conversion.\n", - "\n", - "Only a subset of the Sigma rules are convertible currently. Failure to convert\n", - "could be for one or more of these reasons:\n", - "- known limitations of the converter\n", - "- mismatch between the syntax expressible in Sigma and KQL\n", - "- data sources referenced in Sigma rules do not yet exist in Azure Sentinel\n", - "\n", - "The sigmac tool is downloadable as a package from PyPi but since we are downloading\n", - "the rules from the repo, we also copy and import the package from the repo source.\n", - "\n", - "After conversion you can use an interactive browser to step through the rules and\n", - "view (and copy/save) the KQL equivalents. You can also take the conversion results and \n", - "use them in another way (e.g.bulk save to files).\n", - "\n", - "The notebook is all somewhat experimental and offered as-is without any guarantees" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Download and unzip the Sigma repo" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import requests\n", - "# Download the repo ZIP\n", - "sigma_git_url = 'https://github.com/Neo23x0/sigma/archive/master.zip'\n", - "r = requests.get(sigma_git_url)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from ipywidgets import widgets, Layout\n", - "import os\n", - "from pathlib import Path\n", - "def_path = Path.joinpath(Path(os.getcwd()), \"sigma\")\n", - "path_wgt = widgets.Text(value=str(def_path), \n", - " description='Path to extract to zipped repo files: ', \n", - " layout=Layout(width='50%'),\n", - " style={'description_width': 'initial'})\n", - "path_wgt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import zipfile\n", - "import io\n", - "repo_zip = io.BytesIO(r.content)\n", - "\n", - "zip_archive = zipfile.ZipFile(repo_zip, mode='r')\n", - "zip_archive.extractall(path=path_wgt.value)\n", - "RULES_REL_PATH = 'sigma-master/rules'\n", - "rules_root = Path(path_wgt.value) / RULES_REL_PATH" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Check that we have the files\n", - "You should see a folder with folders such as application, apt, windows..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "%ls {rules_root}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Convert Sigma Files to Log Analytics Kql queries" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "# Read the Sigma YAML file paths into a dict and make a\n", - "# a copy for the target Kql queries\n", - "from pathlib import Path\n", - "from collections import defaultdict\n", - "import copy\n", - "\n", - "def get_rule_files(rules_root):\n", - " file_dict = defaultdict(dict)\n", - " for file in Path(rules_root).resolve().rglob(\"*.yml\"):\n", - " rel_path = Path(file).relative_to(rules_root)\n", - " path_key = '.'.join(rel_path.parent.parts)\n", - " file_dict[path_key][rel_path.name] = file\n", - " return file_dict\n", - " \n", - "sigma_dict = get_rule_files(rules_root)\n", - "kql_dict = copy.deepcopy(sigma_dict)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Add downloaded sigmac tool to sys.path and import Sigmac functions\n", - "import os\n", - "import sys\n", - "module_path = os.path.abspath(os.path.join('sigma/sigma-master/tools'))\n", - "if module_path not in sys.path:\n", - " sys.path.append(module_path)\n", - "from sigma.parser.collection import SigmaCollectionParser\n", - "from sigma.parser.exceptions import SigmaCollectionParseError, SigmaParseError\n", - "from sigma.configuration import SigmaConfiguration, SigmaConfigurationChain\n", - "from sigma.config.exceptions import SigmaConfigParseError, SigmaRuleFilterParseException\n", - "from sigma.filter import SigmaRuleFilter\n", - "import sigma.backends.discovery as backends\n", - "from sigma.backends.base import BackendOptions\n", - "from sigma.backends.exceptions import BackendError, NotSupportedError, PartialMatchError, FullMatchError" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "# Sigma to Log Analytics Conversion\n", - "import yaml\n", - "_LA_MAPPINGS = '''\n", - "fieldmappings:\n", - " Image: NewProcessName\n", - " ParentImage: ParentProcessName\n", - " ParentCommandLine: NO_MAPPING\n", - "'''\n", - "\n", - "NOT_CONVERTIBLE = 'Not convertible'\n", - "\n", - "def sigma_to_la(file_path):\n", - " with open(file_path, 'r') as input_file:\n", - " try:\n", - " sigmaconfigs = SigmaConfigurationChain()\n", - " sigmaconfig = SigmaConfiguration(_LA_MAPPINGS)\n", - " sigmaconfigs.append(sigmaconfig)\n", - " backend_options = BackendOptions(None, None)\n", - " backend = backends.getBackend('ala')(sigmaconfigs, backend_options)\n", - " parser = SigmaCollectionParser(input_file, sigmaconfigs, None)\n", - " results = parser.generate(backend)\n", - " kql_result = ''\n", - " for result in results:\n", - " kql_result += result\n", - " except (NotImplementedError, NotSupportedError):\n", - " kql_result = NOT_CONVERTIBLE\n", - " input_file.seek(0,0)\n", - " sigma_txt = input_file.read()\n", - " if not kql_result == NOT_CONVERTIBLE:\n", - " try:\n", - " kql_header = \"\\n\".join(get_sigma_properties(sigma_txt))\n", - " kql_result = kql_header + \"\\n\" + kql_result\n", - " except Exception as e:\n", - " print(\"exception reading sigma YAML: \", e)\n", - " print(sigma_txt, kql_result, sep='\\n')\n", - " return sigma_txt, kql_result\n", - "\n", - "sigma_keys = ['title', 'description', 'tags', 'status', \n", - " 'author', 'logsource', 'falsepositives', 'level']\n", - "\n", - "def get_sigma_properties(sigma_rule):\n", - " sigma_docs = yaml.load_all(sigma_rule, Loader=yaml.SafeLoader)\n", - " sigma_rule_dict = next(sigma_docs)\n", - " for prop in sigma_keys:\n", - " yield get_property(prop, sigma_rule_dict)\n", - "\n", - "def get_property(name, sigma_rule_dict):\n", - " sig_prop = sigma_rule_dict.get(name, 'na')\n", - " if isinstance(sig_prop, dict):\n", - " sig_prop = ' '.join([f\"{k}: {v}\" for k, v in sig_prop.items()])\n", - " return f\"// {name}: {sig_prop}\"\n", - " \n", - " \n", - "_KQL_FILTERS = {\n", - " 'date': ' | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end}) ',\n", - " 'host': ' | where Computer has {host_name} '\n", - "}\n", - "\n", - "def insert_at(source, insert, find_sub):\n", - " pos = source.find(find_sub)\n", - " if pos != -1:\n", - " return source[:pos] + insert + source[pos:]\n", - " else:\n", - " return source + insert\n", - " \n", - "def add_filter_clauses(source, **kwargs):\n", - " if \"{\" in source or \"}\" in source:\n", - " source = (\"// Warning: embedded braces in source. Please edit if necessary.\\n\"\n", - " + source)\n", - " source = source.replace('{', '{{').replace('}', '}}')\n", - " if kwargs.get('host', False):\n", - " source = insert_at(source, _KQL_FILTERS['host'], '|')\n", - " if kwargs.get('date', False):\n", - " source = insert_at(source, _KQL_FILTERS['date'], '|')\n", - " return source\n", - "\n", - "\n", - "# Run the conversion\n", - "conv_counter = {}\n", - "for categ, sources in sigma_dict.items():\n", - " src_converted = 0\n", - " for file_name, file_path in sources.items():\n", - " sigma, kql = sigma_to_la(file_path)\n", - " kql_dict[categ][file_name] = (sigma, kql)\n", - " if not kql == NOT_CONVERTIBLE:\n", - " src_converted += 1\n", - " conv_counter[categ] = (len(sources), src_converted)\n", - " \n", - "print(\"Conversion statistics\")\n", - "print(\"-\" * len(\"Conversion statistics\"))\n", - "print('\\n'.join([f'{categ}: rules: {counter[0]}, converted: {counter[1]}'\n", - " for categ, counter in conv_counter.items()]))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Display the results in an interactive browser" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "from ipywidgets import widgets, Layout\n", - "\n", - "# Browser Functions\n", - "def on_cat_value_change(change):\n", - " queries_w.options = kql_dict[change['new']].keys()\n", - " queries_w.value = queries_w.options[0]\n", - "\n", - "def on_query_value_change(change):\n", - " if view_qry_check.value:\n", - " qry_text = kql_dict[sub_cats_w.value][queries_w.value][1]\n", - " if \"Not convertible\" not in qry_text:\n", - " qry_text = add_filter_clauses(qry_text,\n", - " date=add_date_filter_check.value,\n", - " host=add_host_filter_check.value)\n", - " query_text_w.value = qry_text.replace('|', '\\n|')\n", - " orig_text_w.value = kql_dict[sub_cats_w.value][queries_w.value][0]\n", - "\n", - "def on_view_query_value_change(change):\n", - " vis = 'visible' if view_qry_check.value else 'hidden'\n", - " on_query_value_change(None)\n", - " query_text_w.layout.visibility = vis\n", - " orig_text_w.layout.visibility = vis\n", - "\n", - "# Function defs for ExecuteQuery cell below\n", - "def click_exec_hqry(b):\n", - " global qry_results\n", - " query_name = queries_w.value\n", - " query_cat = sub_cats_w.value\n", - " query_text = query_text_w.value\n", - " query_text = query_text.format(**qry_wgt.query_params)\n", - "\n", - " disp_results(query_text)\n", - " \n", - "def disp_results(query_text):\n", - " out_wgt.clear_output()\n", - " with out_wgt:\n", - " print(\"Running query...\", end=' ')\n", - " qry_results = execute_kql_query(query_text)\n", - " print(f'done. {len(qry_results)} rows returned.')\n", - " display(qry_results)\n", - " \n", - "exec_hqry_button = widgets.Button(description=\"Execute query..\")\n", - "out_wgt = widgets.Output() #layout=Layout(width='100%', height='200px', visiblity='visible'))\n", - "exec_hqry_button.on_click(click_exec_hqry)\n", - "\n", - "# Browser widget setup\n", - "categories = list(sorted(kql_dict.keys()))\n", - "sub_cats_w = widgets.Select(options=categories, \n", - " description='Category : ',\n", - " layout=Layout(width='30%', height='120px'),\n", - " style = {'description_width': 'initial'})\n", - "\n", - "queries_w = widgets.Select(options = kql_dict[categories[0]].keys(),\n", - " description='Query : ',\n", - " layout=Layout(width='30%', height='120px'),\n", - " style = {'description_width': 'initial'})\n", - "\n", - "query_text_w = widgets.Textarea(\n", - " value='',\n", - " description='Kql Query:',\n", - " layout=Layout(width='100%', height='300px', visiblity='hidden'),\n", - " disabled=False)\n", - "orig_text_w = widgets.Textarea(\n", - " value='',\n", - " description='Sigma Query:',\n", - " layout=Layout(width='100%', height='250px', visiblity='hidden'),\n", - " disabled=False)\n", - "\n", - "query_text_w.layout.visibility = 'hidden'\n", - "orig_text_w.layout.visibility = 'hidden'\n", - "sub_cats_w.observe(on_cat_value_change, names='value')\n", - "queries_w.observe(on_query_value_change, names='value')\n", - "\n", - "view_qry_check = widgets.Checkbox(description=\"View query\", value=True)\n", - "add_date_filter_check = widgets.Checkbox(description=\"Add date filter\", value=False)\n", - "add_host_filter_check = widgets.Checkbox(description=\"Add host filter\", value=False)\n", - "\n", - "view_qry_check.observe(on_view_query_value_change, names='value')\n", - "add_date_filter_check.observe(on_view_query_value_change, names='value')\n", - "add_host_filter_check.observe(on_view_query_value_change, names='value')\n", - "# view_qry_button.on_click(click_exec_hqry)\n", - "# display(exec_hqry_button);\n", - "\n", - "vbox_opts = widgets.VBox([view_qry_check, add_date_filter_check, add_host_filter_check])\n", - "hbox = widgets.HBox([sub_cats_w, queries_w, vbox_opts])\n", - "vbox = widgets.VBox([hbox, orig_text_w, query_text_w])\n", - "on_view_query_value_change(None)\n", - "display(vbox)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Click the `Execute query` button to run the currently display query\n", - "**Notes:**\n", - "- To run the queries, first authenticate to Log Analytics (scroll down and execute remaining cells in the notebook)\n", - "- If you added a date filter to the query set the date range below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "from msticpy.nbtools.nbwidgets import QueryTime\n", - "qry_wgt = QueryTime(units='days', before=5, after=0, max_before=30, max_after=10)\n", - "vbox = widgets.VBox([exec_hqry_button, out_wgt])\n", - "display(vbox)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Set Query Time bounds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "qry_wgt.display()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Authenticate to Azure Sentinel" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def clean_kql_comments(query_string):\n", - " \"\"\"Cleans\"\"\"\n", - " import re\n", - " return re.sub(r'(//[^\\n]+)', '', query_string, re.MULTILINE).replace('\\n', '').strip()\n", - "\n", - "def execute_kql_query(query_string):\n", - " if not query_string or len(query_string.strip()) == 0:\n", - " print('No query supplied')\n", - " return None\n", - " src_query = clean_kql_comments(query_string)\n", - " result = get_ipython().run_cell_magic('kql', line='', cell=src_query)\n", - " \n", - " if result is not None and result.completion_query_info['StatusCode'] == 0:\n", - " results_frame = result.to_dataframe()\n", - " return results_frame\n", - " return []" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from msticpy.nbtools.wsconfig import WorkspaceConfig\n", - "from msticpy.nbtools import kql, GetEnvironmentKey\n", - "\n", - "ws_config_file = 'config.json'\n", - "try:\n", - " ws_config = WorkspaceConfig(ws_config_file)\n", - " print('Found config file')\n", - " for cf_item in ['tenant_id', 'subscription_id', 'resource_group', 'workspace_id', 'workspace_name']:\n", - " print(cf_item, ws_config[cf_item])\n", - "except:\n", - " ws_config = None\n", - "\n", - "ws_id = GetEnvironmentKey(env_var='WORKSPACE_ID',\n", - " prompt='Log Analytics Workspace Id:')\n", - "if ws_config:\n", - " ws_id.value = ws_config['workspace_id']\n", - "ws_id.display()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "try:\n", - " WORKSPACE_ID = select_ws.value\n", - "except NameError:\n", - " try:\n", - " WORKSPACE_ID = ws_id.value\n", - " except NameError:\n", - " WORKSPACE_ID = None\n", - " \n", - "if not WORKSPACE_ID:\n", - " raise ValueError('No workspace selected.')\n", - "\n", - "kql.load_kql_magic()\n", - "\n", - "%kql loganalytics://code().workspace(WORKSPACE_ID)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Save All Converted Files" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "path_save_wgt = widgets.Text(value=str(def_path) + \"_kql_out\",\n", - " description='Path to save KQL files: ',\n", - " layout=Layout(width='50%'),\n", - " style={'description_width': 'initial'})\n", - "path_save_wgt" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "root = Path(path_save_wgt.value)\n", - "root.mkdir(exist_ok=True)\n", - "for categ, kql_files in kql_dict.items():\n", - " sub_dir = root.joinpath(categ)\n", - " \n", - " for file_name, contents in kql_files.items():\n", - " kql_txt = contents[1]\n", - " if not kql_txt == NOT_CONVERTIBLE:\n", - " sub_dir.mkdir(exist_ok=True)\n", - " file_path = sub_dir.joinpath(file_name.replace('.yml', '.kql'))\n", - " with open(file_path, 'w') as output_file:\n", - " output_file.write(kql_txt)\n", - " print(f\"Saved {file_path}\")\n" - ] - } - ], - "metadata": { - "hide_input": false, - "kernelspec": { - "display_name": "Python 3.6", - "language": "python", - "name": "python36" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Import and convert Neo23x0 Sigma scripts\n", + "ianhelle@microsoft.com\n", + "\n", + "This notebook is a is a quick and dirty Sigma to Log Analytics converter.\n", + "It uses the modules from sigmac package to do the conversion.\n", + "\n", + "Only a subset of the Sigma rules are convertible currently. Failure to convert\n", + "could be for one or more of these reasons:\n", + "- known limitations of the converter\n", + "- mismatch between the syntax expressible in Sigma and KQL\n", + "- data sources referenced in Sigma rules do not yet exist in Azure Sentinel\n", + "\n", + "The sigmac tool is downloadable as a package from PyPi but since we are downloading\n", + "the rules from the repo, we also copy and import the package from the repo source.\n", + "\n", + "After conversion you can use an interactive browser to step through the rules and\n", + "view (and copy/save) the KQL equivalents. You can also take the conversion results and \n", + "use them in another way (e.g.bulk save to files).\n", + "\n", + "The notebook is all somewhat experimental and offered as-is without any guarantees" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Download and unzip the Sigma repo" + ], + "metadata": {} }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " + { + "cell_type": "code", + "source": [ + "from pathlib import Path\r\n", + "import sys\r\n", + "from IPython.display import display, HTML, Markdown\r\n", + "\r\n", + "REQ_PYTHON_VER=(3, 6)\r\n", + "REQ_MSTICPY_VER=(0, 6, 0)\r\n", + "\r\n", + "display(HTML(\"

Starting Notebook setup...

\"))\r\n", + "if Path(\"./utils/nb_check.py\").is_file():\r\n", + " from utils.nb_check import check_python_ver, check_mp_ver\r\n", + "\r\n", + " check_python_ver(min_py_ver=REQ_PYTHON_VER)\r\n", + " try:\r\n", + " check_mp_ver(min_msticpy_ver=REQ_MSTICPY_VER)\r\n", + " except ImportError:\r\n", + " !pip install --upgrade msticpy\r\n", + " if \"msticpy\" in sys.modules:\r\n", + " importlib.reload(sys.modules[\"msticpy\"])\r\n", + " else:\r\n", + " import msticpy\r\n", + " check_mp_ver(REQ_MSTICPY_VER)\r\n", + "\r\n", + "# If not using Azure Notebooks, install msticpy with\r\n", + "# !pip install msticpy\r\n", + "\r\n", + "from msticpy.nbtools import nbinit\r\n", + "nbinit.init_notebook(namespace=globals());" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "collapsed": true, + "jupyter": { + "source_hidden": false, + "outputs_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + } + }, + { + "cell_type": "code", + "source": [ + "import requests\n", + "# Download the repo ZIP\n", + "sigma_git_url = 'https://github.com/Neo23x0/sigma/archive/master.zip'\n", + "r = requests.get(sigma_git_url)" + ], + "outputs": [], + "execution_count": null, + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "from ipywidgets import widgets, Layout\n", + "import os\n", + "from pathlib import Path\n", + "def_path = Path.joinpath(Path(os.getcwd()), \"sigma\")\n", + "path_wgt = widgets.Text(value=str(def_path), \n", + " description='Path to extract to zipped repo files: ', \n", + " layout=Layout(width='50%'),\n", + " style={'description_width': 'initial'})\n", + "path_wgt" + ], + "outputs": [], + "execution_count": null, + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "RULES_REL_PATH = 'sigma-master/rules'\r\n", + "rules_root = Path(path_wgt.value) / RULES_REL_PATH" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "collapsed": true, + "jupyter": { + "source_hidden": false, + "outputs_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + } + }, + { + "cell_type": "code", + "source": [ + "import zipfile\n", + "import io\n", + "repo_zip = io.BytesIO(r.content)\n", + "\n", + "zip_archive = zipfile.ZipFile(repo_zip, mode='r')\n", + "zip_archive.extractall(path=path_wgt.value)\n" + ], + "outputs": [], + "execution_count": null, + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "### Check that we have the files\n", + "You should see a folder with folders such as application, apt, windows..." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "%ls {rules_root}" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "scrolled": true + } + }, + { + "cell_type": "markdown", + "source": [ + "## Convert Sigma Files to Log Analytics Kql queries" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "# Read the Sigma YAML file paths into a dict and make a\n", + "# a copy for the target Kql queries\n", + "from pathlib import Path\n", + "from collections import defaultdict\n", + "import copy\n", + "\n", + "def get_rule_files(rules_root):\n", + " file_dict = defaultdict(dict)\n", + " for file in Path(rules_root).resolve().rglob(\"*.yml\"):\n", + " rel_path = Path(file).relative_to(rules_root)\n", + " path_key = '.'.join(rel_path.parent.parts)\n", + " file_dict[path_key][rel_path.name] = file\n", + " return file_dict\n", + " \n", + "sigma_dict = get_rule_files(rules_root)\n", + "kql_dict = copy.deepcopy(sigma_dict)\n" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "scrolled": false + } + }, + { + "cell_type": "code", + "source": [ + "# Add downloaded sigmac tool to sys.path and import Sigmac functions\n", + "import os\n", + "import sys\n", + "module_path = os.path.abspath(os.path.join('sigma/sigma-master/tools'))\n", + "if module_path not in sys.path:\n", + " sys.path.append(module_path)\n", + "from sigma.parser.collection import SigmaCollectionParser\n", + "from sigma.parser.exceptions import SigmaCollectionParseError, SigmaParseError\n", + "from sigma.configuration import SigmaConfiguration, SigmaConfigurationChain\n", + "from sigma.config.exceptions import SigmaConfigParseError, SigmaRuleFilterParseException\n", + "from sigma.filter import SigmaRuleFilter\n", + "import sigma.backends.discovery as backends\n", + "from sigma.backends.base import BackendOptions\n", + "from sigma.backends.exceptions import BackendError, NotSupportedError, PartialMatchError, FullMatchError" + ], + "outputs": [], + "execution_count": null, + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "# Sigma to Log Analytics Conversion\n", + "import yaml\n", + "_LA_MAPPINGS = '''\n", + "fieldmappings:\n", + " Image: NewProcessName\n", + " ParentImage: ParentProcessName\n", + " ParentCommandLine: NO_MAPPING\n", + "'''\n", + "\n", + "NOT_CONVERTIBLE = 'Not convertible'\n", + "\n", + "def sigma_to_la(file_path):\n", + " with open(file_path, 'r') as input_file:\n", + " try:\n", + " sigmaconfigs = SigmaConfigurationChain()\n", + " sigmaconfig = SigmaConfiguration(_LA_MAPPINGS)\n", + " sigmaconfigs.append(sigmaconfig)\n", + " backend_options = BackendOptions(None, None)\n", + " backend = backends.getBackend('ala')(sigmaconfigs, backend_options)\n", + " parser = SigmaCollectionParser(input_file, sigmaconfigs, None)\n", + " results = parser.generate(backend)\n", + " kql_result = ''\n", + " for result in results:\n", + " kql_result += result\n", + " except (NotImplementedError, NotSupportedError, TypeError):\n", + " kql_result = NOT_CONVERTIBLE\n", + " input_file.seek(0,0)\n", + " sigma_txt = input_file.read()\n", + " if not kql_result == NOT_CONVERTIBLE:\n", + " try:\n", + " kql_header = \"\\n\".join(get_sigma_properties(sigma_txt))\n", + " kql_result = kql_header + \"\\n\" + kql_result\n", + " except Exception as e:\n", + " print(\"exception reading sigma YAML: \", e)\n", + " print(sigma_txt, kql_result, sep='\\n')\n", + " return sigma_txt, kql_result\n", + "\n", + "sigma_keys = ['title', 'description', 'tags', 'status', \n", + " 'author', 'logsource', 'falsepositives', 'level']\n", + "\n", + "def get_sigma_properties(sigma_rule):\n", + " sigma_docs = yaml.load_all(sigma_rule, Loader=yaml.SafeLoader)\n", + " sigma_rule_dict = next(sigma_docs)\n", + " for prop in sigma_keys:\n", + " yield get_property(prop, sigma_rule_dict)\n", + "\n", + "def get_property(name, sigma_rule_dict):\n", + " sig_prop = sigma_rule_dict.get(name, 'na')\n", + " if isinstance(sig_prop, dict):\n", + " sig_prop = ' '.join([f\"{k}: {v}\" for k, v in sig_prop.items()])\n", + " return f\"// {name}: {sig_prop}\"\n", + " \n", + " \n", + "_KQL_FILTERS = {\n", + " 'date': ' | where TimeGenerated >= datetime({start}) and TimeGenerated <= datetime({end}) ',\n", + " 'host': ' | where Computer has {host_name} '\n", + "}\n", + "\n", + "def insert_at(source, insert, find_sub):\n", + " pos = source.find(find_sub)\n", + " if pos != -1:\n", + " return source[:pos] + insert + source[pos:]\n", + " else:\n", + " return source + insert\n", + " \n", + "def add_filter_clauses(source, **kwargs):\n", + " if \"{\" in source or \"}\" in source:\n", + " source = (\"// Warning: embedded braces in source. Please edit if necessary.\\n\"\n", + " + source)\n", + " source = source.replace('{', '{{').replace('}', '}}')\n", + " if kwargs.get('host', False):\n", + " source = insert_at(source, _KQL_FILTERS['host'], '|')\n", + " if kwargs.get('date', False):\n", + " source = insert_at(source, _KQL_FILTERS['date'], '|')\n", + " return source\n", + "\n", + "\n", + "# Run the conversion\n", + "print(\"Converting rules\")\n", + "conv_counter = {}\n", + "for categ, sources in sigma_dict.items():\n", + " src_converted = 0\n", + " print(\"\\n\", categ, end=\"\")\n", + " for file_name, file_path in sources.items():\n", + " try:\n", + " sigma, kql = sigma_to_la(file_path)\n", + " print(\".\", end=\"\")\n", + " except:\n", + " print(f\"Error converting {file_name} ({file_path})\")\n", + " continue\n", + " kql_dict[categ][file_name] = (sigma, kql)\n", + " if not kql == NOT_CONVERTIBLE:\n", + " src_converted += 1\n", + " conv_counter[categ] = (len(sources), src_converted)\n", + "\n", + "print(\"\\nConversion statistics\")\n", + "print(\"-\" * len(\"Conversion statistics\"))\n", + "print('\\n'.join([f'{categ}: rules: {counter[0]}, converted: {counter[1]}'\n", + " for categ, counter in conv_counter.items()]))" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "scrolled": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Display the results in an interactive browser" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "from ipywidgets import widgets, Layout\n", + "\n", + "# Browser Functions\n", + "def on_cat_value_change(change):\n", + " queries_w.options = kql_dict[change['new']].keys()\n", + " queries_w.value = queries_w.options[0]\n", + "\n", + "def on_query_value_change(change):\n", + " if view_qry_check.value:\n", + " qry_text = kql_dict[sub_cats_w.value][queries_w.value][1]\n", + " if \"Not convertible\" not in qry_text:\n", + " qry_text = add_filter_clauses(qry_text,\n", + " date=add_date_filter_check.value,\n", + " host=add_host_filter_check.value)\n", + " query_text_w.value = qry_text.replace('|', '\\n|')\n", + " orig_text_w.value = kql_dict[sub_cats_w.value][queries_w.value][0]\n", + "\n", + "def on_view_query_value_change(change):\n", + " vis = 'visible' if view_qry_check.value else 'hidden'\n", + " on_query_value_change(None)\n", + " query_text_w.layout.visibility = vis\n", + " orig_text_w.layout.visibility = vis\n", + "\n", + "# Function defs for ExecuteQuery cell below\n", + "def click_exec_hqry(b):\n", + " global qry_results\n", + " query_name = queries_w.value\n", + " query_cat = sub_cats_w.value\n", + " query_text = query_text_w.value\n", + " query_text = query_text.format(**qry_wgt.query_params)\n", + "\n", + " disp_results(query_text)\n", + " \n", + "def disp_results(query_text):\n", + " out_wgt.clear_output()\n", + " with out_wgt:\n", + " print(\"Running query...\", end=' ')\n", + " qry_results = execute_kql_query(query_text)\n", + " print(f'done. {len(qry_results)} rows returned.')\n", + " display(qry_results)\n", + " \n", + "exec_hqry_button = widgets.Button(description=\"Execute query..\")\n", + "out_wgt = widgets.Output() #layout=Layout(width='100%', height='200px', visiblity='visible'))\n", + "exec_hqry_button.on_click(click_exec_hqry)\n", + "\n", + "# Browser widget setup\n", + "categories = list(sorted(kql_dict.keys()))\n", + "sub_cats_w = widgets.Select(options=categories, \n", + " description='Category : ',\n", + " layout=Layout(width='30%', height='120px'),\n", + " style = {'description_width': 'initial'})\n", + "\n", + "queries_w = widgets.Select(options = kql_dict[categories[0]].keys(),\n", + " description='Query : ',\n", + " layout=Layout(width='30%', height='120px'),\n", + " style = {'description_width': 'initial'})\n", + "\n", + "query_text_w = widgets.Textarea(\n", + " value='',\n", + " description='Kql Query:',\n", + " layout=Layout(width='100%', height='300px', visiblity='hidden'),\n", + " disabled=False)\n", + "orig_text_w = widgets.Textarea(\n", + " value='',\n", + " description='Sigma Query:',\n", + " layout=Layout(width='100%', height='250px', visiblity='hidden'),\n", + " disabled=False)\n", + "\n", + "query_text_w.layout.visibility = 'hidden'\n", + "orig_text_w.layout.visibility = 'hidden'\n", + "sub_cats_w.observe(on_cat_value_change, names='value')\n", + "queries_w.observe(on_query_value_change, names='value')\n", + "\n", + "view_qry_check = widgets.Checkbox(description=\"View query\", value=True)\n", + "add_date_filter_check = widgets.Checkbox(description=\"Add date filter\", value=False)\n", + "add_host_filter_check = widgets.Checkbox(description=\"Add host filter\", value=False)\n", + "\n", + "view_qry_check.observe(on_view_query_value_change, names='value')\n", + "add_date_filter_check.observe(on_view_query_value_change, names='value')\n", + "add_host_filter_check.observe(on_view_query_value_change, names='value')\n", + "# view_qry_button.on_click(click_exec_hqry)\n", + "# display(exec_hqry_button);\n", + "\n", + "vbox_opts = widgets.VBox([view_qry_check, add_date_filter_check, add_host_filter_check])\n", + "hbox = widgets.HBox([sub_cats_w, queries_w, vbox_opts])\n", + "vbox = widgets.VBox([hbox, orig_text_w, query_text_w])\n", + "on_view_query_value_change(None)\n", + "display(vbox)" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "scrolled": false, + "collapsed": false + } + }, + { + "cell_type": "markdown", + "source": [ + "## Click the `Execute query` button below to run the currently display query\n", + "**Notes:**\n", + "- To run the queries, first authenticate to Azure Sentinel\n", + "- If you added a date filter to the query set the date range below in the control below" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "### Authenticate to Azure Sentinel and Set Query Time bounds" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "from msticpy.nbtools.nbwidgets import QueryTime\r\n", + "from IPython.display import display\r\n", + "from msticpy.data import QueryProvider\r\n", + "from msticpy.common.wsconfig import WorkspaceConfig\r\n", + "ws_config = WorkspaceConfig()\r\n", + "qry_prov = QueryProvider(\"LogAnalytics\")\r\n", + "qry_prov.connect(ws_config.code_connect_str)\r\n", + "\r\n", + "exec_hqry_button = widgets.Button(description=\"Execute Query\")\r\n", + "exec_hqry_button.on_click(exec_query_btn)\r\n", + "\r\n", + "qry_wgt = QueryTime(units='days', before=5, after=0, max_before=30, max_after=10)\r\n", + "\r\n", + "vbox = widgets.VBox([exec_hqry_button, out_wgt])\r\n", + "display(qry_wgt)" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "collapsed": true, + "jupyter": { + "source_hidden": false, + "outputs_hidden": false + }, + "nteract": { + "transient": { + "deleting": false + } + } + } + }, + { + "cell_type": "markdown", + "source": [ + "### Execute the Query" + ], + "metadata": { + "nteract": { + "transient": { + "deleting": false + } + } + } + }, + { + "cell_type": "code", + "source": [ + "def clean_kql_comments(query_string):\r\n", + " \"\"\"Cleans\"\"\"\r\n", + " import re\r\n", + " return re.sub(r'(//[^\\n]+)', '', query_string, re.MULTILINE).replace('\\n', '').strip()\r\n", + "\r\n", + "def execute_kql_query(query_string):\r\n", + " if not query_string or len(query_string.strip()) == 0:\r\n", + " print('No query supplied')\r\n", + " return None\r\n", + " src_query = clean_kql_comments(query_string)\r\n", + " src_query = src_query.format(start=qry_wgt.start, end=qry_wgt.end)\r\n", + " result = qry_prov.exec_query(src_query)\r\n", + " \r\n", + " return result\r\n", + "\r\n", + "disp_result = display(display_id=True)\r\n", + "\r\n", + "def exec_query_btn(btn):\r\n", + " query = query_text_w.value\r\n", + " result = execute_kql_query(query)\r\n", + " disp_result.update(result)\r\n", + "\r\n", + "display(exec_hqry_button)" + ], + "outputs": [], + "execution_count": null, + "metadata": { + "scrolled": true + } + }, + { + "cell_type": "markdown", + "source": [ + "## Save All Converted Files" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "path_save_wgt = widgets.Text(value=str(def_path) + \"_kql_out\",\n", + " description='Path to save KQL files: ',\n", + " layout=Layout(width='50%'),\n", + " style={'description_width': 'initial'})\n", + "path_save_wgt" + ], + "outputs": [], + "execution_count": null, + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "root = Path(path_save_wgt.value)\n", + "root.mkdir(exist_ok=True)\n", + "for categ, kql_files in kql_dict.items():\n", + " sub_dir = root.joinpath(categ)\n", + " \n", + " for file_name, contents in kql_files.items():\n", + " kql_txt = contents[1]\n", + " if not kql_txt == NOT_CONVERTIBLE:\n", + " sub_dir.mkdir(exist_ok=True)\n", + " file_path = sub_dir.joinpath(file_name.replace('.yml', '.kql'))\n", + " with open(file_path, 'w') as output_file:\n", + " output_file.write(kql_txt)\n", + " print(f\"Saved {file_path}\")\n" + ], + "outputs": [], + "execution_count": null, + "metadata": {} } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "name": "python3-azureml", + "language": "python", + "display_name": "Python 3.6 - AzureML" + }, + "language_info": { + "name": "python", + "version": "3.6.9", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + }, + "toc": { + "toc_position": {}, + "skip_h1_title": false, + "number_sections": false, + "title_cell": "Table of Contents", + "toc_window_display": false, + "base_numbering": 1, + "toc_section_display": true, + "title_sidebar": "Contents", + "toc_cell": false, + "nav_menu": {}, + "sideBar": true + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + }, + "kernel_info": { + "name": "python3-azureml" + }, + "nteract": { + "version": "nteract-front-end@1.0.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/TroubleShootingNotebooks.ipynb b/TroubleShootingNotebooks.ipynb index 74890402..c0bd9751 100644 --- a/TroubleShootingNotebooks.ipynb +++ b/TroubleShootingNotebooks.ipynb @@ -33,12 +33,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:21.030512Z", - "start_time": "2020-02-27T00:34:21.016520Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "import sys\n", @@ -98,13 +93,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:26.670210Z", - "start_time": "2020-02-27T00:34:21.032510Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "import importlib\n", @@ -112,7 +101,7 @@ "import warnings\n", "from IPython.display import display, HTML, Markdown\n", "\n", - "MSTICPY_REQ_VERSION = (0, 2, 7)\n", + "REQ_PYTHON_VER = (0, 2, 7)\n", "display(Markdown(\"#### Checking msticpy...\"))\n", "warn_mssg = []\n", "err_mssg = []\n", @@ -147,7 +136,7 @@ " \n", " else:\n", " setup_warn(\"msticpy missing or out-of-date.\")\n", - " display(Markdown(\"Please run `pip install --user --upgrade msticpy` to upgrade/install msticpy\"))\n", + " display(Markdown(\"Please run `pip install --upgrade msticpy` to upgrade/install msticpy\"))\n", " " ] }, @@ -164,12 +153,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:26.683205Z", - "start_time": "2020-02-27T00:34:26.671186Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "display(Markdown(\"#### Checking pandas...\"))\n", @@ -190,7 +174,7 @@ "if need_update:\n", " resp = input(\"Install the package now? (y/n)\")\n", " if resp.casefold().startswith(\"y\"):\n", - " !pip install --user --upgrade pandas\n", + " !pip install --upgrade pandas\n", " if \"pandas\" in sys.modules:\n", " importlib.reload(pd)\n", " else:\n", @@ -199,17 +183,12 @@ " \n", " else:\n", " setup_warn(\"pandas missing or out-of-date.\")\n", - " display(Markdown(\"Please run `pip install --user --upgrade pandas` to upgrade/install pandas\"))" + " display(Markdown(\"Please run `pip install --upgrade pandas` to upgrade/install pandas\"))" ] }, { "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-24T19:41:28.539607Z", - "start_time": "2020-02-24T19:41:28.536637Z" - } - }, + "metadata": {}, "source": [ "## Workspace Configuration Check\n", "\n", @@ -275,13 +254,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:26.714184Z", - "start_time": "2020-02-27T00:34:26.684179Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "import os\n", @@ -370,12 +343,7 @@ }, { "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-24T19:41:42.199685Z", - "start_time": "2020-02-24T19:41:42.196662Z" - } - }, + "metadata": {}, "source": [ "# msticpy Configuration\n", "\n", @@ -399,13 +367,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:26.742145Z", - "start_time": "2020-02-27T00:34:26.715162Z" - }, - "scrolled": true - }, + "metadata": {}, "outputs": [], "source": [ "import os\n", @@ -483,12 +445,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2020-02-27T00:34:26.758139Z", - "start_time": "2020-02-27T00:34:26.745144Z" - } - }, + "metadata": {}, "outputs": [], "source": [ "if errors:\n", @@ -522,7 +479,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.5" + "version": "3.6.7" }, "toc": { "base_numbering": 1, diff --git a/aznbsetup.sh b/aznbsetup.sh index 417b5f5c..43f0f501 100644 --- a/aznbsetup.sh +++ b/aznbsetup.sh @@ -1,7 +1,17 @@ #!/bin/bash -# Activate environment -source /home/nbuser/anaconda3_501/bin/activate +# Activate environment (add this to the end of .bashrc) +source ~/anaconda3_501/bin/activate +echo >> ~.bashrc +echo source ~/anaconda3_501/bin/activate >> .bashrc +echo Started environment setup +date +touch ~/.mpnb.lock # pip -pip install -r /home/nbuser/library/requirements.txt \ No newline at end of file +pip install --upgrade pip +pip install --disable-pip-version-check -r ~/library/requirements.txt + +rm -f ~/.mpnb.lock +echo Environment setup complete +date \ No newline at end of file diff --git a/images/nb_ipexplorer-mindmap.png b/images/nb_ipexplorer-mindmap.png new file mode 100644 index 00000000..77e6abab Binary files /dev/null and b/images/nb_ipexplorer-mindmap.png differ diff --git a/requirements.txt b/requirements.txt index 45fd7eae..cbc0127c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -msticpy>=0.5.0 -Kqlmagic>=0.1.106 -pandas>=0.25 +msticpy>=0.6.0 diff --git a/utils/check_nb_kernel.py b/utils/check_nb_kernel.py index c0aaba8a..5c6a65fb 100644 --- a/utils/check_nb_kernel.py +++ b/utils/check_nb_kernel.py @@ -1,47 +1,217 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +""" +Checker/Updater for Notebook kernelspec versions. + +check_nb_kernel.py CMD [-h] [--path PATH] [--target TARGET] [--verbose] + +CMD is one of: + {check, list, update} (default is "check") + + list - shows list of internal kernelspecs that can be used + check - checks the notebook or notebooks for comformance to kernspecs + update - updates notebook or notebooks to target kernelspec + +optional arguments: + -h, --help show this help message and exit + --path PATH, -p PATH Path search for notebooks. Can be a single file, + a directory path or a 'glob'-compatible wildcard. + (e.g. "*" for all files in current folder, "**/*" + for all files in folder and subfolders) + Defaults to current directory. + --target TARGET, -t TARGET + Target kernel spec to check or set. + Required for 'update' command + --verbose, -v Show details of all checked notebooks. Otherwise + only list notebooks with errors or updated notebooks. + +Notes +----- + +If CMD is 'update' you must specify a kernelspec target. The updated +notebook is written to the same name as the input. The old version is +saved as {input-notebook-name}.{previous-kernelspec-name} +If CMD is 'view', target is optional and it reports any notebooks +with kernelspecs different to internal kernelspecs (view with 'list' command) +as errors. + +""" import argparse from pathlib import Path +from typing import Optional, Iterable +import sys + import nbformat -PY36_KERNEL = {"name": ["python36", "python3"], "display_name": ["Python 3.6", "Python 3"], 'language': 'python'} +IP_KERNEL_SPEC = { + "python36": { + "name": "python36", + "language": "python", + "display_name": "Python 3.6", + }, + "python3": {"name": "python3", "language": "python", "display_name": "Python 3"}, + "python3-azureml": { + "name": "python3-azureml", + "language": "python", + "display_name": "Python 3.6 - AzureML", + }, +} + + +def check_notebooks(nb_path: str, k_tgts: Iterable[str], verbose: bool = False): + """Check notebooks for valid kernelspec.""" + err_count = 0 + good_count = 0 + for nbook in _get_notebook_paths(nb_path): + if ".ipynb_checkpoints" in str(nbook): + continue + nb_obj = nbformat.read(str(nbook), as_version=4.0) + kernelspec = nb_obj.get("metadata", {}).get("kernelspec", None) + if not kernelspec: + print("Error: no kernel information.") + continue + nb_ok = False + for config in k_tgts: + tgt_spec = IP_KERNEL_SPEC[config] + for k_name, k_item in kernelspec.items(): + if tgt_spec[k_name] != k_item: + break + else: + nb_ok = True + if not nb_ok: + err_count += 1 + _print_nb_header(nbook) + print("ERROR - Invalid kernelspec '" f"{kernelspec.get('name')}" "'") + print(" ", kernelspec, "\n") + continue + if verbose: + _print_nb_header(nbook) + print(f"{kernelspec['name']} ok\n") + good_count += 1 + print(f"{good_count} with no errors, {err_count} with errors") + + +def _get_notebook_paths(nb_path: str): + """Generate notebook paths.""" + if "*" in nb_path: + for glob_path in Path().glob(nb_path): + if glob_path.is_file() and glob_path.suffix.casefold() == ".ipynb": + yield glob_path + elif Path(nb_path).is_dir(): + yield from Path(nb_path).glob("*.ipynb") + elif Path(nb_path).is_file(): + yield Path(nb_path) + +def _print_nb_header(nbook_path): + print(str(nbook_path.name)) + print("-" * len(str(nbook_path.name))) + print(str(nbook_path.resolve())) -def check_notebooks(nb_path): - notebooks = Path(nb_path).glob("**/*.ipynb") - for nb_path in notebooks: - if ".ipynb_checkpoints" in str(nb_path): +def set_kernelspec(nb_path: str, k_tgt: str, verbose: bool = False): + """Update specified notebooks to `k_tgt` kernelspec.""" + changed_count = 0 + good_count = 0 + for nbook in _get_notebook_paths(nb_path): + if ".ipynb_checkpoints" in str(nbook): continue - nb = nbformat.read(str(nb_path), as_version=4.0) - kernelspec = nb.get("metadata", {}).get("kernelspec", None) - print(str(nb_path)) - print("-" * len(str(nb_path))) - nb_ok = True - for config in PY36_KERNEL: - if not kernelspec: - print("no kernel information.") - if not kernelspec[config] in PY36_KERNEL[config]: - print("Incorrect value in", config, end=". ") - print(f"Should be: '{PY36_KERNEL[config]}' Found:'{kernelspec[config]}'") - nb_ok = False - if nb_ok: - print(f"{kernelspec['name']} ok")) - else: - print() - - + with open(str(nbook), "r") as nb_read: + nb_obj = nbformat.read(nb_read, as_version=4.0) + kernelspec = nb_obj.get("metadata", {}).get("kernelspec", None) + current_kspec_name = kernelspec.get("name") + if not kernelspec: + print("Error: no kernel information.") + continue + updated = False + tgt_spec = IP_KERNEL_SPEC[k_tgt] + for k_name, k_item in kernelspec.items(): + if tgt_spec[k_name] != k_item: + updated = True + kernelspec[k_name] = tgt_spec[k_name] + if updated: + changed_count += 1 + _print_nb_header(nbook) + print( + f"Kernelspec updated from '{current_kspec_name}' to '" + f"{kernelspec.get('name')}" + "'" + ) + print(" ", kernelspec, "\n") + nbook.rename(f"{str(nbook)}.{current_kspec_name}") + nbformat.write(nb_obj, str(nbook)) + continue + if verbose: + _print_nb_header(nbook) + print(f"{kernelspec['name']} ok\n") + good_count += 1 + print(f"{good_count} with no changes, {changed_count} updated") + + def _add_script_args(): parser = argparse.ArgumentParser(description="Notebook kernelspec checker.") + parser.add_argument( + "cmd", default="check", type=str, choices=["check", "list", "update"], + ) parser.add_argument( "--path", "-p", default=".", required=False, help="Path search for notebooks." ) + parser.add_argument( + "--target", "-t", required=False, help="Target kernel spec to check or set." + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Show details of all checked notebooks.", + ) return parser +def _view_targets(): + print("Valid targets:") + for kernel, settings in IP_KERNEL_SPEC.items(): + print(f"{kernel}:") + print(" ", settings) + + # pylint: disable=invalid-name if __name__ == "__main__": arg_parser = _add_script_args() args = arg_parser.parse_args() - check_notebooks(args.path) - + if args.cmd == "list": + _view_targets() + sys.exit(0) + + krnl_tgt: Optional[str] = None + if args.target: + krnl_tgt = args.target + if krnl_tgt not in IP_KERNEL_SPEC: + print("'target' must be a valid kernelspec definition") + print("Valid kernel specs:") + _view_targets() + sys.exit(1) + + if krnl_tgt is not None: + krnl_tgts = [krnl_tgt] + else: + krnl_tgts = list(IP_KERNEL_SPEC.keys()) + + if not args.path: + print("check and update commands need a 'path' parameter.") + sys.exit(1) + if args.cmd == "check": + check_notebooks(args.path, krnl_tgts, verbose=args.verbose) + sys.exit(0) + + if args.cmd == "update": + if not krnl_tgt: + print("A kernel target must be specified with 'update'.") + sys.exit(1) + set_kernelspec(args.path, krnl_tgt, verbose=args.verbose) + sys.exit(0) diff --git a/utils/nb_check.py b/utils/nb_check.py index 1d5509d2..b9627141 100644 --- a/utils/nb_check.py +++ b/utils/nb_check.py @@ -4,32 +4,29 @@ # license information. # -------------------------------------------------------------------------- """Checker for Python and msticpy versions.""" -import importlib import os import sys -import warnings -from IPython.display import display, HTML, Markdown +from IPython.display import display, HTML + -warn_mssg = [] -err_mssg = [] MISSING_PKG_ERR = """ -

The package '{package}' is not +

The package '{package}' is not installed or has an incorrect version

Please install this now

""" MIN_PYTHON_VER_DEF = (3, 6) -MSTICPY_REQ_VERSION = (0, 5, 0) +MSTICPY_REQ_VERSION = (0, 5, 2) def check_python_ver(min_py_ver=MIN_PYTHON_VER_DEF): """ - Checks the current version of the Python kernel. - + Check the current version of the Python kernel. + Parameters ---------- min_py_ver : Tuple[int, int] Minimum Python version - + Raises ------ RuntimeError @@ -38,18 +35,22 @@ def check_python_ver(min_py_ver=MIN_PYTHON_VER_DEF): """ display(HTML("Checking Python kernel version...")) if sys.version_info < min_py_ver: - display(HTML( - """ + display( + HTML( + """

This notebook requires a different notebook (Python) kernel version.

- From the Notebook menu (above), choose Kernel then + From the Notebook menu (above), choose Kernel then Change Kernel... from the menu.
Select a Python %s.%s (or later) version kernel and then re-run this cell.

- """ % min_py_ver - )) - display(HTML( """ + % min_py_ver + ) + ) + display( + HTML( + """ Please see the TroubleShootingNotebooks in this folder for more information


@@ -58,21 +59,26 @@ def check_python_ver(min_py_ver=MIN_PYTHON_VER_DEF): ) raise RuntimeError("Python %s.%s or later kernel is required." % min_py_ver) - display(HTML( - "Python kernel version %s.%s.%s OK" % ( - sys.version_info[0], sys.version_info[1], sys.version_info[2] + display( + HTML( + "Python kernel version %s.%s.%s OK" + % (sys.version_info[0], sys.version_info[1], sys.version_info[2]) ) - )) + ) + + _check_nteract() + +# pylint: disable=import-outside-toplevel def check_mp_ver(min_msticpy_ver=MSTICPY_REQ_VERSION): """ - Checks the current version of . - + Check and optionally update the current version of msticpy. + Parameters ---------- min_py_ver : Tuple[int, int] Minimum Python version - + Raises ------ RuntimeError @@ -82,26 +88,49 @@ def check_mp_ver(min_msticpy_ver=MSTICPY_REQ_VERSION): display(HTML("Checking msticpy version...")) try: import msticpy + wrong_ver_err = "msticpy %s.%s.%s or later is needed." % min_msticpy_ver mp_version = tuple([int(v) for v in msticpy.__version__.split(".")]) if mp_version < min_msticpy_ver: - raise ImportError("msticpy %s.%s.%s or later is needed." % min_msticpy_ver) - + raise ImportError(wrong_ver_err) + except ImportError: display(HTML(MISSING_PKG_ERR.format(package="msticpy"))) - resp = input("Install? (y/n)") + resp = input("Install? (y/n)") # nosec if resp.casefold().startswith("y"): raise ImportError("Install msticpy") - else: - display(HTML( + + display( + HTML( """ -

The notebook cannot be run without - the correct version of '%s' (%s.%s.%s or later) -

- Please see the - TroubleShootingNotebooks - in this folder for more information


- """ % ("msticpy", *min_msticpy_ver) - ) +

The notebook cannot be run without + the correct version of '%s' (%s.%s.%s or later) +

+ Please see the + TroubleShootingNotebooks + in this folder for more information


+ """ + % ("msticpy", *min_msticpy_ver) ) - raise RuntimeError("msticpy %s.%s.%s or later is required." % min_msticpy_ver) - display(HTML("msticpy version %s.%s.%s OK" % mp_version)) \ No newline at end of file + ) + raise RuntimeError(wrong_ver_err) + + display(HTML("msticpy version %s.%s.%s OK" % mp_version)) + + +_NTERACT_MSSG = """ +Azure ML detected
+It looks like this notebook is running in an Azure Machine Learning workspace. +If you using the AzureML native notebook interface +(i.e. not Jupyter or Jupyter lab) we need to adjust a +setting for the UI to behave properly. +Ignoring or answering "n" will not affect the functionality of the notebook +but you may see some extraneous UI elements being displayed. +""" + + +def _check_nteract(): + if os.environ.get("USER", "").casefold() == "azureuser": + display(HTML(_NTERACT_MSSG)) + set_app = input("Configure for Azure ML Notebooks? (y/n)") # nosec + if set_app.casefold().startswith("y"): + os.environ["KQLMAGIC_NOTEBOOK_APP"] = "nteract"