Chronos and Code Understand the systems behind modern computing.

Part 3: Splunk Detection & Automated Response

On this page
    THREAT-INTELLIGENCE DETECTION LAB · PART 3 OF 4

    Build the Splunk detection logic, normalize alert fields, and connect the detection to a Python-based automated response workflow.

    SplunkSPLPythonMISPTheHiveREST APIs

    Building the Detection Logic

    Splunk is now receiving telemetry from Endpoint-01, and the threat-intelligence layer is available for later enrichment. The next step is to turn the raw Windows Firewall activity into a structured detection that the automation workflow can use.

    The detection used in this lab watches for the controlled connection from Endpoint-01 to the Kali test server:

    Endpoint-01

    192.168.95.134

    ↓

    Windows Firewall Event

    ↓

    Splunk

    ↓

    Detect traffic to 192.168.95.135:8080

    ↓

    Extract network fields

    ↓

    Add detection context

    ↓

    Return normalized fields

    ↓

    Automation

    The automation later expects the Splunk result to contain these six fields:

    alert_name

    source_ip

    destination_ip

    destination_port

    url

    filename

    Splunk therefore needs to extract the network information from the raw firewall event and add the additional context required by the response workflow.

    1. Identify the Controlled Traffic

    The detection is based on a safe HTTP request from Endpoint-01 to the Kali test system.

    The expected connection is:

    Source:

    192.168.95.134

    Destination:

    192.168.95.135

    Destination Port:

    8080

    Protocol:

    TCP

    In Splunk, open:

    Search & Reporting

    Start with a simple search:

    index=main sourcetype=pfirewall-2 “192.168.95.135” “8080” earliest=-15m

    This searches the Windows Firewall telemetry for recent activity involving the Kali system and TCP port 8080.

    A matching firewall event should contain values similar to:

    ALLOW

    TCP

    192.168.95.134

    192.168.95.135

    <source-port>

    8080

    At this point, the event exists in Splunk, but the values still need to be converted into named fields that the automation can consume.

    2. Extract the Firewall Fields

    The Windows Firewall log stores the relevant values as space-separated fields.

    Use rex to extract them from _raw:

    | rex field=_raw “^(?<event_date>\S+)\s+(?<event_time>\S+)\s+(?<action>\S+)\s+(?<protocol>\S+)\s+(?<source_ip>\S+)\s+(?<destination_ip>\S+)\s+(?<source_port>\d+)\s+(?<destination_port>\d+)”

    This creates the following Splunk fields:

    event_date

    event_time

    action

    protocol

    source_ip

    destination_ip

    source_port

    destination_port

    For the controlled test, the important extracted values should be:

    Field

    Expected value

    action

    ALLOW

    protocol

    TCP

    source_ip

    192.168.95.134

    destination_ip

    192.168.95.135

    destination_port

    8080

    These values come directly from the Windows Firewall telemetry.

    3. Add the Detection Name

    The automation requires a consistent alert name so that the same detection can be identified across Splunk, MISP, and TheHive.

    Add:

    | eval alert_name=”Suspicious Software Update Retrieval Validation”

    The same name will later be used when the response script creates or updates the corresponding MISP event and creates the TheHive investigation case.

    4. Add the URL and Filename

    The Windows Firewall log provides network-layer information such as IP addresses and ports, but it does not record the complete HTTP URL or requested filename.

    Because this lab uses a known controlled test request, those values are added by the detection logic.

    Construct the URL:

    | eval url=”http://”.destination_ip.”:”.destination_port.”/test-file.txt”

    Then assign the known test filename:

    | eval filename=”test-file.txt”

    This produces:

    url

    http://192.168.95.135:8080/test-file.txt

    filename

    test-file.txt

    It is important to distinguish between extracted and constructed values.

    The following values come from the firewall log:

    action

    protocol

    source_ip

    destination_ip

    source_port

    destination_port

    The following values are added by the detection logic:

    alert_name

    url

    filename

    The URL and filename are therefore detection context for this controlled lab rather than values directly observed in the Windows Firewall log.

    5. Normalize the Detection Output

    The bridge script should not have to understand the original firewall-log format.

    Splunk instead returns a clean set of named fields using table:

    | table _time host action protocol source_ip destination_ip source_port destination_port alert_name url filename

    Finally, return one matching detection result:

    | head 1

    This produces a predictable structured result that can later be written into Splunk’s results.csv.gz file and passed to the automation script.

    The automation section already expects alert_name, source_ip, destination_ip, destination_port, url, and filename to be available in the Splunk result.

    6. Use the Complete Detection Search

    The complete detection search is:

    index=main sourcetype=pfirewall-2 “192.168.95.135” “8080” _index_earliest=-6m _index_latest=now

    | rex field=_raw “^(?<event_date>\S+)\s+(?<event_time>\S+)\s+(?<action>\S+)\s+(?<protocol>\S+)\s+(?<source_ip>\S+)\s+(?<destination_ip>\S+)\s+(?<source_port>\d+)\s+(?<destination_port>\d+)”

    | eval alert_name=”Suspicious Software Update Retrieval Validation”

    | eval url=”http://”.destination_ip.”:”.destination_port.”/test-file.txt”

    | eval filename=”test-file.txt”

    | table _time host action protocol source_ip destination_ip source_port destination_port alert_name url filename

    | head 1

    Run the search in Splunk.

    A successful result should contain values similar to:

    Field

    Expected value

    source_ip

    192.168.95.134

    destination_ip

    192.168.95.135

    destination_port

    8080

    alert_name

    Suspicious Software Update Retrieval Validation

    url

    http://192.168.95.135:8080/test-file.txt

    filename

    test-file.txt

    7. Understand the Detection Window

    The search uses:

    _index_earliest=-6m _index_latest=now

    The scheduled alert will later execute every five minutes.

    Using a six-minute indexed-time window gives the detection a small overlap between scheduled runs so that recently indexed firewall activity is less likely to fall between execution windows.

    The search then uses:

    | head 1

    to return one normalized result for the automation workflow.

    Repeat detections are handled later by the response layer. For example, the MISP workflow can reuse an existing event rather than creating a new event every time the same detection occurs.

    8. Understand Why Normalization Matters

    The detection and response components have separate responsibilities:

    Windows Firewall Log

    ↓

    Splunk Detection

    ↓

    rex extracts network values

    ↓

    eval adds detection context

    ↓

    table creates normalized output

    ↓

    results.csv.gz

    ↓

    cti_splunk_bridge.py

    ↓

    cti_response.py

    Splunk handles the raw telemetry and detection logic.

    The bridge handles the Splunk result format.

    The response script handles validation and the MISP and TheHive API operations.

    Keeping these responsibilities separate makes the workflow easier to troubleshoot and prevents the response script from having to parse raw Windows Firewall logs itself.

    Your existing automation section is already designed around this normalized interface and expects those six fields before launching cti_response.py.

    9. Do Not Enable the Automated Response Yet

    At this stage, only verify that the detection search produces the correct normalized fields.

    Do not manually connect the search to the response script yet.

    The next section, Automation & Response Script, will:

    Save the detection

    ↓

    Schedule it every five minutes

    ↓

    Receive results.csv.gz

    ↓

    Run cti_splunk_bridge.py

    ↓

    Launch cti_response.py

    ↓

    Send indicators to MISP

    ↓

    Create the TheHive case

    Detection Logic Checkpoint

    Before moving to the automation section, confirm:

    Windows Firewall telemetry is available in Splunk ✓

    Traffic from 192.168.95.134 to 192.168.95.135:8080

    can be identified ✓

    rex extracts the network fields ✓

    alert_name is added ✓

    URL and filename context are added ✓

    The final table contains the required automation fields ✓

    The search returns one normalized result ✓

    At this point, Splunk is no longer only collecting endpoint telemetry. It can identify the controlled activity and transform the raw firewall event into structured detection data ready for the automated response workflow.

    Automation & Response Script

    At this stage, Splunk can detect the suspicious activity and return the fields needed for investigation. The next step is to connect that detection to the intelligence and case-management layers automatically.

    Instead of placing all response logic directly inside Splunk, this lab uses two Python components:

    Splunk Scheduled Detection

    ↓

    Normalized Result Fields

    ↓

    results.csv.gz

    ↓

    Splunk Bridge Script

    ↓

    Response Script

    ↙ ↘

    MISP TheHive

    ↓

    Response Logs

    Splunk provides scripted-alert results as a compressed results.csv.gz file. A small bridge script reads that file, validates the expected fields, and passes them to a separate response script. The response script then handles the MISP and TheHive API operations. This separation keeps Splunk-specific result handling separate from the CTI response logic.

    The normalized interface uses these fields:

    alert_name

    source_ip

    destination_ip

    destination_port

    url

    filename

    These fields are generated by the Splunk detection search and passed dynamically rather than being permanently hard-coded into the response script.

    1. Prepare the Automation Files

    The automation runs on Splunk-01.

    Create two scripts:

    /opt/splunk/bin/scripts/cti_splunk_bridge.py

    /opt/splunk/bin/scripts/cti_response.py

    Their responsibilities are different:

    cti_splunk_bridge.py

    Splunk

    ↓

    results.csv.gz

    ↓

    Read + validate result fields

    ↓

    Launch cti_response.py

    cti_response.py

    Normalized detection fields

    ↓

    Input validation

    ↓

    MISP event / indicators

    ↓

    TheHive case / observables

    ↓

    Logging + exit status

    Restricted script permissions and dedicated logs allow the automation to be tested and troubleshot without exposing API credentials.

    Create the files:

    sudo nano /opt/splunk/bin/scripts/cti_splunk_bridge.py

    and

    sudo nano /opt/splunk/bin/scripts/cti_response.py

    2. Protect the Automation Files

    Restrict both scripts:

    sudo chown root:root /opt/splunk/bin/scripts/cti_splunk_bridge.py

    sudo chown root:root /opt/splunk/bin/scripts/cti_response.py

    sudo chmod 700 /opt/splunk/bin/scripts/cti_splunk_bridge.py

    sudo chmod 700 /opt/splunk/bin/scripts/cti_response.py

    Both automation scripts are restricted to root with mode 700.

    3. Store the Response API Credentials

    The response script needs authentication for:

    MISP and TheHive

    Keep these values outside the Python source code.

    Create:

    sudo nano /opt/splunk/etc/cti_secrets.conf

    MISP_API_KEY=YOUR_MISP_API_KEY

    THEHIVE_API_KEY=YOUR_THEHIVE_API_KEY

    We will put only the required MISP and TheHive configuration in this protected file and never publish the real key values.

    Restrict it:

    sudo chown root:root /opt/splunk/etc/cti_secrets.conf

    sudo chmod 600 /opt/splunk/etc/cti_secrets.conf

    Store the MISP and TheHive API credentials in /opt/splunk/etc/cti_secrets.conf with root:root ownership and mode 600.

    The response script connects to TheHive 5.7.3-1 over HTTPS and uses legacy-compatible API endpoints that this version accepts.

    To verify TheHive’s HTTPS certificate, copy the public certificate from TheHive-01 to Splunk-01. Only the public certificate is required; do not copy the private key.

    On TheHive-01:

    sudo cp /etc/nginx/ssl/thehive/thehive.crt /tmp/thehive.crt

    sudo chmod 644 /tmp/thehive.crt

    scp /tmp/thehive.crt dmz@192.168.95.133:/home/dmz/thehive.crt

    sudo rm /tmp/thehive.crt

    On Splunk-01:

    sudo mkdir -p /opt/splunk/etc/certs

    sudo mv /home/dmz/thehive.crt /opt/splunk/etc/certs/thehive.crt

    sudo chown root:root /opt/splunk/etc/certs/thehive.crt

    sudo chmod 644 /opt/splunk/etc/certs/thehive.crt

    Verify

    ls -l /opt/splunk/etc/certs/thehive.crt

    4. Build the Response Script

    This is the main CTI automation script:

    /opt/splunk/bin/scripts/cti_response.py

    It should accept these values dynamically:

    alert_name

    source_ip

    destination_ip

    destination_port

    url

    filename

    The response script should:

    Detection Fields

    ↓

    Validate Inputs

    ↓

    Read Protected API Credentials

    ↓

    Create / Update MISP Event

    ↓

    Add Detection Indicators

    ↓

    Create TheHive Case

    ↓

    Add Observables

    ↓

    Write Automation Log

    The response script uses dynamic arguments instead of one fixed scenario and includes input validation, dry-run support, logging, and meaningful exit codes.

    cti_response.py

    #!/usr/bin/python3

    import requests

    import urllib3

    from datetime import date

    import datetime

    import logging

    import argparse

    import ipaddress

    from urllib.parse import urlparse

    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    # =========================

    # LOGGING

    # =========================

    LOG_FILE = “/opt/splunk/var/log/splunk/cti_response.log”

    logging.basicConfig(

    filename=LOG_FILE,

    level=logging.INFO,

    format=”%(asctime)s | %(levelname)s | %(message)s”

    )

    def log_info(message):

    print(message)

    logging.info(message)

    def log_error(message):

    print(message)

    logging.error(message)

    # =========================

    # SECRET MANAGEMENT

    # =========================

    def load_secrets(path):

    secrets = {}

    with open(path, “r”, encoding=”utf-8″) as secret_file:

    for raw_line in secret_file:

    line = raw_line.strip()

    if not line or line.startswith(“#”):

    continue

    key, separator, value = line.partition(“=”)

    if not separator or not key.strip() or not value.strip():

    raise RuntimeError(“Invalid entry in CTI secrets file”)

    secrets[key.strip()] = value.strip()

    required_keys = {

    “MISP_API_KEY”,

    “THEHIVE_API_KEY”

    }

    missing_keys = required_keys – secrets.keys()

    if missing_keys:

    raise RuntimeError(

    f”Missing required secrets: {‘, ‘.join(sorted(missing_keys))}”

    )

    return secrets

    SECRETS = load_secrets(“/opt/splunk/etc/cti_secrets.conf”)

    # =========================

    # CONFIG

    # =========================

    MISP_URL = “https://192.168.95.131”

    MISP_API_KEY = SECRETS[“MISP_API_KEY”]

    THEHIVE_URL = “https://192.168.95.132”

    THEHIVE_CA_CERT = “/opt/splunk/etc/certs/thehive.crt”

    THEHIVE_API_KEY = SECRETS[“THEHIVE_API_KEY”]

    # Neutral defaults used for manual testing.

    # Splunk supplies these values dynamically during automated execution.

    ALERT_NAME = “ Suspicious Software Update Retrieval Validation”

    SOURCE_IP = “192.168.95.134”

    DESTINATION_IP = “192.168.95.135”

    DETECTED_URL = “http://192.168.95.135:8080/test-file.txt”

    FILENAME = “test-file.txt”

    PORT = “8080”

    # =========================

    # ARGUMENTS

    # =========================

    def parse_arguments():

    parser = argparse.ArgumentParser(

    description=”Threat Detection Lab automated CTI response workflow”

    )

    parser.add_argument(

    “–alert-name”,

    default=ALERT_NAME

    )

    parser.add_argument(

    “–source-ip”,

    default=SOURCE_IP

    )

    parser.add_argument(

    “–destination-ip”,

    default=DESTINATION_IP

    )

    parser.add_argument(

    “–port”,

    default=PORT

    )

    parser.add_argument(

    “–url”,

    default=DETECTED_URL

    )

    parser.add_argument(

    “–filename”,

    default=FILENAME

    )

    parser.add_argument(

    “–dry-run”,

    action=”store_true”,

    help=”Validate inputs without contacting MISP or TheHive”

    )

    return parser.parse_args()

    # =========================

    # INPUT VALIDATION

    # =========================

    def validate_inputs():

    errors = []

    for label, value in (

    (“source IP”, SOURCE_IP),

    (“destination IP”, DESTINATION_IP),

    ):

    try:

    ipaddress.ip_address(value)

    except ValueError:

    errors.append(f”Invalid {label}: {value}”)

    try:

    port_number = int(PORT)

    if not 1 <= port_number <= 65535:

    raise ValueError

    except ValueError:

    errors.append(f”Invalid port: {PORT}”)

    parsed_url = urlparse(DETECTED_URL)

    if (

    parsed_url.scheme not in (“http”, “https”)

    or not parsed_url.hostname

    ):

    errors.append(f”Invalid URL: {DETECTED_URL}”)

    if not ALERT_NAME.strip():

    errors.append(“Alert name cannot be empty”)

    if (

    not FILENAME.strip()

    or “/” in FILENAME

    or “\\” in FILENAME

    ):

    errors.append(f”Invalid filename: {FILENAME}”)

    if errors:

    raise ValueError(“; “.join(errors))

    # =========================

    # MISP

    # =========================

    def find_existing_event():

    headers = {

    “Authorization”: MISP_API_KEY,

    “Accept”: “application/json”,

    “Content-Type”: “application/json”

    }

    payload = {

    “value”: f”{ALERT_NAME} – Automated Splunk Detection”,

    “searchall”: True

    }

    response = requests.post(

    f”{MISP_URL}/events/restSearch”,

    headers=headers,

    json=payload,

    verify=False,

    timeout=30

    )

    print(

    “MISP existing-event search:”,

    response.status_code

    )

    if response.status_code != 200:

    return None

    results = response.json().get(“response”, [])

    if not results:

    return None

    return results[0][“Event”][“id”]

    def create_misp_event():

    existing_id = find_existing_event()

    if existing_id:

    print(

    f”Existing event found, reusing ID {existing_id}”

    )

    add_misp_attribute(

    existing_id,

    “Other”,

    “text”,

    f”Re-detected at {datetime.datetime.now().isoformat()}”,

    “Additional automated detection occurrence”

    )

    return existing_id

    headers = {

    “Authorization”: MISP_API_KEY,

    “Accept”: “application/json”,

    “Content-Type”: “application/json”

    }

    payload = {

    “Event”: {

    “info”: (

    f”{ALERT_NAME} – Automated Splunk Detection”

    ),

    “date”: str(date.today()),

    “threat_level_id”: “2”,

    “analysis”: “1”,

    “distribution”: “0”

    }

    }

    response = requests.post(

    f”{MISP_URL}/events/add”,

    headers=headers,

    json=payload,

    verify=False,

    timeout=30

    )

    print(

    “MISP event:”,

    response.status_code,

    response.text

    )

    if response.status_code not in [200, 201]:

    return None

    return response.json()[“Event”][“id”]

    def add_misp_attribute(

    event_id,

    category,

    attr_type,

    value,

    comment

    ):

    headers = {

    “Authorization”: MISP_API_KEY,

    “Accept”: “application/json”,

    “Content-Type”: “application/json”

    }

    payload = {

    “Attribute”: {

    “event_id”: event_id,

    “category”: category,

    “type”: attr_type,

    “value”: value,

    “comment”: comment,

    “to_ids”: False,

    “distribution”: “0”

    }

    }

    response = requests.post(

    f”{MISP_URL}/attributes/add/{event_id}”,

    headers=headers,

    json=payload,

    verify=False,

    timeout=30

    )

    print(

    f”MISP attribute {value}:”,

    response.status_code,

    response.text

    )

    # =========================

    # THEHIVE

    # =========================

    def create_thehive_case():

    headers = {

    “Authorization”: f”Bearer {THEHIVE_API_KEY}”,

    “Content-Type”: “application/json”

    }

    payload = {

    “title”: (

    f”{ALERT_NAME} – Automated Splunk Alert”

    ),

    “description”: (

    f”Splunk detected activity associated with “

    f”{ALERT_NAME}. “

    f”Source {SOURCE_IP} communicated with “

    f”destination {DESTINATION_IP} on port {PORT}. “

    f”The detected URL was {DETECTED_URL}, and the “

    f”associated filename was {FILENAME}. “

    f”Indicators were sent to MISP, and this case “

    f”was created for analyst investigation.”

    ),

    “severity”: 2,

    “tlp”: 2,

    “pap”: 2,

    “tags”: [

    “splunk”,

    “misp”,

    “cti”,

    “automated-alert”,

    “threat-detection-lab”

    ]

    }

    response = requests.post(

    f”{THEHIVE_URL}/api/case”,

    headers=headers,

    json=payload,

    verify=THEHIVE_CA_CERT,

    timeout=30

    )

    print(

    “TheHive case:”,

    response.status_code,

    response.text

    )

    if response.status_code not in [200, 201]:

    return None

    return response.json()[“_id”]

    def add_thehive_observable(

    case_id,

    data_type,

    data,

    message

    ):

    headers = {

    “Authorization”: f”Bearer {THEHIVE_API_KEY}”,

    “Content-Type”: “application/json”

    }

    payload = {

    “dataType”: data_type,

    “data”: data,

    “message”: message,

    “tlp”: 2,

    “pap”: 2,

    “tags”: [

    “automated-from-splunk”

    ]

    }

    response = requests.post(

    f”{THEHIVE_URL}/api/case/{case_id}/artifact”,

    headers=headers,

    json=payload,

    verify=THEHIVE_CA_CERT,

    timeout=30

    )

    print(

    f”TheHive observable {data}:”,

    response.status_code,

    response.text

    )

    # =========================

    # MAIN WORKFLOW

    # =========================

    def main():

    global ALERT_NAME

    global SOURCE_IP

    global DESTINATION_IP

    global PORT

    global DETECTED_URL

    global FILENAME

    args = parse_arguments()

    ALERT_NAME = args.alert_name

    SOURCE_IP = args.source_ip

    DESTINATION_IP = args.destination_ip

    PORT = str(args.port)

    DETECTED_URL = args.url

    FILENAME = args.filename

    try:

    validate_inputs()

    except ValueError as error:

    log_error(

    f”Input validation failed: {error}”

    )

    return 2

    log_info(

    f”CTI workflow started: alert={ALERT_NAME}”

    )

    log_info(

    f”Indicators received: “

    f”source={SOURCE_IP}, “

    f”destination={DESTINATION_IP}, “

    f”port={PORT}, “

    f”url={DETECTED_URL}, “

    f”filename={FILENAME}”

    )

    if args.dry_run:

    log_info(

    “Dry-run validation completed; “

    “no MISP or TheHive API calls were made”

    )

    return 0

    # ————————-

    # MISP RESPONSE

    # ————————-

    event_id = create_misp_event()

    if event_id:

    add_misp_attribute(

    event_id,

    “Network activity”,

    “ip-src”,

    SOURCE_IP,

    “Source IP detected by Splunk”

    )

    add_misp_attribute(

    event_id,

    “Network activity”,

    “ip-dst”,

    DESTINATION_IP,

    “Destination IP detected by Splunk”

    )

    add_misp_attribute(

    event_id,

    “Network activity”,

    “url”,

    DETECTED_URL,

    f”URL associated with {ALERT_NAME}”

    )

    add_misp_attribute(

    event_id,

    “Payload delivery”,

    “filename”,

    FILENAME,

    f”Filename associated with {ALERT_NAME}”

    )

    add_misp_attribute(

    event_id,

    “Network activity”,

    “port”,

    PORT,

    “Destination port detected by Splunk”

    )

    # ————————-

    # THEHIVE RESPONSE

    # ————————-

    case_id = create_thehive_case()

    if case_id:

    add_thehive_observable(

    case_id,

    “ip”,

    SOURCE_IP,

    “Detected source IP”

    )

    add_thehive_observable(

    case_id,

    “ip”,

    DESTINATION_IP,

    “Detected destination IP”

    )

    add_thehive_observable(

    case_id,

    “url”,

    DETECTED_URL,

    f”URL associated with {ALERT_NAME}”

    )

    add_thehive_observable(

    case_id,

    “filename”,

    FILENAME,

    f”Filename associated with {ALERT_NAME}”

    )

    add_thehive_observable(

    case_id,

    “other”,

    f”TCP/{PORT}”,

    “Detected network port”

    )

    log_info(

    “CTI workflow completed successfully”

    )

    return 0

    if __name__ == “__main__”:

    raise SystemExit(main())

    5. Add Input Validation and Dry-Run Mode

    Before connecting the script to a live Splunk alert, validate the incoming values.

    The script should reject:

    • invalid IP addresses;
    • ports outside the valid range;
    • malformed URLs;
    • blank alert names;
    • unsafe filenames.

    A –dry-run option should allow the values to be tested without contacting MISP or TheHive. This lets you test the interface on its own before contacting MISP or TheHive.

    Example safe test:

    sudo /usr/bin/python3 /opt/splunk/bin/scripts/cti_response.py \

    –alert-name “ Suspicious Software Update Retrieval Validation” \

    –source-ip “192.168.95.134” \

    –destination-ip “192.168.95.135” \

    –port “8080” \

    –url “http://192.168.95.135:8080/test-file.txt” \

    –filename “test-file.txt” \

    –dry-run

    A successful dry run should validate the supplied fields and exit without creating any MISP event or TheHive case.

    Figure 18. Sanitized CTI response script successfully validating dynamic detection fields in dry-run mode without contacting MISP or TheHive.

    6. Build the Splunk Bridge

    Now create the bridge script:

    sudo nano /opt/splunk/bin/scripts/cti_splunk_bridge.py

    The bridge exists because Splunk scripted alerts do not automatically pass arbitrary SPL fields as normal command-line arguments. Instead, Splunk supplies the alert results through a compressed CSV results file.

    The bridge workflow is:

    results.csv.gz

    ↓

    Decompress CSV

    ↓

    Read First Result

    ↓

    Validate Required Fields

    ↓

    Launch cti_response.py

    ↓

    Pass Dynamic Arguments

    The required fields are:

    alert_name

    source_ip

    destination_ip

    destination_port

    url

    filename

    bridge-script

    #!/usr/bin/python3

    import csv

    import gzip

    import os

    import subprocess

    import sys

    from datetime import datetime

    RESULT_SCRIPT = “/opt/splunk/bin/scripts/cti_response.py”

    LOG_FILE = “/opt/splunk/var/log/splunk/cti_bridge.log”

    def write_log(level, message):

    with open(LOG_FILE, “a”, encoding=”utf-8″) as log:

    log.write(f”{datetime.now().isoformat()} | {level} | {message}\n”)

    def main():

    results_file = os.environ.get(“SPLUNK_ARG_8”)

    if not results_file:

    for argument in reversed(sys.argv[1:]):

    if argument.endswith(“results.csv.gz”):

    results_file = argument

    break

    if not results_file:

    write_log(“ERROR”, “No Splunk results file was received”)

    return 2

    try:

    with gzip.open(

    results_file,

    “rt”,

    encoding=”utf-8-sig”,

    newline=””

    ) as handle:

    reader = csv.DictReader(handle)

    row = next(reader, None)

    except Exception as error:

    write_log(

    “ERROR”,

    f”Could not read results file: {type(error).__name__}: {error}”

    )

    return 1

    if not row:

    write_log(“ERROR”, “Splunk results file contained no rows”)

    return 2

    required_fields = [

    “alert_name”,

    “source_ip”,

    “destination_ip”,

    “destination_port”,

    “url”,

    “filename”,

    ]

    missing = [

    field for field in required_fields

    if not row.get(field, “”).strip()

    ]

    if missing:

    write_log(

    “ERROR”,

    “Missing required fields: ” + “, “.join(missing)

    )

    return 2

    command = [

    “/usr/bin/python3”,

    RESULT_SCRIPT,

    “–alert-name”, row[“alert_name”],

    “–source-ip”, row[“source_ip”],

    “–destination-ip”, row[“destination_ip”],

    “–port”, row[“destination_port”],

    “–url”, row[“url”],

    “–filename”, row[“filename”],

    ]

    write_log(

    “INFO”,

    (

    f”Launching CTI response: alert={row[‘alert_name’]}, “

    f”source={row[‘source_ip’]}, “

    f”destination={row[‘destination_ip’]}, “

    f”port={row[‘destination_port’]}, “

    f”filename={row[‘filename’]}”

    )

    )

    try:

    result = subprocess.run(

    command,

    capture_output=True,

    text=True,

    timeout=120,

    check=False

    )

    except Exception as error:

    write_log(

    “ERROR”,

    f”CTI response launch failed: {type(error).__name__}: {error}”

    )

    return 1

    write_log(

    “INFO” if result.returncode == 0 else “ERROR”,

    f”CTI response completed with exit code {result.returncode}”

    )

    if result.stderr.strip():

    write_log(“ERROR”, result.stderr.strip()[-500:])

    return result.returncode

    if __name__ == “__main__”:

    raise SystemExit(main())

    7. Understand results.csv.gz

    This part is important and should not be skipped.

    Splunk’s scripted-alert mechanism passes the search results as a compressed CSV file called:

    results.csv.gz

    During testing, the initial result contained mainly metadata and _raw. The SPL search therefore had to explicitly extract and return the fields required by the bridge.

    The automation depends on the result containing:

    alert_name

    source_ip

    destination_ip

    destination_port

    url

    filename

    8. Prepare the SPL Output

    Your detection search should finish by normalizing the values required by the automation.

    The important pattern is:

    | rex …

    | eval alert_name=” Suspicious Software Update Retrieval Validation”

    | eval url=…

    | eval filename=…

    | table _time host action protocol source_ip destination_ip source_port destination_port alert_name url filename

    | head 1

    Use rex, eval, and table so the named fields appear inside results.csv.gz.

    9. Connect the Scheduled Splunk Alert

    Save the detection search as a scheduled alert.

    For this lab, a five-minute schedule is suitable:

    Detection Search

    ↓

    Every 5 Minutes

    ↓

    If Result Exists

    ↓

    Run cti_splunk_bridge.py

    A five-minute schedule works well for this lab.

    The bridge then receives the compressed Splunk result and starts the response workflow automatically.

    Figure 19. Scheduled Splunk detection configured to run every five minutes and trigger when matching results are returned.

    Figure 20. Splunk alert action configured to launch the CTI bridge script when the detection triggers.

    Note: Splunk marks the built-in Run a script alert action as deprecated. This lab uses it to demonstrate the scripted-alert workflow.

    10. Test the Response Script Safely

    Before testing the complete alert chain, run the response script manually using –dry-run.

    Then verify the exit code:

    echo $?

    Expected : 0

    for a valid dry-run.

    Invalid inputs should return a non-zero exit code rather than continuing to the APIs. Exit codes make it possible to test invalid inputs without continuing to the downstream APIs.

    11. Run the Automated Workflow

    Once the individual components are working, generate a fresh controlled detection.

    The resulting path should be:

    Windows Endpoint

    ↓

    Universal Forwarder

    ↓

    Splunk Detection

    ↓

    results.csv.gz

    ↓

    Bridge Script

    ↓

    Response Script

    ↙ ↘

    MISP TheHive

    Following this path, endpoint activity reaches Splunk, the detection is normalized, the bridge and response script execute, and the results are sent automatically to MISP and TheHive.

    12. Verify the Automation Logs

    The bridge and response script should each maintain a dedicated log.

    The bridge and response script use these dedicated logs:

    /opt/splunk/var/log/splunk/cti_bridge.log

    /opt/splunk/var/log/splunk/cti_response.log

    Check them with:

    sudo cat /opt/splunk/var/log/splunk/cti_bridge.log

    and

    sudo cat /opt/splunk/var/log/splunk/cti_response.log

    Figure 21. Bridge and response logs confirming successful automated processing of the Splunk detection and completion of the CTI response workflow.

    The logs should confirm that:

    Splunk result received ✓

    Required fields validated ✓

    Response script launched ✓

    MISP operation completed ✓

    TheHive operation completed ✓

    Exit code 0

    Dedicated bridge and CTI logs help with troubleshooting and provide useful audit evidence.

    13. Automation Checkpoint

    Before moving to TheHive Investigation Workflow, verify:

    Splunk produces the required normalized fields ✓

    results.csv.gz reaches the bridge ✓

    Bridge validates the required fields ✓

    Response script receives dynamic values ✓

    Dry-run validation succeeds ✓

    MISP API operation succeeds ✓

    TheHive API operation succeeds ✓

    API keys remain outside source code ✓

    Bridge and response logs record successful execution ✓

    At this point, the detection is no longer just a Splunk search. It has become an automated response workflow capable of moving structured detection data into the intelligence and investigation layers.

    Complete Project Guide

    Prefer the entire lab in one place?

    Get the complete Threat-Intelligence-Driven Detection Lab as one structured PDF bringing all four parts, implementation steps, screenshots, architecture, validation, and technical notes together.

    143 Pages 4 Parts Version 1.0 PDF
    PDF
    View Complete Guide → Free Chronos & Code account required for the PDF download.

    If this piece gave you something to think about, you can support my writing here ☕

    3 comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *