{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "# Zeek to Spark Clustering\n", "In this notebook we will pull Zeek data into Spark then do some analysis and clustering. The first step is to convert your Zeek log data into a Parquet file, for instructions on how to do this (just a few lines of Python code using the ZAT package) please see this notebook:\n", "\n", "\n", "\n", "### See these related notebooks\n", "- [Zeek to Parquet](https://nbviewer.jupyter.org/github/SuperCowPowers/zat/blob/main/notebooks/Zeek_to_Parquet.ipynb)\n", "- [Zeek to Spark](https://nbviewer.jupyter.org/github/SuperCowPowers/zat/blob/main/notebooks/Zeek_to_Spark.ipynb)\n", "\n", "Apache Parquet is a columnar storage format focused on performance. Reading Parquet data is fast and efficient, for this notebook we will specifically be using it for loading data into Spark.\n", "\n", "\n", "\n", "\n", "### Software\n", "- Zeek Analysis Tools (ZAT): https://github.com/SuperCowPowers/zat\n", "- Parquet: https://parquet.apache.org\n", "- Spark: https://spark.apache.org\n", "- Spark MLLib: https://spark.apache.org/mllib/\n", "\n", "### Data\n", "- About 1/2 million rows of a Zeek dns.log\n", "- Grabe the data here: [data.kitware.com](https://data.kitware.com/#collection/58d564478d777f0aef5d893a) (with headers)" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ZAT: 0.3.7\n", "PySpark: 2.4.4\n" ] } ], "source": [ "# Third Party Imports\n", "import pyspark\n", "from pyspark.sql import SparkSession\n", "\n", "# Local imports\n", "import zat\n", "\n", "# Good to print out versions of stuff\n", "print('ZAT: {:s}'.format(zat.__version__))\n", "print('PySpark: {:s}'.format(pyspark.__version__))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Spark It!\n", "### Spin up Spark with 4 Parallel Executors\n", "Here we're spinning up a local spark server with 4 parallel executors, although this might seem a bit silly since we're probably running this on a laptop, there are a couple of important observations:\n", "\n", "\n", "\n", "- If you have 4/8 cores use them!\n", "- It's the exact same code logic as if we were running on a distributed cluster.\n", "- We run the same code on **DataBricks** (www.databricks.com) which is awesome BTW.\n", "\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Spin up a local Spark Session (with 4 executors)\n", "spark = SparkSession.builder.master(\"local[4]\").appName('my_awesome').getOrCreate()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "## Read in our Parquet File\n", "Here we're loading in a Zeek DNS log with ~1/2 million rows to demonstrate the functionality and do some analysis and clustering on the data. For more information on converting Zeek logs to Parquet files please see our Zeek to Spark notebook:\n", "\n", "#### Zeek logs to Parquet Notebook\n", "- [Zeek to Spark (and Parquet)](https://nbviewer.jupyter.org/github/SuperCowPowers/zat/blob/main/notebooks/Zeek_to_Spark.ipynb)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# Have Spark read in the Parquet File\n", "spark_df = spark.read.parquet('/Users/briford/data/bro/dns.parquet')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Lets look at our data\n", "We should always inspect out data when it comes in. Look at both the data values and the data types to make sure you're getting exactly what you should be." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of Rows: 427935\n", "Columns: ts,uid,id_orig_h,id_orig_p,id_resp_h,id_resp_p,proto,trans_id,query,qclass,qclass_name,qtype,qtype_name,rcode,rcode_name,AA,TC,RD,RA,Z,answers,TTLs,rejected\n" ] } ], "source": [ "# Get information about the Spark DataFrame\n", "num_rows = spark_df.count()\n", "print(\"Number of Rows: {:d}\".format(num_rows))\n", "columns = spark_df.columns\n", "print(\"Columns: {:s}\".format(','.join(columns)))" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "+----------+-----+------+\n", "|qtype_name|proto| count|\n", "+----------+-----+------+\n", "| A| udp|212473|\n", "| NB| udp| 77199|\n", "| AAAA| udp| 54519|\n", "| PTR| udp| 52991|\n", "| TXT| udp| 12644|\n", "| SRV| udp| 12268|\n", "| -| udp| 3472|\n", "| *| udp| 882|\n", "| AXFR| tcp| 440|\n", "| SOA| udp| 346|\n", "| TXT| tcp| 226|\n", "| -| tcp| 176|\n", "| MX| udp| 169|\n", "| NS| udp| 43|\n", "| HINFO| udp| 30|\n", "| NAPTR| udp| 27|\n", "| PTR| tcp| 26|\n", "| A| tcp| 4|\n", "+----------+-----+------+\n", "\n" ] } ], "source": [ "spark_df.groupby('qtype_name','proto').count().sort('count', ascending=False).show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "# Data looks good, lets take a deeper dive\n", "Spark has a powerful SQL engine as well as a Machine Learning library. So now that we've loaded our Zeek data we're going to utilize the Spark SQL commands to do some investigation of our data including clustering from the MLLib.\n", "\n", "\n", "" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "# Plotting defaults\n", "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", "from zat.utils import plot_utils\n", "plot_utils.plot_defaults()" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# Add a column with the string length of the DNS query\n", "from pyspark.sql.functions import col, length\n", "\n", "# Create new dataframe that includes two new column\n", "spark_df = spark_df.withColumn('query_length', length(col('query')))\n", "spark_df = spark_df.withColumn('answer_length', length(col('answers')))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Text(0, 0.5, 'Counts')" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "