courses

Lab — Week 7: Detection and Monitoring
Due: 14 August 2026
Submission: GitLab repo secdevops-s26-<CECS>

Overview

Build the detection layer on top of the preventive controls: knowing when an attack is happening, what it’s doing, and how to respond.

Prerequisites


Part 1: Wazuh Rules, Tuning, and Active Response

Built-in Wazuh rules cover standard Linux events well but know nothing about DVWA’s specific log format or your threat model’s priority threats. This part writes two custom detection rules, tests them by triggering the attack from Kali, and configures an active response that automatically blocks a brute-force source.

Custom Rule: DVWA Login Brute Force

  1. On the Wazuh manager (10.10.10.30), create /var/ossec/etc/rules/local_rules.xml if it does not exist.

  2. Write a rule pair:
    • A parent rule (level 3, id in range 100001–100099) that matches any nginx access log line referencing /dvwa/login.php
    • A child rule (level 10) using frequency and timeframe that fires when the parent rule matches 5 or more times from the same source IP within 60 seconds

    Keep those levels in that order. The parent matches a single login request, which is normal traffic; the composite is the attack. Giving the parent a higher level than the child inverts the severity of your own detection and buries the real alert under one alert per request. Use <same_source_ip /> on the child so five requests from five different addresses do not add up to a brute force.

    If your rule matches in wazuh-logtest but produces no alerts in production, suspect a built-in rule matching first: Wazuh’s rule 31108 (Ignored URLs) is level 0 and swallows web-access events before custom children see them. <if_sid>31100</if_sid> parents your rule under the access-log group, and you can override 31108 with overwrite="yes" and a narrower pattern if it is eating your traffic.

  3. Test the rule syntax without restarting:
    /var/ossec/bin/wazuh-logtest
    

    Paste a sample nginx log line for /dvwa/login.php and confirm the parent rule matches.

  4. Restart the manager: sudo systemctl restart wazuh-manager

  5. From Kali, trigger the brute-force rule (use the host port your nginx container publishes — 8080 if you followed Week 3):
    for i in {1..7}; do
      curl -s -o /dev/null -X POST http://10.10.10.20:8080/dvwa/login.php \
        -d "username=admin&password=wrong&Login=Login"
    done
    
  6. Confirm the level-10 alert fires in the Wazuh dashboard. Take a screenshot showing the rule ID, level, and source IP.

Custom Rule: Unexpected Container Spawn

  1. Write a second rule (level 12, id in range 100100–100199) in the same local_rules.xml that fires when a docker run or docker exec command appears in the auditd log outside of the docker compose process tree.

    Use <match> or <regex> targeting the execve syscall with docker run in the arguments, and <if_matched_group> or <different_user> to distinguish ad-hoc runs from compose-managed ones if possible.

  2. Test from ubuntu-server: docker run --rm hello-world

  3. Confirm the alert fires in the dashboard. Take a screenshot.

Active Response

  1. On the Wazuh manager, configure firewall-drop to trigger when the DVWA brute-force rule (your level-10 rule) fires:
    <active-response>
      <command>firewall-drop</command>
      <location>local</location>
      <rules_id>YOUR_RULE_ID</rules_id>
      <timeout>3600</timeout>
    </active-response>
    

    Add this stanza to /var/ossec/etc/ossec.conf on the manager, inside <ossec_config>.

  2. Restart the manager: sudo systemctl restart wazuh-manager

  3. From Kali, trigger the brute-force rule again (5+ POST requests in 60 seconds).

  4. On ubuntu-server, confirm the active response fired:
    sudo tail -20 /var/ossec/logs/active-responses.log
    sudo nft list ruleset | grep 10.10.10.10
    
  5. Take a screenshot showing the blocked IP in nftables.

  6. Unblock Kali: sudo nft delete element inet ... @blocked_ips { 10.10.10.10 } or wait for the 3600-second timeout.

Deliverables

Create lab13/ in your repo containing:


Part 2: Suricata Integration and File Integrity Monitoring

Network and host detection are complementary layers. Suricata sees packet contents; auditd sees syscalls. Neither sees everything the other does. This part adds network-layer detection by integrating Suricata EVE logs with Wazuh, writes a custom Suricata rule targeting a known attack pattern, and completes the FIM configuration to detect unauthorized changes to critical files — closing the final gaps in your detection stack.

Part 1 must be complete — Wazuh custom rules working.

