From ae0b864a92cefc33593f5817fe4a6afe75e395d1 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 28 May 2025 13:02:35 -0500 Subject: feat: add jupyter notebooks folder (#11) * feat: add jupyter notebooks folder * Commit from GitHub Actions (Ruff) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- notebooks/basic_data_analysis.ipynb | 688 +++++++++++++++++++++++++++++++++++ notebooks/racf_access_analysis.ipynb | 526 ++++++++++++++++++++++++++ notebooks/sample_racf_data.txt | 54 +++ 3 files changed, 1268 insertions(+) create mode 100644 notebooks/basic_data_analysis.ipynb create mode 100644 notebooks/racf_access_analysis.ipynb create mode 100644 notebooks/sample_racf_data.txt diff --git a/notebooks/basic_data_analysis.ipynb b/notebooks/basic_data_analysis.ipynb new file mode 100644 index 0000000..6609858 --- /dev/null +++ b/notebooks/basic_data_analysis.ipynb @@ -0,0 +1,688 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2bc817b1", + "metadata": {}, + "source": [ + "\n", + "# πŸ“Š Basic Data Analysis & Visualization\n", + "\n", + "This notebook demonstrates how to load a sample dataset, perform quick exploratory analysis, group and pivot the data, and create visualizations.\n", + "\n", + "We’re using a CSV file from: https://sample-files.com/downloads/data/csv/basic-data.csv\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "a92fdfd1", + "metadata": {}, + "source": [ + "\n", + "## πŸ“¦ Install Dependencies\n", + "\n", + "If you haven't installed the required libraries, run:\n", + "\n", + "```bash\n", + "pip install pandas matplotlib seaborn\n", + "```\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b5d7308a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "Requirement already satisfied: pandas in /Users/cmc/Library/Python/3.9/lib/python/site-packages (2.2.3)\n", + "Requirement already satisfied: matplotlib in /Users/cmc/Library/Python/3.9/lib/python/site-packages (3.9.4)\n", + "Requirement already satisfied: seaborn in /Users/cmc/Library/Python/3.9/lib/python/site-packages (0.13.2)\n", + "Requirement already satisfied: numpy>=1.22.4 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (1.26.4)\n", + "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2.9.0.post0)\n", + "Requirement already satisfied: pytz>=2020.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2025.2)\n", + "Requirement already satisfied: tzdata>=2022.7 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2025.2)\n", + "Requirement already satisfied: contourpy>=1.0.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (1.3.0)\n", + "Requirement already satisfied: cycler>=0.10 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (0.12.1)\n", + "Requirement already satisfied: fonttools>=4.22.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (4.58.1)\n", + "Requirement already satisfied: kiwisolver>=1.3.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (1.4.7)\n", + "Requirement already satisfied: packaging>=20.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (24.2)\n", + "Requirement already satisfied: pillow>=8 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (11.2.1)\n", + "Requirement already satisfied: pyparsing>=2.3.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (3.2.3)\n", + "Requirement already satisfied: importlib-resources>=3.2.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (6.5.2)\n", + "Requirement already satisfied: zipp>=3.1.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from importlib-resources>=3.2.0->matplotlib) (3.21.0)\n", + "Requirement already satisfied: six>=1.5 in /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages (from python-dateutil>=2.8.2->pandas) (1.15.0)\n", + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.1.1\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49m/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip\u001b[0m\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install pandas matplotlib seaborn" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "0632140a", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "# Set plot style\n", + "sns.set(style=\"whitegrid\")" + ] + }, + { + "cell_type": "markdown", + "id": "d326b5b6", + "metadata": {}, + "source": [ + "## πŸ“‚ Load Dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "5d5de24b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
IDNameAgeCountryEmail
01Name_162Country_1email_1@example.com
12Name_248Country_2email_2@example.com
23Name_361Country_3email_3@example.com
34Name_432Country_4email_4@example.com
45Name_569Country_5email_5@example.com
\n", + "
" + ], + "text/plain": [ + " ID Name Age Country Email\n", + "0 1 Name_1 62 Country_1 email_1@example.com\n", + "1 2 Name_2 48 Country_2 email_2@example.com\n", + "2 3 Name_3 61 Country_3 email_3@example.com\n", + "3 4 Name_4 32 Country_4 email_4@example.com\n", + "4 5 Name_5 69 Country_5 email_5@example.com" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load the dataset from URL\n", + "url = \"https://sample-files.com/downloads/data/csv/basic-data.csv\"\n", + "df = pd.read_csv(url, skiprows=1)\n", + "df.columns = df.columns.str.strip()\n", + "\n", + "# Preview the data\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "e7f7c2e7", + "metadata": {}, + "source": [ + "## πŸ“Š Basic Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "aca7309e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(100, 5)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
IDNameAgeCountryEmail
count100.000000100100.000000100100
uniqueNaN100NaN10100
topNaNName_1NaNCountry_1email_1@example.com
freqNaN1NaN101
mean50.500000NaN44.530000NaNNaN
std29.011492NaN15.190012NaNNaN
min1.000000NaN18.000000NaNNaN
25%25.750000NaN32.000000NaNNaN
50%50.500000NaN43.500000NaNNaN
75%75.250000NaN59.250000NaNNaN
max100.000000NaN69.000000NaNNaN
\n", + "
" + ], + "text/plain": [ + " ID Name Age Country Email\n", + "count 100.000000 100 100.000000 100 100\n", + "unique NaN 100 NaN 10 100\n", + "top NaN Name_1 NaN Country_1 email_1@example.com\n", + "freq NaN 1 NaN 10 1\n", + "mean 50.500000 NaN 44.530000 NaN NaN\n", + "std 29.011492 NaN 15.190012 NaN NaN\n", + "min 1.000000 NaN 18.000000 NaN NaN\n", + "25% 25.750000 NaN 32.000000 NaN NaN\n", + "50% 50.500000 NaN 43.500000 NaN NaN\n", + "75% 75.250000 NaN 59.250000 NaN NaN\n", + "max 100.000000 NaN 69.000000 NaN NaN" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check shape and summary stats\n", + "print(df.shape)\n", + "df.describe(include=\"all\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "e775e42f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " ID Name Age Country Email\n", + "0 1 Name_1 62 Country_1 email_1@example.com\n", + "1 2 Name_2 48 Country_2 email_2@example.com\n", + "2 3 Name_3 61 Country_3 email_3@example.com\n", + "3 4 Name_4 32 Country_4 email_4@example.com\n", + "4 5 Name_5 69 Country_5 email_5@example.com\n", + ".. ... ... ... ... ...\n", + "95 96 Name_96 60 Country_6 email_96@example.com\n", + "96 97 Name_97 26 Country_7 email_97@example.com\n", + "97 98 Name_98 52 Country_8 email_98@example.com\n", + "98 99 Name_99 24 Country_9 email_99@example.com\n", + "99 100 Name_100 55 Country_0 email_100@example.com\n", + "\n", + "[100 rows x 5 columns]\n" + ] + } + ], + "source": [ + "print(df)" + ] + }, + { + "cell_type": "markdown", + "id": "dc10be72", + "metadata": {}, + "source": [ + "## πŸ“ˆ Grouping and Pivoting Data" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "f87ea8af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CountryRecordCount
0Country_110
1Country_210
2Country_310
3Country_410
4Country_510
\n", + "
" + ], + "text/plain": [ + " Country RecordCount\n", + "0 Country_1 10\n", + "1 Country_2 10\n", + "2 Country_3 10\n", + "3 Country_4 10\n", + "4 Country_5 10" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Example: Group by 'Country' and count number of records\n", + "country_counts = df[\"Country\"].value_counts().reset_index()\n", + "country_counts.columns = [\"Country\", \"RecordCount\"]\n", + "country_counts.head()" + ] + }, + { + "cell_type": "markdown", + "id": "f156d6de", + "metadata": {}, + "source": [ + "## πŸ“Š Visualizing Record Counts by Country" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "d644fc06", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/pf/tt0qdz214wn0q90g989_0r0h0000gn/T/ipykernel_79108/1394705902.py:3: FutureWarning: \n", + "\n", + "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n", + "\n", + " sns.barplot(data=country_counts, x='Country', y='RecordCount', palette='viridis')\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA00AAAJUCAYAAADJrc3LAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABLd0lEQVR4nO3dB5hU5dk/4BcpAnZR0VhRbBi72A12jb3X2GtsGDR2JbFi+RT7Z+899hhbRKOxa4waRQ0KdqygKCjF+V/P+f6z7uJy5BwWt8x9X9e6uzPD7DuP786e33nLaVepVCoJAACARk3T+M0AAAAEoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwDU45rvk6Y2QK0SmgBagF122SUtuuiiDT4WW2yxtNxyy6Wtttoq3X333aml++CDD7J233HHHT/72E8++SSdccYZacMNN0xLL710Wn311dP++++fXnjhhdScHnnkkXTkkUfmPuaoo45Ka6+99lRrw/fff5+uvvrqtPXWW6fll18+rbjiimmHHXZId911V7OGlv/+979pxx13bLafD9CcOjTrTwegTq9evVL//v3rvp8wYUIaPnx4dgB9xBFHpJlnnjn16dMntXYvvvhiOvDAA9Mss8ySdt1119SjR480cuTIdMstt2Th8bTTTktbbLFFs7Qtat2cPv/887T33nunjz/+OKvFUkstlX744Yf06KOPZmEtQuVJJ52U2rVr94u37YEHHkgvvfTSL/5zAVoCoQmghZh++unTMsss85Pbf/Ob36RVVlklG8Fp7aEpwtGhhx6aFlhggXTVVVelLl261N23wQYbpH333TedcMIJ2cjTbLPNlmpNjHJFUI4AGTWqWnPNNdOvfvWrdPbZZ6e11lorrbPOOs3aToBaY3oeQAs37bTTpk6dOjUYXYjRh0svvTStt9566de//nUWOK677rqf/NuY0rXllltmU+DiwPt//ud/0tixY+vuf/XVV9Nee+2VVlpppWwqYEyRi2lYVc8++2w25e7mm2/ODtbjMU8++WR230MPPZQ222yzbDQkfsYbb7zxs68l2vPpp5+mY445pkFgCtNMM006/PDD084775y++eabutvj5+20007ZVLVo52GHHZaNxFSdf/75WRsnFrfFffWnDt5///3pkEMOScsuu2w27e24445Lo0ePzh4TIzvPPfdc9hGPjdeeJ4JN1DRe/2677ZZef/31umC45JJLZgGnvjFjxmSv4eKLL270+QYPHpz++c9/Zv8/6gemqt133z2rTdeuXetuGzZsWPZ6VltttSxwx2uIkbyJ//9N/FricfFRFdMNzzvvvHT66aenVVddNXtN0Y54/mqNL7jggp/UNb6O22MKafyb+LrMawdo6YQmgBYi1quMHz++7iPWtrzzzjvp6KOPTt9++23afPPN6x77pz/9KTvIjdDyv//7v9naoFNPPTVdeOGFdY+54YYbspGLJZZYIjuYjVGcCFYnn3xydv8zzzxTt0Yl/m3cHmEk1s+8/fbbDdoW/z6eK0aBInAMGjQoO1iPg+b4mb/97W/TH//4x599jU888UQ2ghQH2I2JdVzxc6qhIULWnnvumeaaa67sQDxqEVPEtt9++/TFF18UrnFMf5x77rnTRRddlIWCv/zlL3UH8nFfTJGMjwhEUbdJidGgqEmMmkW7vvrqqyyEfPTRR9k0ynXXXTfde++9DdYgPfzww1lAm9TUw6hNmNR6qQjPUf8YdQxDhgzJwkoEwgh/Z511VhasI8BF8Cvq2muvzfpbTI+MvvCf//ynbn3Xtttum7bZZpvs66hNfF8V/W/TTTfN+mOE9zKvHaClMz0PoIV4/vnnf3KgHgfBiyyySDr33HOzkZ4wdOjQdOutt6Z+/fplQSjEdLZ47CWXXJKNysw000xZmIkD2GpIqp7xv++++9K4ceOyUaf5558/G7Fq37593fPE6FUcAMfPrIrnjGBWFc8dwefMM8/Mvl9jjTWyz/GceSJsRGiZHDGaFkEg2lT/eWO0a6ONNkpXXHFFttariJjeWA0CET5iFOuxxx7LRq969uyZTZEMjU2TrC/Wm1VrEGIkL2odoTSePzZx+Nvf/paN8Ky88sp1ATBGcSIANqY6ejbPPPNM1muJ0BYjkBF2qu2Oka9NNtkk22QjAmERM844YxYmq33hvffey0aURowYkeacc87so7HarLDCCmmPPfao+77Mawdo6Yw0AbQQEZjiQDc+4uA1wlKMuAwcOLBBYIkRojiLHyMS9Uem4vsYnYrpWRGsYiQmAlB9MboSa6MiNMXUvBghqh4kVw+cI5xNPFKx+OKL13393Xffpddee60uxFXFc/2c+FkROCZHvIbPPvssCwH1zTfffNloV5nRlIkP+CMIVKfnFTHvvPM2GC2bffbZs+eO4BsiIMQapOquhxEWn3766Wwa46RU/z9Mbn3i9cf/g2pgCh06dEgbb7xxNkoUo5NFxLS6+n2hGpIiaOep3zfKvnaAlk5oAmghpptuuuzANT5ioX9slPD1119n09O+/PLLusfFmpkQB8cRtKof1SlTsZ139THdunVr9GeNGjUqC16NbbYQt8X99dVfRxNT0eLfxu539c0xxxw/+xrjYLr+eqTGVO+vvobJbePkaGwdVZltvBtrU9Q6/n9Vnzemzj344INZyIwAEeFm4hBbX3UELqb4TUr8v622N/4/TKo28Zj668LK1qY64penft8o+9oBWjqhCaCFioPfWMMSIeKUU05pMBoUrrnmmrqRqfofMSpVfUz9sBViqlVMSZthhhmy6XyxxfXEYnQn1uVMStwXB8YT/9tqyMkT0/hiBCxGuSa1GUJMMYutv6ttmFQbq6GtukFG/RGaoqMsRUVgaaxNs846a933ERxiFOvxxx/PNqCIKYWxLmlSYhpi+Mc//tHo/TGaGOvaYrv2EFMwJ1WbEPWp1mbi4DO161P0tQO0dEITQAsWASiCxl//+te66WixhqQagKojU/ERASnWIUV4WXDBBbOD5ri+T31x1j/WQcX0vNh1Lw5o64eNGL2JNT6x09mkxMFvTI+L3fPqj9LE5hA/JzauiKlssdlAjELUF+2INUwdO3bMpvrF9ZvisfHa63v//ffTv//972xtU6hOT4tpYFX1d5Arojq6MjlTB2PNT1UE29igInb3qz9yFOumYs1RhMEIEnkWXnjhbHv5yy67LHuNE4v1avH/PGoYevfunf3/rT+iFDWMNWvRH2K9U2O1icA38UYfTVmbMq8doKUTmgBauNieO4JEbOgQB8WxY10cOB9//PHp8ssvz9Y43XTTTdnudRGcYh1UrE05+OCDs1AUF0ON0aXrr78+2+Ahtq2OUYrY/CAO/iNEPfLII9nFS2PntdiSvDqaMSmxCUUceB900EHZaEKMesVz/5wY4RowYEC2PXdMJ4yd2GLDgAhzsZNftDO2ve7evXt2kB4/J7bhjrbGCExsKBCbDkT7q5sPVK9dFaNyTz31VLr99tuz3QVjumNRMUIXNYk1OI2NJtUPjr///e/T3//+92waWqwVi5GxqF99seNcrHNaaKGFss0ifs6f//znrEbbbbddtqtfvJ7YeS5ef2zKEDsbVte3Re1jDVtcIDj+38X/w7gwbgSuqFuIvhKbL8SmFfE80d54zMRT8Sa3NiFCbGOhbmJFXztASyY0AbRwMWoU21m/+eabWTgKMVIToSGunxQHwbHtc0yBuvLKK+sW80c4ioASoWS//fbLprzts88+dTvOxUhArJuKEZ84yI4QFmElduaLTSjyxGhXjIjEGps4eI/wE9uWT46YhnbbbbdlI10xehJtit3xYjpiPE+s1aqKEYoIYxFkIsjF64lRrpiGGKNQIUakImjF1tsRAGN0I4Li5KyxmljULAJqtCnC4KTEtuQR+iKcRT1jc4obb7yxwfS8aqCLKXKTO9ISa76iBhGaIpzEa47txGOdU9Qofl79kan4mbGWKrZij9AcI3/x+mMzhhB9IeoXtY3/xzHNM+q7/vrrF65N/JsYwTrqqKOynQt/TtHXDtCStauUWQELAPys2Ho7QlWMkk1qU462qpZfO9D2uE4TADSxmAYXm13ESGCMtNRSaKjl1w60XabnAUATi6mCsc4rpiDGtLlaUsuvHWi7TM8DAADIYaQJAAAgh9AEAACQQ2gCAADIUXO758UV22MZV1yHAwAAqF3jxo3LrikX1wDMU3OhKQKTvS8AAIDKZOaCmgtN1RGmuKo5AABQu1599dXJepw1TQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAAC0ltB0ySWXpF122aXBbYMHD06/+93v0jLLLJPWXnvtdO211zZb+wAAgNrTYkLTDTfckAYOHNjgthEjRqQ99tgjzTfffOn2229PBx54YDrrrLOyrwEAAH4JHVIz++STT1L//v3Ts88+mxZYYIEG9916662pY8eO6cQTT0wdOnRICy20UHr33XfTpZdemrbeeutmazMAAFA7mn2k6bXXXsuC0T333JOWXnrpBve98MILacUVV8wCU9XKK6+chg0blj7//PNmaC0AAFBrmn2kKdYpxUdjhg8fnhZZZJEGt80xxxzZ548//jjNNttspX5mpVJJo0ePnuT97dq1S7UqalOGmpWjbsWpWTnqVpyalaNuxalZOepWnJo1fvvk1KXZQ1Oe7777LnXq1KnBbdNOO232+fvvvy/9vOPGjcs2mGhMjHr16tWrwehWrRg/fnx6/fXXs/oUoWbFa/Zj3ZZIHTq0T7Vm/PgJ6fXXXyvZ19SsKHUr2deW6JU6tK/B97UJ49Prr03B+5q6Ffp3ala+ry2xRK/UvgbrNmHC+PRayb6mZuMavX/ivNGYFl21zp07p7Fjxza4rRqWunbtWvp5o9P07Nmz0fsiacbB/xlHXZnef2d4qhXzLjhnOmLAnmnhhRcufPaiWrMzT78tvf/+Z6lWzDvv7OmPR25bqmY/1q19OvXCO9N7H9XOdNP5fjVbOubALaegr7VPf7r6zjRseO3UbIE5Z0t/2r1czerX7bhb70hDP6uduvWYfbZ08nZble9r7Tuko+/7S3rni9qp2YLdZkunbbzNlPW19h3Snx+/Nb07snb+Hsw/8+yp/2+2m6K+dsnL16ePv/kk1Yq5pu+e9lv6d1PU1+Lg/943L0pfjP4o1YpuXX+VNl30gNJ9LWr2ryF/Tt+MeTfVium7zJ+W69l/kjUbMmTIZD1Piw5Nc845Z/r0008b3Fb9vnv37qWfNzrNz4WuCExvD34/1ZouXbqU/rcRmN4e8nGqNVNSsxCBaciw2gnoTVG3CExvfaBmRUVgeuMjdSsiAtMbn3pfKyoC01tf1s6BbFPULQLTu19/mGrNlPa1CEyffDss1ZopqVsEpq9Gv5VqTZdJ1Gxypyw2+0YQeXr37p1efPHFNGHChLrbnnnmmdSjR4/UrVu3Zm0bAABQG1p0aIptxb/55pt07LHHZkNnd9xxR7r66qvTfvvt19xNAwAAakSLDk0xmnT55ZenoUOHpi233DJdcMEF6Ygjjsi+BgAA+CW0qDVNAwYM+MltSy21VLrllluapT0AAAAteqQJAACguQlNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABAaw9N48ePT+eee25aa6210rLLLpt23nnn9O9//7u5mwUAANSAVhGaLr744nTbbbelk046Kd11112pR48eae+9906ffvppczcNAABo41pFaPr73/+eNtlkk7T66qun+eefPx111FFp1KhRRpsAAICprlWEpm7duqVHH300ffDBB2nChAnplltuSZ06dUqLLbZYczcNAABo4zqkVuDYY49Nffv2Teuss05q3759mmaaadL555+f5ptvvlLPV6lU0ujRoxu9r127dqlLly6pVo0ZMyarTxFqVrxmQd30taL0tXL0teL0tXL0teL0tXL0taarWdwWtWkToWnIkCFphhlmSBdeeGHq3r17tr7p8MMPT9dff31afPHFCz/fuHHj0uDBgxu9LzpTr169Uq0aOnRo1qmKULPiNQvqpq8Vpa+Vo68Vp6+Vo68Vp6+Vo681bc1iBlurD00ff/xxOuyww9LVV1+dVlhhhey2JZdcMgtSMdp00UUXFX7Ojh07pp49ezZ63+QkzbYsNtkoc+ailpWpWVA3fa0ofa0cfa04fa0cfa04fa0cfa3pahaZYnK0+ND08ssvZyNDEZTqW3rppdPjjz9e6jmj03Tt2rWJWti21PKwbVlqVo66Fadm5ahbcWpWjroVp2blqFvT1Wxyw2SL3whizjnnzD6/+eabDW5/66230gILLNBMrQIAAGpFiw9NSy21VFp++eXTkUcemZ555pk0bNiwNHDgwPT000+nfffdt7mbBwAAtHEtfnpe7JQXF7eNoHT00Uenr776Ki2yyCLZGqeYogcAAFDToSnMNNNMqX///tkHAADAL6nFT88DAABoTkITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAA0NSh6eijj07vv/9+o/e98847af/99y/ztAAAAC1Oh8l94EcffVT39V133ZXWXXfd1L59+5887vHHH09PPfVU07UQAACgNYSmP//5z1kgqjrooIMafVylUkmrrbZa07QOAACgtYSmE088MRtBilB0zDHHpN///vdpvvnma/CYaaaZJs0444xppZVWmhptBQAAaLmhqXv37mnLLbfMvm7Xrl3q06dPmnXWWadm2wAAAFpPaKovwlOMOL3++utp9OjR2dcT6927d1O0DwAAoPWFpldeeSX17ds3DR8+PPu+GppiBCq+js+DBw9u2pYCAAC0ltB02mmnpQ4dOmSf55xzzmwtEwAAQFtUKjS99tpr6eyzz862Hf+lxDbnl156aXZ9qNiAInbv++1vf/uL/XwAAKA2lRoi6tatW6PXaJpa7r777nTsscemnXfeOd13331pk002Sf369UsvvfTSL9YGAACgNpUKTTvttFO65JJLsk0gprZYI3XuueemXXfdNQtNMcoU252vuuqq6bnnnpvqPx8AAKhtpabnvfvuu+ntt9/OLmK78MILp86dOze4PzaCuOaaa5qkgUOHDk0ffvhh2nTTTRvcfsUVVzTJ8wMAAEyV0LTYYovVfT/xluONbUE+JaEpxKjWXnvtlW1zPs8882SjTWuvvXap54z2TWqULAJfly5dUq0aM2ZM4f9/ala8ZkHd9LWi9LVy9LXi9LVy9LXi9LVy9LWmq1l15++pEpquu+669Ev55ptvss9HHnlktvnD4Ycfnh588MF0wAEHpKuuuiqtssoqhZ9z3Lhxk9wSPTpTr169Uq2KkBqdqgg1K16zoG76WlH6Wjn6WnH6Wjn6WnH6Wjn6WtPWrFOnTlMnNP2SOnbsmH2OUaa4qG5YfPHFsxGnsqEpnrNnz56N3jc5SbMt69GjR6kzF7WsTM2CuulrRelr5ehrxelr5ehrxelr5ehrTVezIUOGTNa/LxWaYlrczxX+kUceSU2he/fu2edFFlmkwe0Reh577LFSzxlt79q1a5O0r62p5WHbstSsHHUrTs3KUbfi1KwcdStOzcpRt6ar2eSGyVKhacUVV/zJD/j222/Tq6++mr7//vu02267paayxBJLpOmmmy69/PLLaYUVVqi7/a233sp20gMAAJiaSoWmAQMGTHKtUKw1KjM3dVJiZ7699947XXjhhdmo01JLLZVdq+nJJ59MV199dZP9HAAAgKm+pinWCsX1lI4++uh06KGHNtnzRhCLIbVzzjknffLJJ2mhhRZK559/flpppZWa7GcAAAD8IhtBfPXVV9lUvaa2xx57ZB8AAAAtPjTdddddP7ltwoQJafjw4en6669vsPYIAACg5kLTUUcdNcn7ll122XT88cdPSZsAAABad2hqbDvx2E1v+umnTzPOOGNTtAsAAKD1hqa555677uu33347jRo1Ks0yyywCEwAA0OaU3gjir3/9azr99NPT559/XnfbbLPNlg477LC0xRZbNFX7AAAAWl9oGjRoUPrjH/+YVl555dSvX78sLH366afpnnvuybYbn3nmmdOaa67Z9K0FAABoDaHp4osvThtuuGF23aT6tt566/SHP/whXXLJJUITAADQJkxT5h+99dZbacstt2z0vrj9jTfemNJ2AQAAtN7QFJs+xEVsGzNy5MjUqVOnKW0XAABA6w1Nq6yySrrggguyi9nW9/HHH6cLL7wwrbbaak3VPgAAgNa3pik2f4j1S+uvv352MdvYCCJ20XvppZfSTDPNlO2gBwAAULMjTbPPPnu688470y677JLGjBmT/vOf/2Sf4/u4vf51nAAAAGryOk3dunXLth0HAABoywqNNI0fPz5df/316eGHH25w+4QJE7Jd866++ur0ww8/NHUbAQAAWn5oGjduXDrggAPSKaeckq1dqu/LL7/MwtKAAQPSQQcdlIUoAACAmgpNt9xyS3rmmWfSWWedlY444oifrHG6++67s9D0+OOPp9tvv31qtBUAAKDlhqY77rgj7b777mnjjTee5GO22GKLtO2226bbbrutqdoHAADQOkLTu+++m1ZeeeWffVyfPn3SsGHDprRdAAAArSs0dejQIVvXNDmPa9eu3ZS2CwAAoHWFpoUXXjg9++yzP/u45557Ls0zzzxT2i4AAIDWFZo233zzdNNNN6VXXnllko957bXX0g033JB++9vfNlX7AAAAWsfFbbfZZpv017/+Ne2yyy7Z12uuuWY2ohRbjX/44YfZrnm33nprWnTRRbPHAAAA1FRoinVKl1xySTr11FOz7cdvvPHGuvsqlUq2lil2zuvXr1/q3Lnz1GovAABAywxNIcLQiSeemA499ND09NNPp+HDh6f27dunueeeO9tZb4YZZph6LQUAAGjpoalq1llnzb1eEwAAQM2FprvuuqvQE8eFbgEAAGomNB111FENvq9eiynWM018WxCaAACAmgpNjzzySN3XgwcPTn/84x/TAQcckG0vPsccc6QRI0akQYMGpfPPPz+ddtppU6u9AAAALTM0xWYPVQcffHAWmPbZZ5+627p375523HHHNHbs2HTmmWemPn36NH1rAQAAWurFbet7++23U69evRq9b8EFF0wffPDBlLYLAACg9YamBRZYIN17772N3hfXcFpkkUWmtF0AAACtd8vxAw88MPXt2zcNGzYsrbXWWmmWWWZJn3/+eXrooYfSkCFD0mWXXdb0LQUAAGgtoWn99ddPF154YbrooovSwIEDsx30pplmmrTsssumq6++Oq2wwgpN31IAAIDWEpqefvrptOqqq6a11147ff/99+mrr75KM888c+rUqVPTtxAAAKC1rWmK3fNiKl6Ydtppsy3HBSYAAKAtKhWaZpxxxtS5c+embw0AAEBbmJ633377pZNPPjkNHTo0LbbYYqlr164/eUzv3r2bon0AAACtLzT1798/+3zOOedkn9u1a1d3X2wKEd8PHjy4qdoIAADQukLTtdde2/QtAQAAaCuhacUVV2z6lgAAALSV0BRiPdN5552XnnvuufT1119nF7iN6zPFhW8XWmihpm0lAABAawpNQ4YMSTvssENq3759dq2m2WabLX322Wfp0UcfTY899li67bbbBCcAAKB2Q9NZZ52V5plnnnTdddelGWaYoe72UaNGpd122y3bIOKCCy5oynYCAAC0nus0Pf/882n//fdvEJhCfL/vvvtm9wMAANRsaOrQoUOadtppG72vU6dOaezYsVPaLgAAgNYbmpZccsl04403Ztdkqi++v+GGG9Kvf/3rpmofAABAsyq1pqlv375pxx13TJtttlnacMMN0+yzz55tBPHAAw9ku+pdddVVTd9SAACA1hKaYqTp8ssvT//zP/+TbfgQI0zt2rXLRpguu+yy1Lt376ZvKQAAQGu6TtPKK6+cbr755mz9UlynacYZZ0zjx4//yeYQAAAANbemady4cal///5pu+22S126dEndu3dPL730UlpllVXS6aefnn744YembykAAEBrCU3nn39+uueee9LGG29cd1uvXr3S4Ycfnm699dZs6h4AAEDNTs+7995705FHHpl22GGHuttmnnnmtPvuu2fbkV977bXZ9ZoAAABqcqRpxIgRad555230vgUXXDANHz58StsFAADQekNTBKMHH3yw0fsGDRqU5p9//iltFwAAQOudnrfrrrumo446Ko0cOTKtu+66qVu3bunLL79Mjz76aLr//vvTaaed1vQtBQAAaC2haYsttkjffvttuuiii9JDDz1Ud/sss8ySjj/++Ox+AACAmr5O084775x22mmnNHTo0GzEKa7TFNP2ppmm1Iw/AACAFmmKEk5c1DZC05tvvplmnXXWNGzYsFSpVJqudQAAAK11pOniiy9Ol1xySfruu+9Su3bt0lJLLZUGDhyY7ax35ZVXZiNPAAAANTnSdP3112cXuN1jjz2yi9lWR5d+97vfpffffz+de+65Td1OAACA1hOarrvuuuzitX379k1LLLFE3e19+vRJhx56aLbtOAAAQM2Gpo8++iituOKKjd4Xm0F8/vnnU9ouAACA1hua5pprrvTSSy81et9//vOf7H4AAICa3Qhim222ydY0de7cOa255prZbaNHj04PPvhgtjlErHUCAACo2dC0zz77pA8++CCdddZZ2UfYddddsw0hNttss7Tffvs1dTsBAABaT2iKLcZPPPHEtOeee6Znnnkmu7jtDDPMkHr37p169uyZbrrppuzitwAAADUVmh5//PF05513ZqFp8803z3bLW2CBBeruf+GFF9JWW22VXexWaAIAAGoqNN1zzz3piCOOSB07dkydOnVK999/fzrvvPPSeuutl400nXzyyem+++5L7du3t6YJAACovdB0zTXXpKWXXjpdccUVWWg6+uij04UXXpgWXnjhLCR9/PHHaY011kjHHHNM6tGjx9RtNQAAQEsLTcOGDUsnnXRSmn766bPvDzrooLTRRhulAw44II0dOzade+65aYMNNpiabQUAAGi5oSm2FK9//aW555472y2vQ4cO2dS9bt26Ta02AgAAtPyL20ZAivVKVdWv//CHPwhMAABAmzXZoWlS5phjjqZpCQAAQFsMTbH9OAAAQFtV6DpNf/rTn+o2gojpeuH4449P00033U+CVOy2BwAAUDOhqXfv3g3C0qRua+x7AACANh+arrvuutTchg4dmrbaaqtsdCs+AwAAtPg1Tb+UcePGpcMPPzzb+hwAAOCX0mpC0/nnn1+3ngoAAOCX0ipC0/PPP59uueWWNGDAgOZuCgAAUGMK7Z7XHL7++ut0xBFHpOOOOy7NNddcTfKcsVHFpKb5xc5/Xbp0SbVqzJgxhTfyULPiNQvqpq8Vpa+Vo68Vp6+Vo68Vp6+Vo681Xc3itsm5hFKLD02xzfmyyy6bNt100yZdHzV48OBG74vO1KtXr1SrYrON6FRFqFnxmgV109eK0tfK0deK09fK0deK09fK0deatmadOnVq3aHprrvuSi+88EK69957m/R5O3bsmHr27NnofbV+sd4ePXqUOnNRy8rULKibvlaUvlaOvlacvlaOvlacvlaOvtZ0NRsyZMhk/fsWHZpuv/329MUXX6Q111yzwe39+/dPf/vb39Lll19e6nmj03Tt2rWJWtm21PKwbVlqVo66Fadm5ahbcWpWjroVp2blqFvT1Wxyw2SLDk1nnXVW+u677xrctv7666dDDjkkbbbZZs3WLgAAoHa06NDUvXv3Rm/v1q3bJO8DAACouS3HAQAAmkuLHmlqzJtvvtncTQAAAGqIkSYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAANDaQ9PIkSPTCSeckH7zm9+k5ZZbLu24447phRdeaO5mAQAANaBVhKZ+/fqll156KZ199tnp9ttvT4svvnjaa6+90jvvvNPcTQMAANq4Fh+a3n333fTkk0+mP/3pT2mFFVZIPXr0SMcff3yaY4450r333tvczQMAANq4Fh+aZplllnTppZemJZdcsu62du3aZR9ff/11s7YNAABo+zqkFm7GGWdMffr0aXDbgw8+mI1AHXPMMaWes1KppNGjRzd6X4SxLl26pFo1ZsyYrD5FqFnxmgV109eK0tfK0deK09fK0deK09fK0dearmZxW9Sm1Yemif3rX/9KRx99dFp//fXTmmuuWeo5xo0blwYPHtzofdGZevXqlWrV0KFDs05VhJoVr1lQN32tKH2tHH2tOH2tHH2tOH2tHH2taWvWqVOnthWa/v73v6fDDz8820HvrLPOKv08HTt2TD179mz0vslJmm1ZrBkrc+ailpWpWVA3fa0ofa0cfa04fa0cfa04fa0cfa3pajZkyJDJ+vetJjRdf/316ZRTTkkbbrhhOv300ycrEeZ1mq5duzZp+9qKWh62LUvNylG34tSsHHUrTs3KUbfi1KwcdStuUjWb3DDZ4jeCCDfeeGM66aST0s4775xtOz4lgQkAAKCIDq1h/uGpp56a1ltvvbTffvulzz//vO6+zp07pxlmmKFZ2wcAALRtLT40xU55sXHDww8/nH3Ut+WWW6YBAwY0W9sAAIC2r8WHpv333z/7AAAAaA6tYk0TAABAcxGaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACA1h6afvjhh3TeeeelNdZYIy2zzDJpn332Se+//35zNwsAAKgBrSI0XXTRRenGG29MJ510Urr55puzELX33nunsWPHNnfTAACANq7Fh6YIRldeeWU65JBD0pprrpkWW2yxdM4556Thw4enhx56qLmbBwAAtHEtPjS98cYb6dtvv02rrLJK3W0zzjhj6tWrV3r++eebtW0AAEDb165SqVRSCxajSQcffHB6+eWXU+fOnetu79u3b/ruu+/SJZdcUuj5/vWvf6V4yR07dpzkY9q1a5e++nJUGj9+QqoVHTq0TzPNOkNWmzKymo38No2fUEM1a98+zTTzdKVrVq3byK9H11zdZp6x6xT1tRHfjK65389Zpi9fs7q6ffttGjfhh1QrOrafJs0yXfnf0ajZl6O/TeN/qKG+Nk37NGvXKX9fG/Fd7dVtls5T1tdGjf2m5mo2Q6fpp7ivjR73dfqhUjt1m6Zd+9S144xT1NfGjhuRfqiMT7VimnYdUqeOs0yyZuPGjcvqstxyy+U+T4fUwo0ZMyb73KlTpwa3TzvttOmrr74q/HxRlPqfJyUCRC36ubrkiQBRi6akZiECRC2akrpFgKhFU9rXIkDUoimpWwSIWjTFfa2zuhUVAaIWTWlfiwBRi6akbhEgalG7SdQsbp+cerb40FQdXYq1TfVHmr7//vvUpUuXws+37LLLNmn7AACAtq3Fr2maa665ss+ffvppg9vj++7duzdTqwAAgFrR4kNT7JY3/fTTp2effbbutq+//jq9/vrrqXfv3s3aNgAAoO1r8dPzYi3T7373u3TWWWelWWedNc0999zpzDPPTHPOOWdaf/31m7t5AABAG9fiQ1OIazSNHz8+HXfccdmOeTHCdMUVV+TugAcAAFATW44DAAA0pxa/pgkAAKA5CU0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAmIgrsgD1CU0AtAoOYvmlfPnll6ldu3bN3YxWye8pbZXQBPAL++6775q7Ca3OySefnIYOHdrczWh1fvjhh+ZuQqvzhz/8IZ1zzjnZ1wJAccImbVWH5m4ATevaa69NK664YlpsscWauymtyoUXXpg+/fTT9PXXX6cddtghLbnkkqlr167N3awW39e++uqr9P3332cHGe3bt88OMPzBzNe3b98022yzpYMPPjjNPPPMzd2cVmHHHXdMX3zxRTriiCOauymtyqmnnpo++eST9M0336Rdd901rbrqqqljx47N3awWbZdddknPP/986t27d/a997PJc/HFF2e/o3FC6KCDDkpzzjmnvwc/46677kqjRo3Kfj+32GKL1K1bt9SpU6fmblaLd8MNN2R1mzBhQtp2223TLLPM8ou9rwlNbcjbb7+drr766vSf//wn7b///mnBBRds7ia1CnvuuWc2FaNXr17ZmezDDjssHX300WnjjTdu7qa16AOLCEzxh/Hll19O77zzTrrooov8gZwMo0ePzt70Z5111qyOM844Y3M3qcUHprFjx2YHGHFA4UBs8uyxxx7ZQezaa6+dnnvuueykkMCUL06YjR8/Pv35z39Of/nLX7ITQlGzaaYxKSfP7rvvnkaOHJkWXnjh9OKLL6bBgwen22+/Pfs99fs66d/POPCfaaaZ0nvvvZduu+229Lvf/S5ts802/ibkiL+Z8b4Wfz8//PDDdOedd2YnhDbddNMsPE1tQlMbMsccc2TJ+4UXXkj/+7//m/bbb7+00EILNXezWrRLLrkke+O66aabUpcuXbLbdtppp3TNNdcITZNw7rnnpm+//TY7qIg/iPHHMT7iDGPnzp2bu3ktXowCR9C84IILspHNGHGafvrpm7tZLfbAIk5oPPjgg3W3jRs3Lnuf69ChQza6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+NlSSy3V4DniukJnnnlmdsGvDTbYINUCdStOzcpRt+LUrBx1K07NylG34tSsRuv2i0Y0Jsuzzz5b2XLLLStDhgypS9ZPPPFE5eyzz64sueSS2XzPpZdeOkvhY8eOrUvtjz76aHZ7LZ2xqE/dilOzctStODUrR92KU7Ny1K04NautuglNzWz48OENhioffvjhyoorrljZfffdK99++21l3LhxDR7/4YcfVs4999zK9ttvX1l55ZUrX3/9dd19I0eOrLzwwguVWqBuxalZOepWnJqVo27FqVk56lacmpXTlupmy/FmNHbs2DRw4MB04403Zt+/8cYb2dDlwgsvnF555ZX0+eefpw4dOmSPq269+Ktf/SodeOCB6dprr03zzjtvuuiii7L7xo8fn2aaaaa0/PLLp7ZO3YpTs3LUrTg1K0fdilOzctStODUrp63VTWhqRp06dco6x8knn5zN4Yw5miuuuGLq27dvmnvuudNee+2Vvvnmm+xx0VmqF+2Kz3Fbr1690siRI7PbotPVCnUrTs3KUbfi1KwcdStOzcpRt+LUrJy2VjehqZkdfPDBafHFF0///ve/0yKLLJJmnnnmtNxyy6XjjjsuTTvttGnrrbfOOlR0luhQVSNGjMgW0H3wwQdZQq+1/TzUrTg1K0fdilOzctStODUrR92KU7Ny2lLdhKZmUP9//JdffpkNP26zzTbpqquuSldffXWWsGP48YQTTsiS9rbbbptGjRrVIGVX/92xxx6bPaZ6ZeS2TN2KU7Ny1K04NStH3YpTs3LUrTg1K6fSVuvWbKupalT9xXDVxW2xM0g455xzKosuumjlqquuqnvM888/X1ljjTUqhxxyyE+e6/vvv6/UCnUrTs3KUbfi1KwcdStOzcpRt+LUrJwJbbhuzT9BsIZE8q7uT3/FFVdke9J37do1m+e5yiqrZHM8I0mffvrp2eeddtopffvtt9kiuqWXXvonzxfJuxaoW3FqVo66Fadm5ahbcWpWjroVp2blVNp63Zo7tdWK+lcyHjhwYHYl45NOOqmy/vrrV9Zee+3KrbfemiXqeFxstRhJvHfv3pUDDzyw7t+OHz++UmvUrTg1K0fdilOzctStODUrR92KU7NyfqiBuglNv7DYf36fffapvPTSS3W37bfffpUNNtigrkNV97E/77zz6vavr98Za5G6Fadm5ahbcWpWjroVp2blqFtxalbOh224bkLTL+iaa66prLnmmpXNNtus8t5779XdHnM963eouNhXfRNf+KvWqFtxalaOuhWnZuWoW3FqVo66Fadm5VzTxutm97ypaOLtETfddNPUuXPn9Oabb9Zd4Ct07NgxXXDBBWnBBRdMZ5xxRnrqqaca/LuWsDf9L0ndilOzctStODUrR92KU7Ny1K04NSunUmt1a+7UVguuu+66yv333599/dVXX1XWXXfdykYbbZTtGFJ/ODKS9hlnnNHi53T+UtStODUrR92KU7Ny1K04NStH3YpTs3Kuq5G6CU1T2fDhwyt77LFHZbXVVqsMGjQou23EiBGVtdZaq7Lpppv+pENVtdYO1VTUrTg1K0fdilOzctStODUrR92KU7NyhtdQ3YSmqbQ/ff0OEovh+vbtW+nTp0/lkUceqetQsZvIFltsUXnqqadaxQK4qUndilOzctStODUrR92KU7Ny1K04NStnQg3XTWiaism7vn//+9+Vgw8+OOtQ1SQ+cuTIylJLLVU54ogjmqmVLY+6Fadm5ahbcWpWjroVp2blqFtxalbO8Bqsm9A0Fdx3332V5ZdfPhuSrC+S+N577135zW9+U3nyySez20aNGtUqhyinBnUrTs3KUbfi1KwcdStOzcpRt+LUrJz7arRuds+bCmaeeebsysbHHntseuGFF+puX2aZZdJGG22UPvnkk7TvvvumZ599Nk0//fSpffv2acKECanWqVtxalaOuhWnZuWoW3FqVo66Fadm5cxcq3Vr7tTWVuZ2TuyVV16p7Lnnntl8zvpJ/MUXX6z069evctttt7WZ5F2GuhWnZuWoW3FqVo66Fadm5ahbcWpWjrr9qF38p7mDW2sV+89PM83/Ddbde++9adiwYemjjz5Kq622WlpvvfXS+++/nwYMGJDeeeed1L9//9SzZ890yimnpNlmmy2deOKJ2b+L5B0JvJaoW3FqVo66Fadm5ahbcWpWjroVp2blqFtDQlMTiAt13XHHHWmFFVZIb7/9dhozZkxaeOGF08CBA9MHH3yQzj777PSPf/wj/epXv0rTTTdd9ti40FeUvl27dqlWqVtxalaOuhWnZuWoW3FqVo66Fadm5ajb/1dv1IkSHn744ewiXq+99lrdbTfddFNls802y4Yn40JeX375ZeWJJ57ILvxVHaqM22uZuhWnZuWoW3FqVo66Fadm5ahbcWpWjrr9qEM1PPHzvvjii/TZZ59lw4yRsMN7772X5pprrrTQQgulcePGZcl6q622Sl999VW6+eabs6HMGK5cffXV654nhio7dKid0qtbcWpWjroVp2blqFtxalaOuhWnZuWoWz67502mY445JvXt2zdtscUW2cdVV12V3R6dJTrOtNNOm3WksWPHpk6dOqWtt946DR8+PBvGnFhbmds5OdStODUrR92KU7Ny1K04NStH3YpTs3LU7ecJTZNhjz32yBa57bnnnunaa6/N5nDGlophk002yTrMJZdckn0fHSlEB1tsscWy+Z21St2KU7Ny1K04NStH3YpTs3LUrTg1K0fdJk/bGztrYueff3624O2iiy5Ks846a4PdRGKB21JLLZV22WWXdOWVV6Zvv/02bb/99umbb75J55xzTurcuXNaYoklUi1St+LUrBx1K07NylG34tSsHHUrTs3KUbfJJzT9jNdffz2tu+66dR0pVLdfjB1BosN06dIl7bPPPumKK67IEnr37t1Tt27dsq/jsfW3bKwV6lacmpWjbsWpWTnqVpyalaNuxalZOeo2+YSmSYgOEIvh/vWvf6Xddtut7raJO8WIESPSfffdl/r06ZPtYf/aa69lHalXr17ZY8ePH98mF8NNiroVp2blqFtxalaOuhWnZuWoW3FqVo66Fdf2Y2FJ0RHi4lzTTz99evrpp+tuqy+GLWeZZZbUo0eP9Mknn2SPj07161//ui5510pHqlK34tSsHHUrTs3KUbfi1KwcdStOzcpRt+KEpkmIjhJbKy644ILpmWeeSUOHDv3JY6oX7IqtFeNiXhOrhaHKialbcWpWjroVp2blqFtxalaOuhWnZuWoW3G19WoLqM7jPOigg7L5npdffnk2jDmxL7/8Mo0aNSobpkTdylCzctStODUrR92KU7Ny1K04NStH3Uqod6FbJuHWW2+tLLHEEpWDDz648tRTT9Vd6fjDDz+s7LfffpXtttuu7grI/EjdilOzctStODUrR92KU7Ny1K04NStH3SZPu/hPmbBVS6JEDz/8cOrfv392wa7YNSQSeszjjKHJa665JrvgVwxfttULepWhbsWpWTnqVpyalaNuxalZOepWnJqVo26TR2gq4KOPPkpPPfVUeuWVV7KFcYsuumjaYIMNsg5US7uHFKVuxalZOepWnJqVo27FqVk56lacmpWjbvmEpiZQ68m7LHUrTs3KUbfi1KwcdStOzcpRt+LUrBx1+z9CU0FRrupuIvW/Jp+6Fadm5ahbcWpWjroVp2blqFtxalaOuk2a0AQAAJDDluMAAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAHAZHKVDoDa1KG5GwAAE3v11VfTtddem55//vn05ZdfpjnmmCOtssoqad99903zzjtvs7TpoosuSp06dUp77713s/x8AJqPkSYAWpQbbrgh7bDDDumLL75Ihx12WLrsssuysPTcc8+lbbbZJr3xxhvN0q5zzz03jRkzpll+NgDNy0gTAC3Giy++mE455ZS08847p2OPPbbu9pVWWimtu+66aYsttkjHHHNMuuOOO5q1nQDUFiNNALQYV1xxRZphhhlSv379fnLfrLPOmo466qi0zjrrpNGjR6cJEyZko1KbbrppWmqppdKaa66ZzjrrrPT999/X/Ztddtkl+6jv2WefTYsuumj2OUQA69WrV3r55ZfT9ttvn5Zccsm01lprZW2piseHCy64oO7r888/P6233nrZbSuuuGJaffXV03HHHZe1ZdSoUT+Z2rf88ssbqQJopYQmAFrMJgv//Oc/s7VLXbp0afQxG220UTrwwANT165d0wknnJBOO+20bATq4osvzkanrr/++nTAAQcU3rDhhx9+SIceemj2/Jdeemlabrnl0hlnnJGeeOKJ7P5bbrkl+xzTA6tfh48++ij94x//SOecc046+uij0x577JGFtgceeKDB8999993Zc0/qdQHQspmeB0CLMGLEiCxwzDPPPD/72CFDhqS//OUv2ZqnWO8UVltttWzDiCOOOCI9/vjjqU+fPpP9syNkRdjadttts+9jVOjhhx9Ojz32WFpjjTXSMsssk90+55xz1n0dxo8fn4488si0wgor1N227LLLZiGp+lz/+te/0rBhw9KAAQMKVAOAlsRIEwAtQvv27bPPMe3u58SmEGHjjTducHt8H89TnXpXRISdqtglL6YDxjTAn7P44os3+H7rrbdOL7zwQvrwww+z7++8887Uo0ePBs8PQOsiNAHQIsw000xpuummy6a8TUqEmK+++ir7CLPPPnuD+zt06JBmmWWWn6wpmhydO3du8P0000wzWdP8os31VafhxWhTjJzdf//9aauttircHgBaDqEJgBYjNlOIUaL6mznUd+utt6aVV1657vvPPvuswf3jxo3LpvlFcKqaeORqckaPpkSEqA033DALS7EmKn7e5ptvPlV/JgBTl9AEQIux5557ppEjR6aBAwf+5L4ISFdeeWXq2bNntmtduO+++xo8Jr6PkBRrksL000+fhg8f/pNtzcuIkafJFRtGvPXWW+maa65Jq666aurevXupnwlAy2AjCABajNhkoW/fvlloevvtt7PrMsWo0X//+99sC/AYgYr7FlpoobTlllum8847L9vGu3fv3mnw4MHZ9t9xTafYvCHE1uGDBg3Kdtlbe+21s7VGd911V6m2zTjjjNmmDs8//3yDjR8aE6Et1jHF2qvYWQ+A1k1oAqBF+f3vf59dNymuwXTqqadm65fmmmuu7DpM+++/f/Z1iIvgzj///On2229Pl112WbZz3q677prtglcdFYpNGd57771sM4abb745C1cRtHbcccfC7YqfHddb2meffdLf/va3n318tPfLL7/MtkQHoHVrVyl6MQsAIFf8aY2d/GKN1jHHHNPczQFgChlpAoAm8s0336Srr746vfrqq+n9999Pu+yyS3M3CYAmIDQBQBOJbctjGuAPP/yQTS2cd955m7tJADQB0/MAAABy2HIcAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACBN2v8DXW28pR6Q0MgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot bar chart\n", + "plt.figure(figsize=(10, 6))\n", + "sns.barplot(data=country_counts, x=\"Country\", y=\"RecordCount\", palette=\"viridis\")\n", + "plt.title(\"Record Count by Country\")\n", + "plt.xticks(rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "887b2525", + "metadata": {}, + "source": [ + "## πŸ“ˆ Pivot Table Example" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "1e26e06b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Age
Country
Country_043.7
Country_143.8
Country_251.0
Country_338.2
Country_444.3
Country_548.3
Country_647.3
Country_741.1
Country_840.5
Country_947.1
\n", + "
" + ], + "text/plain": [ + " Age\n", + "Country \n", + "Country_0 43.7\n", + "Country_1 43.8\n", + "Country_2 51.0\n", + "Country_3 38.2\n", + "Country_4 44.3\n", + "Country_5 48.3\n", + "Country_6 47.3\n", + "Country_7 41.1\n", + "Country_8 40.5\n", + "Country_9 47.1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# If 'Age' exists, average age by country (example only if dataset has relevant column)\n", + "if \"Age\" in df.columns:\n", + " age_pivot = df.pivot_table(index=\"Country\", values=\"Age\", aggfunc=\"mean\")\n", + " display(age_pivot)\n", + "else:\n", + " print(\"No 'Age' column in dataset to pivot on.\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9793c4e", + "metadata": {}, + "source": [ + "\n", + "## πŸŽ‰ Summary\n", + "\n", + "In this notebook, we:\n", + "- Loaded a sample CSV dataset\n", + "- Explored its structure\n", + "- Grouped and counted records by country\n", + "- Visualized results with a bar chart\n", + "- Created a pivot table (if applicable)\n", + "\n", + "πŸ–₯️ Try modifying this notebook for your own datasets or audit use cases!\n", + " " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/racf_access_analysis.ipynb b/notebooks/racf_access_analysis.ipynb new file mode 100644 index 0000000..48bea8d --- /dev/null +++ b/notebooks/racf_access_analysis.ipynb @@ -0,0 +1,526 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4c3a8609", + "metadata": {}, + "source": [ + "\n", + "# πŸ›‘οΈ RACF Access Report Analysis\n", + "\n", + "This notebook demonstrates how to parse a RACF-like mainframe access report stored in a fixed-width text format, extract user access details, identify unusual access configurations, and summarize the results for follow-up.\n", + "\n", + "We'll be working with the file `sample_racf_data.txt`.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "a0b3e2b2", + "metadata": {}, + "source": [ + "\n", + "## πŸ“¦ Install Dependencies\n", + "\n", + "If you haven't already installed `pandas`, run:\n", + "\n", + "```bash\n", + "pip install pandas\n", + "```\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ca527b49", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import re" + ] + }, + { + "cell_type": "markdown", + "id": "d1694149", + "metadata": {}, + "source": [ + "## πŸ“‚ Load RACF Report" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4891d98b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LISTGRP *\n", + "INFORMATION FOR GROUP PAYROLLB\n", + "SUPERIOR GROUP=RESEARCH OWNER=IBMUSER CREATED=06.123\n", + "NO INSTALLATION DATA\n", + "NO MODEL DATA SET\n", + "TERMUACC\n", + "NO SUBGROUPS\n", + "USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS=\n", + "IBMUSER JOIN 000000 ALTER\n", + "CONNECT ATTRIBUTES=NONE\n", + "REVOKE DATE=NONE RESUME DATE=NONE\n", + "DAF0 CREATE 000000 READ\n", + "CONNECT ATTRIBUTES=NONE\n", + "REVOKE DATE=NONE RESUME DATE=NONE\n", + "IA0 CREATE 000000 READ\n", + "CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS\n", + "REVOKE DATE=NONE RESUME DATE=NONE\n", + "AEH0 CREATE 000000 READ\n", + "CONNECT ATTRIBUTES=NONE\n", + "REVOKE DATE=NONE RESUME DATE=NONE\n" + ] + } + ], + "source": [ + "with open(\"sample_racf_data.txt\", \"r\") as file:\n", + " lines = file.readlines()\n", + "\n", + "# Preview first 20 lines\n", + "for line in lines[:20]:\n", + " print(line.strip())" + ] + }, + { + "cell_type": "markdown", + "id": "9d5ee64a", + "metadata": {}, + "source": [ + "## πŸ“ Parse User Access Records" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "18f06bc9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GroupUserAccessAccess CountUniversal AccessAttributes
0PAYROLLBIBMUSERJOIN0ALTERNONE
1PAYROLLBDAF0CREATE0READNONE
2PAYROLLBIA0CREATE0READADSP SPECIAL OPERATIONS
3PAYROLLBAEH0CREATE0READNONE
4RESEARCHIBMUSERJOIN0ALTERNONE
\n", + "
" + ], + "text/plain": [ + " Group User Access Access Count Universal Access \\\n", + "0 PAYROLLB IBMUSER JOIN 0 ALTER \n", + "1 PAYROLLB DAF0 CREATE 0 READ \n", + "2 PAYROLLB IA0 CREATE 0 READ \n", + "3 PAYROLLB AEH0 CREATE 0 READ \n", + "4 RESEARCH IBMUSER JOIN 0 ALTER \n", + "\n", + " Attributes \n", + "0 NONE \n", + "1 NONE \n", + "2 ADSP SPECIAL OPERATIONS \n", + "3 NONE \n", + "4 NONE " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Initialize lists to hold parsed records\n", + "records = []\n", + "current_group = \"\"\n", + "\n", + "for i, line in enumerate(lines):\n", + " if \"INFORMATION FOR GROUP\" in line:\n", + " current_group = line.strip().split()[-1]\n", + "\n", + " # Identify user lines: starts with a non-empty, non-space string followed by access keywords\n", + " match = re.match(r\"^\\s*(\\S+)\\s+(JOIN|CREATE|CONNECT|USE)\\s+(\\d{6})\\s+(\\S+)\", line)\n", + " if match:\n", + " user, access, access_count, universal_access = match.groups()\n", + "\n", + " # Look ahead for CONNECT ATTRIBUTES line\n", + " attr_line = lines[i + 1].strip() if (i + 1) < len(lines) else \"\"\n", + " attr_match = re.search(r\"CONNECT ATTRIBUTES=(.*)\", attr_line)\n", + " attributes = attr_match.group(1) if attr_match else \"NONE\"\n", + "\n", + " records.append(\n", + " {\n", + " \"Group\": current_group,\n", + " \"User\": user,\n", + " \"Access\": access,\n", + " \"Access Count\": int(access_count),\n", + " \"Universal Access\": universal_access,\n", + " \"Attributes\": attributes,\n", + " }\n", + " )\n", + "\n", + "# Convert to DataFrame\n", + "df = pd.DataFrame(records)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "ab5546a6", + "metadata": {}, + "source": [ + "## πŸ“Š Analyze Access Data" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d1b8269a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Access\n", + "CREATE 5\n", + "JOIN 4\n", + "USE 3\n", + "CONNECT 1\n", + "Name: count, dtype: int64\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GroupUserAccessAccess CountUniversal AccessAttributes
0PAYROLLBIBMUSERJOIN0ALTERNONE
2PAYROLLBIA0CREATE0READADSP SPECIAL OPERATIONS
4RESEARCHIBMUSERJOIN0ALTERNONE
6RESEARCHIA0CONNECT4READADSP SPECIAL OPERATIONS
\n", + "
" + ], + "text/plain": [ + " Group User Access Access Count Universal Access \\\n", + "0 PAYROLLB IBMUSER JOIN 0 ALTER \n", + "2 PAYROLLB IA0 CREATE 0 READ \n", + "4 RESEARCH IBMUSER JOIN 0 ALTER \n", + "6 RESEARCH IA0 CONNECT 4 READ \n", + "\n", + " Attributes \n", + "0 NONE \n", + "2 ADSP SPECIAL OPERATIONS \n", + "4 NONE \n", + "6 ADSP SPECIAL OPERATIONS " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Count users by Access type\n", + "access_summary = df[\"Access\"].value_counts()\n", + "print(access_summary)\n", + "\n", + "# Identify users with ALTER access or SPECIAL OPERATIONS attribute\n", + "anomalies = df[\n", + " (df[\"Universal Access\"] == \"ALTER\")\n", + " | (df[\"Attributes\"].str.contains(\"SPECIAL OPERATIONS\"))\n", + "]\n", + "\n", + "anomalies" + ] + }, + { + "cell_type": "markdown", + "id": "38201382", + "metadata": {}, + "source": [ + "## πŸ“‘ Prepare Follow-Up Report" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a885710c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GroupUserAccessUniversal AccessAttributesNotes
0PAYROLLBIBMUSERJOINALTERNONEReview access appropriateness with system owner
1PAYROLLBIA0CREATEREADADSP SPECIAL OPERATIONSReview access appropriateness with system owner
2RESEARCHIBMUSERJOINALTERNONEReview access appropriateness with system owner
3RESEARCHIA0CONNECTREADADSP SPECIAL OPERATIONSReview access appropriateness with system owner
\n", + "
" + ], + "text/plain": [ + " Group User Access Universal Access Attributes \\\n", + "0 PAYROLLB IBMUSER JOIN ALTER NONE \n", + "1 PAYROLLB IA0 CREATE READ ADSP SPECIAL OPERATIONS \n", + "2 RESEARCH IBMUSER JOIN ALTER NONE \n", + "3 RESEARCH IA0 CONNECT READ ADSP SPECIAL OPERATIONS \n", + "\n", + " Notes \n", + "0 Review access appropriateness with system owner \n", + "1 Review access appropriateness with system owner \n", + "2 Review access appropriateness with system owner \n", + "3 Review access appropriateness with system owner " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create a concise follow-up report\n", + "follow_up = anomalies[\n", + " [\"Group\", \"User\", \"Access\", \"Universal Access\", \"Attributes\"]\n", + "].copy()\n", + "follow_up[\"Notes\"] = \"Review access appropriateness with system owner\"\n", + "\n", + "follow_up.reset_index(drop=True, inplace=True)\n", + "follow_up" + ] + }, + { + "cell_type": "markdown", + "id": "0158f787", + "metadata": {}, + "source": [ + "\n", + "## πŸŽ‰ Summary\n", + "\n", + "In this notebook, we:\n", + "- Parsed a RACF-like access report from a fixed-width text file\n", + "- Extracted key fields into a structured DataFrame\n", + "- Analyzed access configurations for high-risk permissions\n", + "- Summarized anomalies requiring follow-up with system owners\n", + "\n", + "πŸ–₯️ Use this as a starting point for mainframe audit automation projects!\n", + " " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/sample_racf_data.txt b/notebooks/sample_racf_data.txt new file mode 100644 index 0000000..a3caa69 --- /dev/null +++ b/notebooks/sample_racf_data.txt @@ -0,0 +1,54 @@ +LISTGRP * +INFORMATION FOR GROUP PAYROLLB + SUPERIOR GROUP=RESEARCH OWNER=IBMUSER CREATED=06.123 + NO INSTALLATION DATA + NO MODEL DATA SET + TERMUACC + NO SUBGROUPS + USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS= + IBMUSER JOIN 000000 ALTER + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + DAF0 CREATE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + IA0 CREATE 000000 READ + CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS + REVOKE DATE=NONE RESUME DATE=NONE + AEH0 CREATE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE +INFORMATION FOR GROUP RESEARCH + SUPERIOR GROUP=SYS1 OWNER=IBMUSER CREATED=06.123 + NO INSTALLATION DATA + NO MODEL DATA SET + TERMUACC + SUBGROUP(S)= PAYROLLB + USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS= + IBMUSER JOIN 000000 ALTER + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + DAF0 JOIN 000002 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + IA0 CONNECT 000004 READ + CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS + REVOKE DATE=NONE RESUME DATE=NONE + ESH25 USE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + PROJECTB USE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + RV2 CREATE 000002 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + RV3 CREATE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE + ADM1 JOIN 000001 READ + CONNECT ATTRIBUTES=OPERATIONS + REVOKE DATE=NONE RESUME DATE=NONE + AEH0 USE 000000 READ + CONNECT ATTRIBUTES=NONE + REVOKE DATE=NONE RESUME DATE=NONE -- cgit v1.2.3-70-g09d2