Hands-on: Network Traffic Analysis

Do you know about the most famous cyber incident, The Case of the Stolen Szechuan Sauce by DFIR Madness?

  • Yes? Great! In this hands-on exercise, you'll have the opportunity to analyze the case once again. More importantly, you'll prepare insightful analytical notebooks that will help you solve this case (and others in the future) with just a few clicks in the interactive environment of Jupyter notebooks.

  • No? Don’t worry! You don’t need prior knowledge of the case. You will work with the provided dataset for the Szechuan case (network traffic capture and memory dump), and we’ve included enough information to keep you on track. The main goal of this exercise isn’t to test your analytical skills but rather to guide you in creating interactive and friendly notebooks.

If you find some extra time during notebook preparation, feel free to enjoy the analysis as well. More details on the case can be found on the original page at https://dfirmadness.com/the-stolen-szechuan-sauce/.

Stolen szechuan sauce logo
The Stolen Szechuan Sauce [image courtesy of artwork-tee]

Case story and analytical tasks

The case begins with discovering a newly developed Szechuan sauce recipe (created by a rather eccentric scientist) on the dark web. This recipe was saved only in a file server on a Domain Controller accessible only from an internal network (range 10.42.X.Y – though the scientists doesn't quite recall the full address). The Domain Controller is now suspected to have been compromised by an attacker who somehow gained access to the server and stole the recipe.

As FBI agents, you've obtained several key artifacts for this case, including a memory dump of the affected Domain Controller and a PCAP file captured by the "edge sensor" on the victim’s network. The memory dump analysis will be covered in the next hands-on exercise. You will now analyze the second artifact provided – the PCAP file.

Your task lies in the automation of the following analytical workflow:

  1. Suricata Analysis – automate the analysis of the PCAP file using Suricata to extract alerts;
  2. Interactive results visualization – display these alerts in an interactive table for further analysis and search;
  3. IP addresses extraction – extract all possible IP addresses from the PCAP file;
  4. Threat intelligence analysis – use VirusTotal to retrieve additional data for the extracted IP addresses.

Note

Threat intelligence analysis uses the VirusTotal database as the Observable analysis notebook. Therefore, this analysis will utilize offline reports from previous analyses to prevent excessive request generation. However, if you have a VirusTotal API key, you can put it into the configuration.

Preparation

For this analysis, ensure that case001.pcap is downloaded in the data/ directory. If you haven't downloaded the file during the preparation stage, follow the instructions in the Preparation → Tutorial section.

A key component of this analytical notebook is the use of Suricata. To simplify the setup and eliminate the need for manual installation, we have provided it as a Docker/Podman image. The configuration is already set up, so no additional changes are required. Additionally, we have prepared a connector module to manage Suricata container and handle data processing functions. You can find it in the connectors/toolbox/suricata/suricata.py file.

Note

In addition to Suricata, we provide other Docker/Podman images, all available in the Toolbox repository. You can build them locally or use already prepared builds available in the Container registry (these are set in the configuration). Each of these tools also has an implemented connector that can be easily loaded in a notebook as a module.

The Suricata image is pre-built for PCAP analysis and uses the rules located at /var/lib/suricata/rules/suricata.rules (in the container image), which are sufficient for this case. For alerts, we utilize the fast.log output file from the PCAP analysis, which contains basic alert information in a human-readable format. For more information, see command line options and fast.log format description.

The easiest way to write interactive analytical notebooks (but of course, not the only approach) is to follow the following code structure for each analytical section:

  1. Define all necessary widgets – You can use ipywidgets or any other widget you found. Don't forget to include Output.
  2. Implement helper functions (optional) – These functions should be bound to the user interface logic, and they should be easily refactor-able into separate modules.
  3. Implement widget functions – Functions that are called by or interact with widgets, they connect widget events to the helper functions or contain the whole code in case of smaller actions.
  4. Display all widgets – You can use various layouts to combine and arrange defined widgets.

For each code section, screenshots of the expected UI component setup will be provided. You can use it as an inspiration.

Initial setup

To create the interactive notebook for analysis of network traffic using Suricata, use the prepared template notebook notebooks/4-pcap_analysis.ipynb. This notebook is based on the 1-template.ipynb described in the Examples → Notebook Example section. The parts that need to be completed are marked with a # TODO comment.

To edit the notebook, you need to open it as a regular notebook instead of using the Voila view. To do so, right click on the notebook in the left panel and select Open With → Notebook (see the following screenshots). This opens a classic notebook where you can easily edit the individual sections.

To see how the notebook is rendered using Voila, click on the Voila icon (a yellow wave with a blue line) in the notebook control panel. This will open the Voila view in a new panel. By default, this view does not automatically update when you save changes to the notebook. To refresh it, simply reload the page.

Notebook view open
Open using Notebook view
Opened notebook
Opened notebook
Voila view
Voila view

Note

If you ever get stuck or are unsure about the next steps, you can refer to the final version of the notebook prepared in tutorial_solutions/4-pcap_analysis.ipynb. But we believe that won't be necessary.

Required edits

Before you start implementing any step of the analytical workflow, you need to prepare the whole notebook environment. Thanks to the prepared template you don't have to define so much. All you need to do is the following three steps.

1. Update initial notebook description

Each section of the notebook can include comments written in Markdown, including the header. To get familiar with it, add your name or nickname to the notebook header. To change the <INSERT YOUR NAME> value, you need to double click on the section, which will switch the comment to the editing mode. To switch back to the view mode, just run the cell ( icon in the notebook control panel).

2. Import all required modules

Insert all modules imports necessary for this notebook (widgets, common data processing modules, or connectors). We recommend importing them only when needed. But to make it easier for you to implement the notebook, modify the include as follows:

# General modules
import sys  # System functions
from pathlib import Path  # OS path searching
from loguru import logger  # Standard logging functionality with colors functionality
import ipywidgets as widgets  # Large widgets library for Jupyter notebooks
import pandas as pd  # Pandas data processing
import itables  # Interactive tables for Jupyter notebooks
from ipyfilechooser import FileChooser  # File picker widget
import perspective  # Perspective table widget
from perspective.widget import PerspectiveWidget  # Perspective table widget

# Add parent folder to the path
sys.path.append(str(Path("../")))

# Platform and notebook specific modules
from connectors.configuration import connectors_configuration  # Connectors configuration
from connectors.docker import docker  # Docker connector
from connectors.podman import podman  # Podman connector
from connectors.virustotal import virustotal  # VirusTotal API module
from connectors.toolbox.suricata import suricata  # Suricata connector
from utilities.suricata_helpers import filter_network_ranges, _read_fast_log, create_virustotal_table_for_ips  # Utilities modules

3. Define used toolbox images IDs

Update the REQUIRED_TOOLBOX_IMAGES_IDS global variable by adding "suricata" value to the list. No other tools from the toolbox are needed for this notebook.

Note

During the implementation you will work with the running_toolbox_containers global variable, which is used to continuously store running containers so that they can be destroyed after the analysis is finished (if they are not terminated earlier).

Analytical part: Suricata analysis

This is the main analytical part of the notebook providing PCAP analysis and IP address evaluation using the VirusTotal database. Specifically, the following steps need to be implemented:

  1. allow the user to select a .pcap file for analysis (use the FileChooser widget),
  2. run Suricata analysis on the selected .pcap file,
  3. display alerts from the generated fast.log output file in a table (use the Perspective widget),
  4. add network range filtering options to apply over the dataset,
  5. extract all IP addresses from the current (filtered) dataset,
  6. display the extracted IoCs in a separate table (use the ITables widget) along with information from their VirusTotal records.

Widgets definition

As the first step, you need to prepare all the Python widgets representing user interface components. Use the following screenshots to identify which widgets you will need.

Suricata overview
Overview of Suricata UI components
Alerts table
Suricata alerts table
VirusTotal table
VirusTotal overview for extracted IoCs

You might be able to tell from the screenshots that you'll definitely need FileChooser and Text widgets. Well, here is their definition:

w_input_file_pcap_chooser = FileChooser(
    path="../data/",
    filter_pattern=["*.pcap"],
    select_desc="Select",
    dir_icon="/",
    dir_icon_append=True,
    layout={"width": "auto", "margin": "1em 0 0 0"}
)

w_exclude_src_networks_text = widgets.Text(
    value='',
    placeholder='Exclude src networks',
    description='Exclude src networks:',
    disabled=False,
    layout={"width": "50em", "margin": "1em 0 0 0"},
    style={'description_width': 'initial'}
)

Besides other widgets from the IPyWidgets, we will use two types of tables: Perspective and Itables, each with its own pros and cons. These table widgets do not need to be defined in the Widgets definition section, as they will be called as functions and rendered into the defined Output widget.

The Perspective table (used for alerts) comes with built-in options for filtering and aggregating any value in the table. The only downside we've encountered is that Jupyter Labs does not support rendering HTML text in these tables, meaning clickable links cannot be included unless custom rendering logic is provided. To avoid this, we use Itables for the VirusTotal output, as it allows us to include clickable links to the VirusTotal website in the last column.

Example of using Perspective and Itables widgets The following code provides a simple demonstration of how to use both widgets, allowing you to easily test and compare them.
 
# Data definition
columns = ['IP A', 'IP B']
df = pandas.DataFrame(data: list[list[str]], columns)

### ITABLES TABLE ###

# To show pandas DataFrame in Itables with columns "IP A" and "IP B"
itables.show(df)

### PERSPECTIVE TABLE ###

# Create Perspective table
table = perspective.table(df)

# Configure widget with useful defaults to show table with columns "IP A" and "IP B"
viewer = PerspectiveWidget(
    table,
    columns=columns,
    sort=[["Timestamp", "desc"]],  # Default sort by latest
    plugin="Datagrid",  # Or "Datagrid" for spreadsheet view
    settings=True  # Show column/plugin picker
)

display(viewer)

You already know everything you need, so try to define all the necessary widgets yourself. If you get stuck, check out the following hidden solution.

Solution: Widgets definition
"""Widgets definition.
"""

w_input_pcap_select_label = widgets.Label(
    value="Select pcap file to analyse: ",
    layout={"margin": "0 1em 0 0"},
    style={"font_weight": "bold"}
)

w_input_file_pcap_chooser = FileChooser(
    path="../data/",
    filter_pattern=["*.pcap"],
    select_desc="Select",
    dir_icon="/",
    dir_icon_append=True,
    layout={"width": "auto", "margin": "1em 0 0 0"}
)

w_start_analysis_button = widgets.Button(
    description="Start .pcap analysis",
    tooltip="Start .pcap analysis",
    button_style="primary",
    disabled=True,
    layout={"width": "20em", "margin": "1em 0 0 0"}
)

w_input_pcap_filter_label = widgets.Label(
    value="Filter out source or destination network ranges (use comman ',' as delimiter):",
    layout={"margin": "1em 1em 1em"},
    style={"font_weight": "bold"}
)

w_filter_ranges_button = widgets.Button(
    description="Filter out specified ranges",
    tooltip="Filter out specified ranges",
    button_style="primary",
    disabled=True,
    layout={"width": "15em", "margin": "1em 1em 1em 1em"}
)

w_exclude_src_networks_text = widgets.Text(
    value='',
    placeholder='Exclude src networks',
    description='Exclude src networks:',
    disabled=False,
    layout={"width": "50em", "margin": "1em 0 0 0"},
    style={'description_width': 'initial'}
)

w_exclude_dst_networks_text = widgets.Text(
    value='',
    placeholder='Exclude dst networks',
    description='Exclude dst networks:',
    disabled=False,
    layout={"width": "50em", "margin": "1em 0 0 0"},
    style={'description_width': 'initial'}
)

w_virustotal_button = widgets.Button(
    description="VirusTotal for IP addresses in events",
    tooltip="VirusTotal",
    button_style="primary",
    disabled=True,
    layout={"width": "20em", "margin": "1em 0 0 0"}
)

w_suricata_analysis_output = widgets.Output(layout={"margin": "1em 0 0 0"})
w_virustotal_output = widgets.Output(layout={"margin": "1em 0 0 0"})
"""Widgets display.
"""

extract_files_select_box = widgets.VBox([w_input_pcap_select_label, w_input_file_pcap_chooser])
filter_out_src_ranges = widgets.HBox([w_exclude_src_networks_text])
filter_out_dst_ranges = widgets.HBox([w_exclude_dst_networks_text])
filter_out_button = widgets.HBox([w_filter_ranges_button, w_virustotal_button])

widgets.VBox([extract_files_select_box, w_start_analysis_button, w_input_pcap_filter_label, filter_out_src_ranges, filter_out_dst_ranges,
              filter_out_button, w_suricata_analysis_output, w_virustotal_output])

Widgets functions definition

Now, it's time to implement the functions that provide the main functionality, which will be triggered by the on-click Button widgets. In this workflow, there are three main buttons for which we need to prepare the corresponding functions:

  • Start .pcap analysis
  • Filter out specified ranges
  • VirusTotal for IP addresses in events

Note

There is a module utilities/suricata_helpers.py containing helper functions useful for this exercise. Feel free to use them in following parts.

For both parts (analyzing and filtering the dataset) you'll ultimately want to display the Suricata alerts in a Perspective table. Therefore, as a first step, complete the following function template that reads the extracted Suricata alerts from the fast.log file (the human-readable Suricata alerts from the PCAP analysis), applies filters to exclude unwanted network ranges, and returns the filtered DataFrame to be displayed in the Perspective table.

Along with this function, we will define a global variablessuricata_alerts_handler that stores the up-to-date view of the Perspective table, which will be used when the current table's content needs to be accessed.

# The handler into up-to-date state of Perspective table with Suricata logs
suricata_alerts_handler = None

def display_suricata_alerts(pcap_filename: str):
    """Displays the Suricata log as an interactive Perspective table.

        Args:
            pcap_filename: The filename of .pcap for which to run Suricata.
    """

    exclude_dst_ranges = w_exclude_dst_networks_text.value.strip()
    exclude_src_ranges = w_exclude_src_networks_text.value.strip()

    # Check whether data for the selected file already exists
    fast_log_path = f"../data/suricata-logs-{pcap_filename}/fast.log"
    df = read_fast_log(fast_log_path) # /utilities/suricata_helpers.py
    df_filtered = filter_network_ranges(df, exclude_dst_ranges, exclude_src_ranges)

    # Clean data types for better Perspective handling
    df_filtered.loc[:, 'Priority'] = df_filtered['Priority'].astype(int)
    df_filtered.loc[:, 'Timestamp'] = pd.to_datetime(df_filtered['Timestamp'])

    table = perspective.table(df_filtered)
    # Configure widget with useful defaults
    viewer = PerspectiveWidget(
        table,
        columns=['Timestamp', 'Alert', 'Classification', 'Priority', 'Protocol', 'Source IP', 'Destination IP'],
        sort=[["Timestamp", "desc"]],  # Default sort by latest
        plugin="Datagrid",  # Or "Datagrid" for spreadsheet view
        settings=True  # Show column/plugin picker
    )


    # ...
    # TODO display alerts in Perspective table
    # ...


    # Save the handler to created table to use when needed
    global suricata_alerts_handler
    suricata_alerts_handler = viewer

Next, we recommend preparing a function that runs the Suricata container and executes Suricata analysis for the .pcap file. Make sure to call the appropriate methods from the suricata_connector to execute commands within the Suricata container. Remember, the Suricata connector is already prepared and available in the connectors/toolbox/suricata directory.

def get_suricata_events_from_container(pcap_path: str, pcap_filename: str) -> bool:
    """Process selected pcap with Suricata to get events.

    Returns:
        bool: False if files failed to store, True otherwise
    """

    # Start container
    suricata_container_name = containers_manager.start_tool(tool_id="suricata", data_path=pcap_path)
    if not suricata_container_name:
        logger.error("Cannot start container.")
        w_start_analysis_button.disabled = False
        return False
    running_toolbox_containers.append(suricata_container_name)

    suricata_connector = suricata.Suricata(containers_manager=containers_manager, container_name=suricata_container_name)

    # ...
    # TODO Prepare data/folders for Suricata analysis results (/connector/toolbox/suricata/data_preparation)
    # TODO Run Suricata for chosen pcap (/connector/toolbox/suricata/pcap_alerts)
    # TODO Store Suricata results to /data/suricata-logs folder  (/connector/toolbox/suricata/store_results)
    # ...

    # Remove container
    if not containers_manager.remove_container(container_name=suricata_container_name):
        logger.warning("Cannot remove Suricata container.")
    else:
        running_toolbox_containers.remove(suricata_container_name)
    return True

Finally, you can complete the function triggered by the Start .pcap analysis button. Note that the file_selected function ensures the user first selects a .pcap file for analysis, and only then can the Start .pcap analysis button be used. The same logic applies to the Filter out specified ranges button. This button is only enabled once the function is successfully completed and there is data available to filter.

def file_selected(chooser):
    """Enable the w_start_analysis_button if user selected a file.
    """

    if w_input_file_pcap_chooser.selected:
        w_start_analysis_button.disabled = False

w_input_file_pcap_chooser.register_callback(file_selected)


def suricata_fast_logs_exist(pcap_filename: str) -> bool:
    """Checks whether there already are results for chosen pcap file (pcap_filename).

        Args:
            pcap_filename: The filename of .pcap for which find logs.
         Returns:
            bool: True if folder with logs already exists, False otherwise.
    """

    data_folder = Path(f"../data")
    search_results = f"suricata-logs-{pcap_filename}"

    if not data_folder.is_dir():
        logger.info("There is no ../data folder.")
        return False

    return any(
        item.is_dir() and search_results in item.name
        for item in data_folder.iterdir()
    )


def analyse_with_suricata(button):
    w_suricata_analysis_output.clear_output()

    pcap_filename = w_input_file_pcap_chooser.selected_filename
    pcap_path = w_input_file_pcap_chooser.selected_path

    with w_suricata_analysis_output:


        # Check whether already exists /data/suricata-logs-{pcap_filename}:
        if suricata_fast_logs_exist(pcap_filename):
            logger.info(f"Results already stored in /data/suricata-logs-{pcap_filename}/. Displaying...\n")
            display_suricata_alerts(pcap_filename)
        else:
            logger.info(f"Extracting data from pcap file '{pcap_filename}'...")

             if # ...
                # TODO Call function to extract Suricata logs 
                # ... 

                logger.info(f"Analysis results stored in /data/suricata-logs.")

                # ...
                # TODO Display Suricata events
                # ..

            else:
                logger.error(f"Analysis of the file '{pcap_filename}' failed.")

        w_start_analysis_button.disabled = True
        w_filter_ranges_button.disabled = False
        w_virustotal_button.disabled = False

w_start_analysis_button.on_click(analyse_with_suricata) # Start .pcap analysis

The function for the Filter out specified ranges button (e.g., filter_suricata_alerts(button)) is straightforward, as you already have a function that filters out network ranges to exclude and displays Suricata alerts. Just remember to call the function within the context manager for w_suricata_analysis_output and use clear_output() before writing to it.

def filter_suricata_alerts(button):
    pcap_filename = w_input_file_pcap_chooser.selected_filename

    # ...
    # TODO Clear Suricata output
    # ...


    with w_suricata_analysis_output:

        # ...
        # ...TODO Call function that displays and filters out network ranges
        # ...

The final step is to collect all the IP addresses in the filtered dataset and call the VirusTotal connector for each of them. This will be done by the display_virustotal_results function, triggered by the VirusTotal for IP addresses in events button. Don't forget to display the results in an Itables at the end.

To get the content of the global variable suricata_alerts_handler, which is of type PerspectiveWidget (table viewer), use the command viewer.table.view(filter=suricata_alerts_handler.filter).to_dataframe(). This command retrieves the content of the table, looks for the current filter settings (where option), and returns the dataset as a DataFrame.

Don't forget to link the widget functions to the buttons that should trigger them on click!

def display_virustotal_results(button):
    w_virustotal_output.clear_output()

    with (w_virustotal_output):

        # ... 
        # TODO Get the current content of Suricata Perspective table with alerts from handler
        # suricata_events_df = ???
        # ...

        if suricata_events_df is None:
            logger.error("You need to get alerts first, start .pcap analysis. ")
            return

        # Collect all IP addresses to run VirusTotal for
        ips = set(ip for ip in suricata_events_df["Source IP"]).union(set(ip for ip in suricata_events_df["Destination IP"]))
        if not ips:
            logger.error("There are no IoC to analyze via TI tools.")
            return

        virustotal.initialize()
        ioc_df = create_virustotal_table_for_ips(ips) # TODO Hmm, function is not working ... (/utilities/suricata_helpers.py)

        # ...
        # TODO Show data with IoC list in Itables table 
        # ...

Amazing! If everything went well up until now, you've created your first hands-on notebook for automated PCAP analysis by Suricata. You may compare your implementation with ours in the following hidden solution.

Solution: Widgets functions definition
"""
Widgets functions
"""

# The handler into up-to-date state of Perspective table with Suricata logs
suricata_alerts_handler = None

def display_suricata_alerts(pcap_filename: str):
    """Displays the Suricata log as an interactive Perspective table.

        Args:
            pcap_filename: The filename of .pcap for which to run Suricata.
    """

    exclude_dst_ranges = w_exclude_dst_networks_text.value.strip()
    exclude_src_ranges = w_exclude_src_networks_text.value.strip()

    # Check whether data for the selected file already exists
    fast_log_path = f"../data/suricata-logs-{pcap_filename}/fast.log"
    df = read_fast_log(fast_log_path)
    df_filtered = filter_network_ranges(df, exclude_dst_ranges, exclude_src_ranges)

    # Clean data types for better Perspective handling
    df_filtered.loc[:, 'Priority'] = df_filtered['Priority'].astype(int)
    df_filtered.loc[:, 'Timestamp'] = pd.to_datetime(df_filtered['Timestamp'])

    table = perspective.table(df_filtered)
    # Configure widget with useful defaults
    viewer = PerspectiveWidget(
        table,
        columns=['Timestamp', 'Alert', 'Classification', 'Priority', 'Protocol', 'Source IP', 'Destination IP'],
        sort=[["Timestamp", "desc"]],  # Default sort by latest
        plugin="Datagrid",  # Or "Datagrid" for spreadsheet view
        settings=True  # Show column/plugin picker
    )

    # Display table
    display(viewer)

    # Save the handler to created table to use when needed
    global suricata_alerts_handler
    suricata_alerts_handler = viewer


def get_suricata_events_from_container(pcap_path: str, pcap_filename: str) -> bool:
    """Process selected pcap with Suricata to get events.

    Returns:
        bool: False if files failed to store, True otherwise
    """

    # Start container
    suricata_container_name = docker_client.start_tool(tool_id="suricata", data_path=pcap_path)
    if not suricata_container_name:
        logger.error("Cannot start container.")
        w_start_analysis_button.disabled = False
        return False
    running_docker_toolbox_containers.append(suricata_container_name)

    suricata_connector = suricata.Suricata(containers_manager=docker_client, container_name=suricata_container_name)
    suricata_connector.data_preparation()
    suricata_connector.pcap_alerts(pcap_filename=pcap_filename)
    suricata_connector.store_results(pcap_filename=pcap_filename) # to /data/suricata-logs-{pcap_filename} folder

    # Remove container
    if not docker_client.remove_container(container_name=suricata_container_name):
        logger.warning("Cannot remove Suricata container.")
    else:
        running_docker_toolbox_containers.remove(suricata_container_name)
    return True


def suricata_fast_logs_exist(pcap_filename: str) -> bool:
    """Checks whether there already are results for chosen pcap file (pcap_filename).

        Args:
            pcap_filename: The filename of .pcap for which find logs.
         Returns:
            bool: True if folder with logs already exists, False otherwise.
    """

    data_folder = Path(f"../data")
    search_results = f"suricata-logs-{pcap_filename}"

    if not data_folder.is_dir():
        logger.info("There is no ../data folder.")
        return False

    return any(
        item.is_dir() and search_results in item.name
        for item in data_folder.iterdir()
    )


def analyse_with_suricata(button):
    w_start_analysis_button.disabled = True

    w_suricata_analysis_output.clear_output()

    pcap_filename = w_input_file_pcap_chooser.selected_filename
    pcap_path = w_input_file_pcap_chooser.selected_path

    with w_suricata_analysis_output:
        # already exist /data/suricata-logs-{pcap_filename}:

        if suricata_fast_logs_exist(pcap_filename):
            logger.info(f"Results already stored in /data/suricata-logs-{pcap_filename}/. Displaying...\n")
            display_suricata_alerts(pcap_filename)
        else:
            logger.info(f"Extracting data from pcap file '{pcap_filename}'...")
            if get_suricata_events_from_container(pcap_path=pcap_path, pcap_filename=pcap_filename):
                logger.info(f"Analysis results stored in /data/suricata-logs-{pcap_filename}/ .")
                display_suricata_alerts(pcap_filename)
            else:
                logger.error(f"Analysis on file '{pcap_filename}' failed.")

        w_start_analysis_button.disabled = False
        w_filter_ranges_button.disabled = False
        w_virustotal_button.disabled = False


def file_selected(chooser):
    """Enable the w_start_analysis_button if user selected a file.
    """
    if w_input_file_pcap_chooser.selected:
        w_start_analysis_button.disabled = False

w_input_file_pcap_chooser.register_callback(file_selected)

def filter_suricata_alerts(button):
    w_suricata_analysis_output.clear_output()
    pcap_filename = w_input_file_pcap_chooser.selected_filename

    with w_suricata_analysis_output:
        display_suricata_alerts(pcap_filename)


def display_virustotal_results(button):
    w_virustotal_output.clear_output()

    with (w_virustotal_output):

        # Get current content of Suricata Perspective table with alerts
        suricata_events_df = suricata_alerts_handler.table.view(filter=suricata_alerts_handler.filter).to_dataframe()

        if suricata_events_df is None:
            logger.error("You need to get alerts first, start .pcap analysis. ")
            return

        # Collect all IP addresses to run VirusTotal for
        ips = set(ip for ip in suricata_events_df["Source IP"]).union(set(ip for ip in suricata_events_df["Destination IP"]))
        if not ips:
            logger.error("There are no IoC to analyze via TI tools.")
            return

        virustotal.initialize()
        ioc_df = create_virustotal_table_for_ips(ips)

        itables.show(ioc_df)

w_start_analysis_button.on_click(analyse_with_suricata) # Start .pcap analysis
w_filter_ranges_button.on_click(filter_suricata_alerts)
w_virustotal_button.on_click(display_virustotal_results)

Analysis time

To make this hands-on exercise more than just creating an analytical notebook, take a step further and analyze the capture yourself. Review Suricata's alert events and try to identify the tool the attacker used for reconnaissance (e.g., network scanning). Are there any IP addresses within these alerts, and could any of them potentially be Indicators of Compromise (IoCs)?

Analysis result In the Suricata alerts, `Nmap` event can be found from `194.61.24.102` to `10.42.85.10`. Since `10.42.85.10` is within the attacked internal network, `194.61.24.102` should be added to our list of IoCs.
Nmap alert
Nmap event in Suricata alerts


If you've successfully completed all tasks, continue to the next hands-on section.