#!/bin/bash # # Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one # or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at # https://securityonion.net/license; you may not use this file except in compliance with the # Elastic License 2.0. {%- set MANAGER = salt['grains.get']('master') %} {%- set VERSION = salt['pillar.get']('global:soversion') %} {%- set IMAGEREPO = salt['pillar.get']('global:imagerepo') %} {%- set MANAGERIP = salt['pillar.get']('global:managerip') %} {%- set URLBASE = salt['pillar.get']('global:url_base') %} . /usr/sbin/so-common function usage { cat << EOF Usage: $0 [options] [pcap-file-2] [pcap-file-N] Imports one or more PCAP files onto a sensor node. The PCAP traffic will be analyzed and made available for review in the Security Onion toolset. Options: --json Outputs summary in JSON format. Implies --quiet. --quiet Silences progress information to stdout. EOF } quiet=0 json=0 INPUT_FILES= while [[ $# -gt 0 ]]; do param=$1 shift case "$param" in --json) json=1 quiet=1 ;; --quiet) quiet=1 ;; -*) echo "Encountered unexpected parameter: $param" usage exit 1 ;; *) if [[ "$INPUT_FILES" != "" ]]; then INPUT_FILES="$INPUT_FILES $param" else INPUT_FILES="$param" fi ;; esac done function status { msg=$1 [[ $quiet -eq 1 ]] && return echo "$msg" } function pcapinfo() { PCAP=$1 ARGS=$2 docker run --rm -v "$PCAP:/input.pcap" --entrypoint capinfos {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -ae $ARGS } function pcapfix() { PCAP=$1 PCAP_OUT=$2 docker run --rm -v "$PCAP:/input.pcap" -v $PCAP_OUT:$PCAP_OUT --entrypoint pcapfix {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -o $PCAP_OUT > /dev/null 2>&1 } function suricata() { PCAP=$1 HASH=$2 NSM_PATH=/nsm/import/${HASH}/suricata mkdir -p $NSM_PATH chown suricata:socore $NSM_PATH LOG_PATH=/opt/so/log/suricata/import/${HASH} mkdir -p $LOG_PATH chown suricata:suricata $LOG_PATH docker run --rm \ -v /opt/so/conf/suricata/suricata.yaml:/etc/suricata/suricata.yaml:ro \ -v /opt/so/conf/suricata/threshold.conf:/etc/suricata/threshold.conf:ro \ -v /opt/so/rules/suricata/:/etc/suricata/rules:ro \ -v ${LOG_PATH}:/var/log/suricata/:rw \ -v ${NSM_PATH}/:/nsm/:rw \ -v "$PCAP:/input.pcap:ro" \ -v /dev/null:/nsm/suripcap:rw \ -v /opt/so/conf/suricata/bpf:/etc/suricata/bpf:ro \ {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-suricata:{{ VERSION }} \ --runmode single -k none -r /input.pcap > $LOG_PATH/console.log 2>&1 } function zeek() { PCAP=$1 HASH=$2 NSM_PATH=/nsm/import/${HASH}/zeek mkdir -p $NSM_PATH/logs mkdir -p $NSM_PATH/extracted mkdir -p $NSM_PATH/spool chown -R zeek:socore $NSM_PATH docker run --rm \ -v $NSM_PATH/logs:/nsm/zeek/logs:rw \ -v $NSM_PATH/spool:/nsm/zeek/spool:rw \ -v $NSM_PATH/extracted:/nsm/zeek/extracted:rw \ -v "$PCAP:/input.pcap:ro" \ -v /opt/so/conf/zeek/local.zeek:/opt/zeek/share/zeek/site/local.zeek:ro \ -v /opt/so/conf/zeek/node.cfg:/opt/zeek/etc/node.cfg:ro \ -v /opt/so/conf/zeek/zeekctl.cfg:/opt/zeek/etc/zeekctl.cfg:ro \ -v /opt/so/conf/zeek/policy/securityonion:/opt/zeek/share/zeek/policy/securityonion:ro \ -v /opt/so/conf/zeek/policy/custom:/opt/zeek/share/zeek/policy/custom:ro \ -v /opt/so/conf/zeek/policy/cve-2020-0601:/opt/zeek/share/zeek/policy/cve-2020-0601:ro \ -v /opt/so/conf/zeek/policy/intel:/opt/zeek/share/zeek/policy/intel:rw \ -v /opt/so/conf/zeek/bpf:/opt/zeek/etc/bpf:ro \ --entrypoint /opt/zeek/bin/zeek \ -w /nsm/zeek/logs \ {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-zeek:{{ VERSION }} \ -C -r /input.pcap local > $NSM_PATH/logs/console.log 2>&1 } # if no parameters supplied, display usage if [ "$INPUT_FILES" == "" ]; then usage exit 1 fi # ensure this is a sensor node if [ ! -d /opt/so/conf/suricata ]; then echo "This command must be run on a sensor node." exit 3 fi # verify that all parameters are files for i in $INPUT_FILES; do if ! [ -f "$i" ]; then echo "\"$i\" is not a valid file!" exit 2 fi done # track oldest start and newest end so that we can generate the Kibana search hyperlink at the end START_OLDEST="2050-12-31" END_NEWEST="1971-01-01" INVALID_PCAPS_COUNT=0 VALID_PCAPS_COUNT=0 SKIPPED_PCAPS_COUNT=0 # paths must be quoted in case they include spaces for PCAP in $INPUT_FILES; do PCAP=$(/usr/bin/realpath "$PCAP") status "Processing Import: ${PCAP}" status "- verifying file" if ! pcapinfo "${PCAP}" > /dev/null 2>&1; then # try to fix pcap and then process the fixed pcap directly PCAP_FIXED=`mktemp /tmp/so-import-pcap-XXXXXXXXXX.pcap` status "- attempting to recover corrupted PCAP file" pcapfix "${PCAP}" "${PCAP_FIXED}" # Make fixed file world readable since the Suricata docker container will runas a non-root user chmod a+r "${PCAP_FIXED}" PCAP="${PCAP_FIXED}" TEMP_PCAPS+=(${PCAP_FIXED}) fi # generate a unique hash to assist with dedupe checks HASH=$(md5sum "${PCAP}" | awk '{ print $1 }') HASH_DIR=/nsm/import/${HASH} status "- assigning unique identifier to import: $HASH" pcap_data=$(pcapinfo "${PCAP}") if ! echo "$pcap_data" | grep -q "Earliest packet time:" || echo "$pcap_data" |egrep -q "Latest packet time: 1970-01-01|Latest packet time: n/a"; then status "- this PCAP file is invalid; skipping" INVALID_PCAPS_COUNT=$((INVALID_PCAPS_COUNT + 1)) else if [ -d $HASH_DIR ]; then status "- this PCAP has already been imported; skipping" SKIPPED_PCAPS_COUNT=$((SKIPPED_PCAPS_COUNT + 1)) else VALID_PCAPS_COUNT=$((VALID_PCAPS_COUNT + 1)) PCAP_DIR=$HASH_DIR/pcap mkdir -p $PCAP_DIR # generate IDS alerts and write them to standard pipeline status "- analyzing traffic with Suricata" suricata "${PCAP}" $HASH {% if salt['pillar.get']('global:mdengine') == 'ZEEK' %} # generate Zeek logs and write them to a unique subdirectory in /nsm/import/zeek/ # since each run writes to a unique subdirectory, there is no need for a lock file status "- analyzing traffic with Zeek" zeek "${PCAP}" $HASH {% endif %} fi if [[ "$HASH_FILTERS" == "" ]]; then HASH_FILTERS="import.id:${HASH}" HASHES="${HASH}" else HASH_FILTERS="$HASH_FILTERS%20OR%20import.id:${HASH}" HASHES="${HASHES} ${HASH}" fi START=$(pcapinfo "${PCAP}" -a |grep "Earliest packet time:" | awk '{print $4}') END=$(pcapinfo "${PCAP}" -e |grep "Latest packet time:" | awk '{print $4}') status "- found PCAP data spanning dates $START through $END" # compare $START to $START_OLDEST START_COMPARE=$(date -d $START +%s) START_OLDEST_COMPARE=$(date -d $START_OLDEST +%s) if [ $START_COMPARE -lt $START_OLDEST_COMPARE ]; then START_OLDEST=$START fi # compare $ENDNEXT to $END_NEWEST ENDNEXT=`date +%Y-%m-%d --date="$END 1 day"` ENDNEXT_COMPARE=$(date -d $ENDNEXT +%s) END_NEWEST_COMPARE=$(date -d $END_NEWEST +%s) if [ $ENDNEXT_COMPARE -gt $END_NEWEST_COMPARE ]; then END_NEWEST=$ENDNEXT fi cp -f "${PCAP}" "${PCAP_DIR}"/data.pcap chmod 644 "${PCAP_DIR}"/data.pcap fi # end of valid pcap status done # end of for-loop processing pcap files # remove temp files for TEMP_PCAP in ${TEMP_PCAPS[@]}; do status "- removing temporary pcap $TEMP_PCAP" rm -f $TEMP_PCAP done # output final messages if [[ $INVALID_PCAPS_COUNT -gt 0 ]]; then status status "WARNING: One or more pcaps was invalid. Scroll up to see which ones were invalid." fi START_OLDEST_SLASH=$(echo $START_OLDEST | sed -e 's/-/%2F/g') END_NEWEST_SLASH=$(echo $END_NEWEST | sed -e 's/-/%2F/g') if [[ $VALID_PCAPS_COUNT -gt 0 ]] || [[ $SKIPPED_PCAPS_COUNT -gt 0 ]]; then URL="https://{{ URLBASE }}/#/dashboards?q=$HASH_FILTERS%20%7C%20groupby%20event.module*%20%7C%20groupby%20-sankey%20event.module*%20event.dataset%20%7C%20groupby%20event.dataset%20%7C%20groupby%20source.ip%20%7C%20groupby%20destination.ip%20%7C%20groupby%20destination.port%20%7C%20groupby%20network.protocol%20%7C%20groupby%20rule.name%20rule.category%20event.severity_label%20%7C%20groupby%20dns.query.name%20%7C%20groupby%20file.mime_type%20%7C%20groupby%20http.virtual_host%20http.uri%20%7C%20groupby%20notice.note%20notice.message%20notice.sub_message%20%7C%20groupby%20ssl.server_name%20%7C%20groupby%20source.as.organization.name%20source.geo.country_name%20%7C%20groupby%20destination.as.organization.name%20destination.geo.country_name&t=${START_OLDEST_SLASH}%2000%3A00%3A00%20AM%20-%20${END_NEWEST_SLASH}%2000%3A00%3A00%20AM&z=UTC" status "Import complete!" status status "Use the following hyperlink to view the imported data. Triple-click to quickly highlight the entire hyperlink and then copy it into a browser:" status "$URL" status status "or, manually set the Time Range to be (in UTC):" status "From: $START_OLDEST To: $END_NEWEST" status status "Note: It can take 30 seconds or more for events to appear in Security Onion Console." RESULT=0 else START_OLDEST= END_NEWEST= URL= RESULT=1 fi if [[ $json -eq 1 ]]; then jq -n \ --arg success_count "$VALID_PCAPS_COUNT" \ --arg fail_count "$INVALID_PCAPS_COUNT" \ --arg skipped_count "$SKIPPED_PCAPS_COUNT" \ --arg begin_date "$START_OLDEST" \ --arg end_date "$END_NEWEST" \ --arg url "$URL" \ --arg hashes "$HASHES" \ '''{ success_count: $success_count, fail_count: $fail_count, skipped_count: $skipped_count, begin_date: $begin_date, end_date: $end_date, url: $url, hash: ($hashes / " ") }''' fi exit $RESULT