Suricata Setup

  1. Enable and start Suricata in IDS mode on ubuntu-server, monitoring the interface that carries lab traffic. This is not the “primary” interface: the default route goes out the management NIC on 172.20.100.0/24, and Kali never appears there. Suricata must watch the interface holding the 10.10.10.20 address, or it will run perfectly and see none of your attacks. Find it, do not guess it:
    ip -br addr show | grep 10\.10\.10\.        # names the interface, e.g. ens19 or eth0
    IFACE=$(ip -o -4 addr show | awk '$4 ~ /^10\.10\.10\./ {print $2}')
    sudo sed -i "0,/- interface: .*/s//- interface: $IFACE/" /etc/suricata/suricata.yaml
    sudo suricata -T -c /etc/suricata/suricata.yaml   # validate before starting
    sudo systemctl enable --now suricata
    sudo suricata --build-info | grep "AF_PACKET"     # confirm AF_PACKET support
    

    If systemctl enable --now suricata reports that the control process exited with an error, the config failed validation — suricata -T above names the offending line, and journalctl -xeu suricata.service shows the rest.

  2. Confirm Suricata is writing EVE JSON:
    sudo tail -5 /var/log/suricata/eve.json | jq .
    

    eve.json is JSON Lines — one complete JSON object per line, not one document. python3 -m json.tool parses a single document and fails on the second line with Extra data: line 2 column 1, which looks like Suricata produced garbage when in fact it produced several valid records. jq reads a stream and handles it correctly.

Wazuh–Suricata Integration

  1. On ubuntu-server, add the following to /var/ossec/etc/ossec.conf inside <ossec_config>:
    <localfile>
      <log_format>json</log_format>
      <location>/var/log/suricata/eve.json</location>
    </localfile>
    
  2. Restart the Wazuh agent: sudo systemctl restart wazuh-agent

  3. From Kali, generate traffic that will produce Suricata events:
    nmap -sV 10.10.10.20
    
  4. Confirm Suricata alert events appear in the Wazuh dashboard filtered by rule.groups: suricata. Take a screenshot.

Custom Suricata Rule

  1. Write a custom Suricata rule in /etc/suricata/rules/local.rules that detects nmap SYN scans:
    alert tcp any any -> $HOME_NET any (
      msg:"Nmap SYN scan detected";
      flags:S,12;
      threshold:type threshold, track by_src, count 10, seconds 2;
      sid:9000001; rev:1;
    )
    
  2. Reference the rule file in /etc/suricata/suricata.yaml under rule-files:.

  3. Reload Suricata rules without restarting: sudo suricatasc -c ruleset-reload-rules

    The command was reload-rules in older releases; current Suricata documents it as ruleset-reload-rules. If neither is accepted, sudo suricatasc -c command-list prints what your build supports.

  4. Trigger the rule from Kali: nmap -sS 10.10.10.20

  5. Confirm the custom rule fires in both eve.json and the Wazuh dashboard. Take a screenshot of the Wazuh alert showing your rule’s msg field.

File Integrity Monitoring

  1. On ubuntu-server, configure Wazuh FIM in /var/ossec/etc/ossec.conf to monitor:
    • /etc (all files, check_all="yes")
    • The directory containing your docker-compose.yml, nginx.conf, and seccomp-dvwa.json
    • /etc/audit/rules.d/ — the auditd rules from Week 6, since disabling logging is a standard first move after a compromise

    Set <frequency>300</frequency> (5-minute check interval).

    Monitor the host-side Compose project directory rather than /etc/nginx/. Your nginx runs in a container, so there is no /etc/nginx/nginx.conf on ubuntu-server to watch — the file that matters is the nginx.conf in your project directory, which is the one that gets copied into the image at build time. Files inside a running container are not visible to host FIM at all.

  2. Restart the Wazuh agent: sudo systemctl restart wazuh-agent

  3. Force an immediate FIM baseline scan. Run this on the manager (10.10.10.30) — agent_control is a manager-side tool — using the agent ID from agent_control -l:
    sudo /var/ossec/bin/agent_control -r -u <agent-id>
    
  4. Simulate post-compromise tampering from ubuntu-server itself, against a file FIM is actually watching:
    echo "# tampered" | sudo tee -a ~/compose/nginx.conf     # your project path
    
  5. Wait up to 5 minutes, or force a rescan. Confirm the FIM alert fires in the Wazuh dashboard. Take a screenshot showing the modified file path and the before/after hash.

    A “file added” alert instead of “file modified” means the path was not in the baseline — you appended to a file that did not exist, so there is no previous hash to compare against. That is a sign you are watching the wrong path, not a successful test.

  6. Restore the file: sudo sed -i '/# tampered/d' ~/compose/nginx.conf

Threat Model — Final

  1. In lab02/threat-model.md, mark all remaining OPEN threats with either MITIGATED (and the control), DETECTED (and the Wazuh/Suricata rule), or ACCEPTED RISK (with rationale). By the end of this lab, every original threat entry must have a non-OPEN status.

Deliverables

Create lab14/ in your repo containing:


Submission

Commit all files for both parts — lab13/ and lab14/ — and push to your GitLab repo. The lab is considered submitted when the commit appears in GitLab before the due date.