Know the attack, stop the attack

Create the material to investigate

You stage reconnaissance, SSH guessing and a successful login inside one VM, then rebuild where it came from, what it tried and whether it worked, using audit-logs, AccessScope, PathScope, PersonalSOC and the FIM records.

  • Level: Practice
  • About 60 min
  • 2026-11-12

#Create the material to investigate

After an alert arrives, or after you sense that something is off, you collect facts from the logs. This module looks for answers to three questions.

  1. Where did it come from: the source address and the account used
  2. What was tried: probing, password guessing, web attack attempts
  3. Did it succeed: a successful login, and what happened after it

Every tool used here only reads. None of them rewrites logs or blocks traffic.

ToolWhat it looks at
roamswitch audit-logsCounts recent journal entries and shows patterns that differ from the usual
accessscope scanDiagnoses web, SSH and auth logs, with the evidence lines
pathscopeLooks for paths from an account to root
personalsoc scanBrings the logs, RoamSwitch's records and the two results above together

How to act in the first hour after containment is covered in A containment fired: the first hour. This module stays with reading logs.

Trigger the practice events

This script creates a user, adds addresses, and installs nginx and a timer (this changes the system). Try it on a throwaway VM. The steps to undo it are at the end of the "Check everything together" chapter.

The sources are addresses reserved for documentation (198.51.100.7, 203.0.113.50), added to this machine's lo. All traffic stays inside the machine. Nothing is sent out.

sudo apt install -y nginx sshpass
cat > /tmp/make-events.sh <<'SH'
#!/bin/sh
# Create practice events inside this machine only (nothing is sent out)
ip addr add 198.51.100.7/32 dev lo     # an ordinary visitor (documentation address)
ip addr add 203.0.113.50/32 dev lo     # probes the web, then tries SSH (documentation address)
useradd -m -s /bin/bash demo
echo 'demo:Demo-Pass-2026' | chpasswd  # a dummy password for practice

# 1) Ordinary browsing
for p in / /index.html /; do
  curl -s -o /dev/null --interface 198.51.100.7 -A 'Mozilla/5.0 (X11; Linux) Firefox/128.0' "http://198.51.100.7$p"
done
# 2) Web probing (a tool User-Agent, secret files, SQL injection, path traversal)
for p in /.env /.git/config /wp-login.php /phpmyadmin/ "/index.php?id=1%27%20OR%20%271%27=%271" \
         "/cgi-bin/../../../../etc/passwd" /backup.zip /.aws/credentials; do
  curl -s -o /dev/null --path-as-is --interface 203.0.113.50 -A 'sqlmap/1.8 (https://sqlmap.org)' "http://203.0.113.50$p"
done
# 3) SSH password guessing (6 failures), then a success
O="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o PubkeyAuthentication=no"
for u in oracle test demo demo demo demo; do
  sshpass -p 'wrong-pass' ssh $O -b 203.0.113.50 $u@203.0.113.50 true 2>/dev/null
done
# 4) After logging in, try sudo (demo cannot use sudo)
sshpass -p 'Demo-Pass-2026' ssh $O -b 203.0.113.50 demo@203.0.113.50 'id; echo bad | sudo -S -k id' 2>&1
# 5) An admin mistake: a script run by a root timer is writable by anyone
printf '#!/bin/sh\ntar czf /var/backups/www.tgz /var/www\n' > /opt/nightly-backup.sh
chmod 777 /opt/nightly-backup.sh
printf '[Service]\nType=oneshot\nExecStart=/opt/nightly-backup.sh\n' > /etc/systemd/system/nightly-backup.service
printf '[Timer]\nOnCalendar=daily\n[Install]\nWantedBy=timers.target\n' > /etc/systemd/system/nightly-backup.timer
systemctl daemon-reload && systemctl enable --now nightly-backup.timer 2>/dev/null
SH
sudo sh /tmp/make-events.sh
Output
uid=1002(demo) gid=1002(demo) groups=1002(demo)
[sudo] password for demo: Sorry, try again.
[sudo] password for demo: 
sudo: no password was provided
sudo: 1 incorrect password attempt

The id line shows that the password login worked after six failures. The sudo that followed failed with a wrong password. From here on, pretend you do not know this, and rebuild the story from the logs alone.

#Summarise the journal

Security log audit counts recent journal entries by kind. It also shows patterns that are new and patterns that suddenly grew. Use it first, to get the overall picture.

Summarise, then run it again
sudo roamswitch audit-logs 1
Excerpt
  • sudo auth failure lines:   2 lines
  • SSH failure lines:         14 lines
  • Firewall drop lines:       0 lines
  • AppArmor/LSM denial lines: 11 lines
  • Malware detection lines:   0 lines
  • USB connection lines:      0 lines
    These are numbers of log lines, not of attempts. One failure can be two or more lines (an SSH login to a user that does not exist logs "Invalid user" and "Failed password"). SSH lines are failed passwords, users that do not exist, and repeated disconnects from the same source. Successful logins are not counted.

  ⚠ Only the latest 500 log lines were examined. Earlier lines are not included.

  [Key event timeline]
   [Nov 12 02:53:24] [Authentication / sudo] sudo authentication failure / privilege escalation attempt - Nov 12 02:53:24 ubuntu sudo[1935]: pam_unix(sudo:auth): conversation failed
   [Nov 12 02:53:21] [Authentication / sudo] sudo authentication failure / privilege escalation attempt - Nov 12 02:53:21 ubuntu sudo[1935]: pam_unix(sudo:auth): authentication failure; logname=demo uid=1002 euid=0 tty= ruser=demo rhost=  user=demo
   [Nov 12 02:53:21] [SSH brute-force attempts] Unauthorized SSH login attempt - Nov 12 02:53:21 ubuntu sshd[1773]: Connection closed by authenticating user demo 203.0.113.50 port 50957 [preauth]
   [Nov 12 02:53:21] [SSH brute-force attempts] Unauthorized SSH login attempt - Nov 12 02:53:21 ubuntu sshd[1773]: Failed password for demo from 203.0.113.50 port 50957 ssh2

  [Template anomalies (new patterns / frequency spikes)]
   [spike z=6.1]  <HOST> sudo[<NUM>]: pam_unix(sudo:session): session opened for user root(uid=<NUM>) by (uid=<NUM>) — 6 (example: Nov 12 02:52:40 ubuntu sudo[877]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=1000)) [learning: 1/3]
   [spike z=6.1]  <HOST> roamswitch-server-daemon[<NUM>]: <NUM>-<NUM>-<NUM>T<NUM>:<NUM>:<NUM>.<NUM>Z  WARN roamswitch_server_daemon: 🚨 [FIM ALERT] Critical path tampering detected! violations=<NUM> — 6 (example: Nov 12 02:53:02 ubuntu roamswitch-server-daemon[885]: 2026-11-12T02:53:02.586895Z  WARN roamswitch_server_daemon: 🚨 [FIM ALERT] Critical path tampering detected! violations=1) [learning: 1/3]
   [spike z=3.6]  <HOST> sshd[<NUM>]: Failed password for demo from <IP> port <NUM> ssh<NUM> — 4 (example: Nov 12 02:53:13 ubuntu sshd[1707]: Failed password for demo from 203.0.113.50 port 56717 ssh2) [learning: 1/3]
   (7 more spikes omitted)
   At least one of the above is occurring more often than usual. It's usually temporary load or a batch of work running at once, but if it coincides with something you didn't do yourself, take a closer look.
   Hint: package-manager activity (apt/dnf/zypper/pacman) was detected during this window. The anomalies above may just be routine upgrade noise.

Read four things.

  • The counts are log lines, not attempts. As the note in the output says, six attempts became "14 lines". One failure leaves several lines, such as Failed password and Connection closed
  • A successful login does not appear in this summary. It counts records such as failures and denials. The success is found in the next chapter
  • Template anomalies: numbers and addresses in each line are folded into <NUM> and <IP>, and the amount per pattern is compared. [learning: 1/3] means the usual amount of that pattern is still being learned. In this run ten spikes were listed, among them RoamSwitch's FIM alerts (changes to /etc/shadow and others when the user was created), SSH Failed password lines and the lab's own sudo lines. An anomaly is a lead, not a verdict on an attack
  • Hint: when apt was used in this window, a line adds that the increase may come from an update. In this lab, that is the nginx install

Times are in UTC (02:53 UTC is 11:53 in Japan).

Run the same command again right away.

sudo roamswitch audit-logs 1 | grep -c 'spike'
Output
11

The spikes were still there on the second run (the count is the lines with spike, the heading included). A manual audit-logs does not move the pattern record (the baseline) that the daemon uses, even with sudo (checked in the code). However often you run it, the range the daemon's scheduled audit looks at stays the same.

The daemon runs the same audit on a schedule. Per the notes in the config file, log_audit_enabled defaults to true, and the interval log_audit_interval_secs defaults to 1800 seconds (minimum 300). After changing them, apply with sudo roamswitch server restart. In the code, the daemon's first audit runs one interval after it starts, and that run only learns and does not notify. From the second run on, it compares only the lines added since the previous audit, and keeps the security-related patterns in the notification history. Automatic notifications for new patterns are held back for 7 days after the pattern record is first created.

#Read web and SSH logs with evidence

Access log diagnosis (AccessScope) is AccessScope, a bundled tool. It finds logs such as nginx and auth.log by itself, and shows attack attempts with the evidence lines. When the same source shows up in different logs, it links them into one sequence.

Diagnose the logs and read the evidence

/var/log/auth.log is readable only by root or the adm group, so use sudo. --since 1h limits the period. Without it, older events left in the logs are mixed in.

sudo accessscope scan --auto --since 1h --lang en
Excerpt
## Check first (possible successes)

| # | Severity | Confidence | Finding | Count | Main sources |
|---|---|---|---|---|---|
| 1 | High | Likely | SSH login succeeded after failures (203.0.113.50) | 7 | 203.0.113.50 |
| 2 | High | Likely | The same address in several logs (203.0.113.50) ※1 | 4 | 203.0.113.50 |

## Findings

| # | Severity | Confidence | Finding | Count | Main sources |
|---|---|---|---|---|---|
| 3 | Medium | Likely | Access to known vulnerability exploit paths | 1 (1 IP) | 203.0.113.50(1) |
| 5 | Low | Confirmed | Attack / scanning tool User-Agents | 7 (1 IP) | 203.0.113.50(7) |
| 6 | Low | Likely | A run of probes for secrets and admin tools (whatever the answer) | 6 (1 IP) | 203.0.113.50(6) |
| 7 | Low | Likely | Suspected SSH brute force | 6 (1 IP) | 203.0.113.50(6) |
| 8 | Low | Likely | Probing for secrets, config files and admin pages | 5 (1 IP) | 203.0.113.50(5) |
| 9 | Low | Likely | SQL injection attempts | 1 (1 IP) | 203.0.113.50(1) |
| 10 | Low | Likely | Path traversal (file read) attempts | 1 (1 IP) | 203.0.113.50(1) |

The success that the previous chapter could not see is at the top. Read the table like this.

  • Check first: findings where an attack may have succeeded. Start here
  • Severity: how much damage it means if it is real
  • Confidence: how likely it is a real attack. Confirmed means the log shows it, Likely means probably real, Hypothesis means only a suspicion
  • The ordinary visitor (198.51.100.7) appears in no row

The table alone does not say why. --report writes out the evidence lines.

sudo accessscope scan --auto --since 1h --lang en --report /tmp/as-report.md
sudo cat /tmp/as-report.md
Excerpt (/tmp/as-report.md)
- SSH authentication log: 15 events (4 failed, 2 for unknown users, 1 accepted), 1 IP

### 1. [High / Likely] SSH login succeeded after failures (203.0.113.50)

Within the 60 minutes before the success, 203.0.113.50 failed 6 times (4 of them against demo, 3 s apart on average) and then authenticated as demo (password). The brute force may have succeeded, or a legitimate user may have mistyped.

auth.log:72: 2026-11-12T02:53:02.891670+00:00 ubuntu sshd[1619]: Invalid user oracle from 203.0.113.50 port 45261
auth.log:94: 2026-11-12T02:53:21.500622+00:00 ubuntu sshd[1801]: Accepted password for demo from 203.0.113.50 port 42371 ssh2

### 2. [High / Likely] The same address in several logs (203.0.113.50)

203.0.113.50 appeared in 2 kinds of logs (Web, SSH) in this order: web: reconnaissance → web: exploit attempt → ssh: repeated authentication failures → ssh: successful login after failures, and a successful login is recorded among them. The only link is the identical address. The logs do not show whether the successful login was the operator's own or an attacker's. Ask the owner of the account whether they recognise it (hypothesis). The address in the web log is the recorded connection source as is (X-Forwarded-For was not used). Behind a reverse proxy or CDN it may be the proxy's address (you can choose otherwise with --trust-forwarded-for).

### 3. [Medium / Likely] Access to known vulnerability exploit paths

1 request from 1 IP. Main sources: 203.0.113.50(1). 2xx responses that differ from normal pages: 0; server errors (5xx): 0. Matched CVEs: CVE-2021-41773/CVE-2021-42013 (Apache HTTP Server 2.4.49 / 2.4.50) x1 (from the CVE rule pack as of 2026-10-19; this only means the request shape matched, not that the attack succeeded).

access.log:9: 203.0.113.50 - - [12/Nov/2026:02:53:02 +0000] "GET /cgi-bin/../../../../etc/passwd HTTP/1.1" 400 166 "-" "-"

With the report, you can answer the three questions from the start.

  • Where from: 203.0.113.50. The web probing and the SSH guessing came from the same address
  • What: probing for .env and the like, SQL injection and path traversal attempts, six password failures
  • Did it succeed: on the web, every response was 4xx. Over SSH, the login as demo succeeded

The evidence lines are the log content as it is. The sender can write any User-Agent or path. A client that calls itself sqlmap is not necessarily sqlmap.

Whose login was the success?

As the report says, the logs alone cannot tell a mistyping owner from a guessing attacker. Ask the owner of the account.

  • The owner recognises it: change the password and allow key authentication only
  • The owner does not recognise it, or you cannot ask: treat the account as taken over. Lock it first, then find out what it could do after logging in (two chapters ahead)

When in doubt, choose to lock. A locked account can be unlocked later.

#Narrow the findings and hand them over

Run it every day and the same findings line up every time. Narrowing down what you review (AccessScope) can show only findings that were not there last time, and can set aside known sources, such as your own scanner, with a reason.

You then hand the result to whoever blocks traffic, or to other tools. Exporting results (AccessScope) writes a blocklist and a list of findings. It only writes them. It blocks nothing and applies nothing.

Show only new findings, set some aside, and export

First record the current state. Then send probes from another address (203.0.113.77), playing your own vulnerability scan.

sudo accessscope scan --auto --since 1h --since-last --lang en > /dev/null
sudo ip addr add 203.0.113.77/32 dev lo
for p in /.env /.git/config /wp-config.php.bak /.aws/credentials /actuator/env /phpmyadmin/ /server-status; do
  curl -s -o /dev/null --interface 203.0.113.77 -A 'Mozilla/5.0 zgrab/0.x' "http://203.0.113.77$p"
done
sudo accessscope scan --auto --since 1h --since-last --new-only --lang en
Excerpt
Record used to compare with the previous run: /root/.config/accessscope/state/cli-97049aabbfbca085.json
| Since last run | 1 new / 10 continuing / 0 gone |
| Note | Showing new findings only (10 continuing findings omitted) |

| # | Severity | Confidence | IP | Stages | Requests |
|---|---|---|---|---|---|
| 1 | Low | Hypothesis | 203.0.113.77 | reconnaissance → exploitation | 7 |

Only one finding was new. The comparison key is the pair of "kind of finding" and "its subject". A finding that gathers all sources into one row, such as "Probing for secrets, config files and admin pages", counts as continuing even when a new source joins. Only per-source rows, such as this chain, become new.

Next, write a rule that sets aside your own scan. reason is required and appears in every report. Give it an expiry too.

cat > /tmp/suppress.toml <<'T'
[[rule]]
reason  = "Our own vulnerability scan (practice)"
ip      = "203.0.113.77"
expires = 2026-11-30
T
sudo accessscope suppress check --suppress /tmp/suppress.toml --lang en
sudo accessscope scan --auto --since 1h --suppress /tmp/suppress.toml --lang en
Excerpt
| /tmp/suppress.toml | OK | 1 | 0 | 0 |

Checked 1 file. Errors 0, rules expiring soon 0, expired rules 0.

| Suppression rules loaded | /tmp/suppress.toml (1 active) |
| Suppressed findings | 5 (each with the rule's reason). Active rules: 1; requests set aside and analysed separately: 7 |
| Note | Rule "Our own vulnerability scan (practice)" (/tmp/suppress.toml): 5 suppressed, 0 kept, 7 matching requests, expires 2026-11-30 |
| Note | The overall risk and the finding counts do not include suppressed findings. List them with --show-suppressed. |

Suppressed findings are kept, not thrown away. Their count and reason appear in the report, and --show-suppressed lists them. suppress check checks the syntax and lists rules that expire within 7 days.

Finally, export the result in forms that others can use.

sudo accessscope scan --auto --since 1h --suppress /tmp/suppress.toml --lang en --export-blocklist nftables 2>/dev/null | grep -E 'Entries|left out'
sudo accessscope scan --auto --since 1h --suppress /tmp/suppress.toml --lang en --export-blocklist nftables --blocklist-include-private 2>/dev/null | tail -6
sudo accessscope scan --auto --since 1h --suppress /tmp/suppress.toml --lang en --format csv > /tmp/findings.csv
python3 -c 'import csv
for r in csv.DictReader(open("/tmp/findings.csv")): print(r["state"], r["severity"], r["kind"], r["ips"], r["suppressed_by"], sep=",")'
Excerpt
# IPs left out: official crawlers 0 / suppression rules 0 / private or reserved ranges 1 / evidence below the criteria 0 / too few hits 0 / over the limit 0
# Entries: 0
set accessscope_blocklist_v4 {
    type ipv4_addr
    elements = {
        203.0.113.50
    }
}
active,high,ssh_bf_ok,203.0.113.50,
active,high,actor_chain,203.0.113.50,
active,medium,web_cve_path,203.0.113.50,
active,low,web_chain,203.0.113.50,
active,low,web_scan,203.0.113.50,
active,low,web_probe_walk,203.0.113.50,
active,low,ssh_bf,203.0.113.50,
active,low,web_secret_probe,203.0.113.50,
active,low,web_sqli,203.0.113.50,
active,low,web_traversal,203.0.113.50,
suppressed,medium,web_cve_path,203.0.113.77,Our own vulnerability scan (practice)
suppressed,low,web_scan,203.0.113.77,Our own vulnerability scan (practice)
suppressed,low,web_chain,203.0.113.77,Our own vulnerability scan (practice)
suppressed,low,web_secret_probe,203.0.113.77,Our own vulnerability scan (practice)
suppressed,low,web_probe_walk,203.0.113.77,Our own vulnerability scan (practice)
  • Blocklist: by default, private, loopback, documentation and similar ranges are left out. The practice addresses are documentation addresses, so the first list had 0 entries. --blocklist-include-private lets them in. A real attack from outside does not need this option
  • Criteria: by default, severity Medium or higher, confidence Likely or higher, and at least 3 hits per IP. IPs covered by a suppression rule are not included either
  • CSV: state is active or suppressed. Suppressed rows carry the rule's reason. According to --help, json and sarif are also available
Don't
  • Apply an exported blocklist as it is, without checking it. Behind NAT or a proxy, many people share one address
  • Delete or rotate logs before the investigation is done. A period that is gone is invisible to every tool
  • Add suppression rules without a reason or an expiry. Later, nobody can tell what was set aside

#How far the taken-over account can reach

You decided to treat demo as taken over. Lock the account first. Then use Where to fix and fix proposals (PathScope) to check whether demo has a way to reach root.

Lock the account, find paths to root, and fix them

usermod -L disables password login (this changes the system). To undo it, run sudo usermod -U demo. loginctl terminate-user ends the account's sessions. When no session is left, it prints "Could not terminate user: … is not logged in or lingering" and exits with code 1.

sudo usermod -L demo
sudo loginctl terminate-user demo
sudo passwd -S demo
sudo pathscope --start=demo --lang=en --json-out=/tmp/ps-before.json
Excerpt
demo L 2026-11-12 0 99999 7 -1

| Start user | demo |
| Paths | 1 |
| Fix points | 1 |
| Not collected | 17 |

| 1 | user demo -[The executable /opt/nightly-backup.sh of the service nightly-backup.service, which runs as root, can be overwritten]-> root (administrator privileges) |

| 1 | Critical (95 pts) | user demo → root (administrator privileges) | The executable /opt/nightly-backup.sh of the service nightly-backup.service, which runs as root, can be overwritten | 1 |

- ATT&CK: T1543.002 Create or Modify System Process: Systemd Service (approximate) https://attack.mitre.org/techniques/T1543/002/

sudo chown root:root /opt/nightly-backup.sh
sudo chmod go-w /opt/nightly-backup.sh

- Known-vulnerability check: used the bundled data from 2026-10-19. It covers (1) entries in the CISA KEV catalog 2026.10.16, (2) vulnerabilities whose FIRST EPSS score, an estimated probability of exploitation, reached 0.5 or higher at some point in the last 30 days (once added, they stay even after the score falls), and (3) vulnerabilities for which a public exploit module (Metasploit Framework) exists. (1) were exploited in real attacks and (2) are rated very likely to be exploited. (3) only means that a public exploit module exists, not that exploitation was observed. Only those that let a local user become root are included, so this is not a complete vulnerability scan.

The L in the second field of passwd -S marks the lock. Read the PathScope result like this.

  • Path: demo can overwrite the script that a root timer runs. Overwrite it, and the next time the timer fires, any command runs with root's rights
  • Fix points: the places worth fixing first, in priority order. Example commands come with them, but nothing is run automatically
  • Not collected: the 17 places that could not be checked are "unchecked", not "safe"
  • Known vulnerabilities: Matching installed software against known vulnerabilities (PathScope) matches the versions of the installed software and kernel against bundled data (CISA KEV and others). Only flaws that let a local user become root are covered. Nothing matched on this VM

When there is a path, also check whether demo has already rewritten the script. Compare its content and modification time with what its author expects.

Fix it as suggested, and compare with the previous result using History comparison and continuous checks (PathScope). This changes permissions (this changes the system).

sudo chown root:root /opt/nightly-backup.sh
sudo chmod go-w /opt/nightly-backup.sh
sudo pathscope --start=demo --lang=en --baseline=/tmp/ps-before.json --fail-on=new > /tmp/ps2.txt; echo "exit code: $?"
cat /tmp/ps2.txt
Excerpt
exit code: 0
| Paths | 0 |

No path from demo to root or sensitive data was found (within the range that could be collected).

## Changes since the previous run (`/tmp/ps-before.json`)

- Paths removed: 1 / paths added: 0
  - `-` `user(demo)-[systemd_writable_exec(nightly-backup.service, /opt/nightly-backup.sh)]->root`

One path is gone. --fail-on=new returns exit code 1 when a path appears that was not in the previous result. Trying it the other way round (the fixed result as the baseline, then loosening the permission again) printed one added path, with exit code 1. Run it on a schedule, and you notice when a configuration change opens a new path.

#Check everything together

One-button status check (PersonalSOC) collects the auth log, the web log and RoamSwitch's detection records, read-only. It also takes in the summaries of the bundled AccessScope and PathScope. At the end, use it to check for anything missed, in one list.

See it all at once
sudo personalsoc scan
Excerpt
△ MCP settings (Claude Code, Claude Desktop, opencode, Cursor)(No matching log in this environment)
△ AI agent settings (Claude Code settings.json)(No matching log in this environment)
○ Authentication log (sshd)
△ auditd(No matching log in this environment)
○ Web access log (nginx / apache)
△ DNS query log (dnsmasq / named)(No matching log in this environment)
○ RoamSwitch (MCP, read-only)(The supply-chain checks were not run (no watched folder is registered in RoamSwitch, or Pro is required))
○ Bundled AccessScope diagnosis (in-depth access logs, last 24 hours)(Some logs could not be read (no findings is not proof of safety))
○ Bundled PathScope analysis (privilege-escalation paths)(Paths start from user: admin / 17 areas could not be collected (no paths is not proof of safety))
△ LAN-layer threats seen by a Sensor on the same LAN (rogue DHCP, ARP spoofing and the like)(No Sensor is installed on this device, or its daemon cannot be reached)

RoamSwitch defense coverage
  67% is active now (of 39 techniques: 0 detected, 26 partly, 13 not watched; 0 have their guards off)
  Techniques no guard watches: T1686 Disable or Modify System Firewall, T1685.004 Disable or Modify Tools: Disable or Modify Linux Audit Syste…, T1070.003 Indicator Removal: Clear Command History, T1036 Masquerading, T1546.018 Event Triggered Execution: Python Startup Hooks

RoamSwitch settings that could not be confirmed
  1 item: Backup Recency
  These could not be measured, so they are counted neither as weaknesses nor as fine (run as root, or check them in RoamSwitch's diagnosis).

Bundled tools' diagnoses
  AccessScope: overall risk 13/100 (Low). Findings: 2 high, 1 medium, 8 low
  PathScope: 1 path to root or sensitive data. 1 fix point (1 of high priority or above). The top 1 cuts 1
  Paths start from user: admin
  See AccessScope and PathScope for the evidence and fixes (PersonalSOC only reads their summaries).

[High/Confirmed] SSH login succeeded after brute force: 203.0.113.50 (T1078)
  [Authentication log (sshd)] 1 success after 6 failures from 203.0.113.50. It may be a legitimate user's typo, so check what the account did right afterwards. (evidence line numbers refer to the collected log) RoamSwitch's SSH guard recorded the same source too (two independent records).
[High/Likely] SSH guard: a login succeeded after repeated failures: 203.0.113.50 (T1078)
  RoamSwitch's SSH guard recorded a success after 6 failures (Thu, 2026-11-12 02:53:21 (UTC)). A password may have been guessed. Check with the account's owner.
[High/Confirmed] Known vulnerabilities in a package: brotli 1.1.0-2build2 (1 CVE) (-)
  brotli 1.1.0-2build2: Highest CVSS is 9.8. Main CVEs: CVE-2026-111497. Fixed in: 1.1.0-2ubuntu0.1.
[High/Likely] PathScope: 1 privilege-escalation path to root or sensitive data (T1068)
  Paths start from user: admin. 1 fix point (1 of high priority or above). Fixing the top 1 cuts 1 of 1 path. Examples: sudo_nopasswd_all(admin, sudoers:/etc/sudoers.d/90-cloud-init-users:4). See PathScope for how to fix and the full picture.
[Medium/Likely] Web attack pattern: 203.0.113.50 (T1190)
  [Web access log (nginx / apache)] 4 requests from 203.0.113.50. Types: Probing for sensitive files, SQL injection, Path traversal / LFI. No successful response was observed. (evidence line numbers refer to the collected log)
[Medium/Likely] RoamSwitch unresolved detection: RoamSwitch guard (-)
  /etc/systemd/system/nightly-backup.timer: new file (no SSH login found: a service tart-guest-agent.service) (action: recorded; 1 time, latest: Thu, 2026-11-12 02:53:24 (UTC))
[Medium/Confirmed] Configuration weakness: Known Kernel CVE Exposure (-)
  Apply your distribution's kernel updates and reboot.

Read five things.

  • ○ and △: ○ is a source that was read, △ one that was not, with the reason. A source that was not read was not checked
  • One event, several records: the SSH success came as "two independent records", the auth log and RoamSwitch's SSH guard. Both count six failures. AccessScope's findings line up next to it. Separate records that agree make the finding more certain
  • The one PathScope path: as "Paths start from user: admin" says, it is admin's sudoers entry (any sudo without a password). The demo path was fixed in the previous chapter
  • Defense coverage: the share of attack techniques that RoamSwitch's guards watch. Techniques marked not watched need other ways to see them
  • Configuration weaknesses, and items that could not be confirmed: items the health check marked WARN are listed as configuration weaknesses. Items it marked UNCONFIRMED are not counted as weaknesses; they appear by count and name in the box "RoamSwitch settings that could not be confirmed". Here that is Backup Recency: a timer exists, but its last run could not be confirmed. Known package vulnerabilities from RoamSwitch's vulnerability data are listed too

RoamSwitch's records also tell you who created the timer. File integrity monitoring (FIM) reports a file placed in /etc/systemd/system on the spot, together with the process that wrote it.

sudo roamswitch notifications | grep -B4 -A5 "nightly-backup.timer"
Excerpt
  2026-11-12T02:53:24.533476487+00:00
  Critical Path FIM Tampering Detected (1 violation)
    Host: ubuntu (IP: 192.168.64.16)
    Integrity violations in critical system files:
    /etc/systemd/system/nightly-backup.timer: new file
      Process: root / /usr/bin/dash / command: sh /tmp/make-events.sh (pid 1565)
      No SSH login was found (it is a process of the systemd service tart-guest-agent.service)
    Recommended Action: Run `sudo roamswitch fim verify` to inspect changes.
    Timestamp: 2026-11-12 02:53:24

The timer on PathScope's path was placed at 02:53:24 UTC by root running sh /tmp/make-events.sh. It also shows that it ran under the service that drives this VM (tart-guest-agent.service), not from an SSH login. In a real investigation, ask whoever placed it, and if nobody recognises it, find out where that script came from.

When you finish the practice, undo it.

sudo systemctl disable --now nightly-backup.timer
sudo rm -f /etc/systemd/system/nightly-backup.service /etc/systemd/system/nightly-backup.timer /opt/nightly-backup.sh
sudo systemctl daemon-reload
sudo userdel -r demo
for a in 198.51.100.7 203.0.113.50 203.0.113.77; do sudo ip addr del $a/32 dev lo; done

#What you cannot see

What logs cannot tell you

Within what was checked on a real machine (a throwaway Ubuntu 24.04 VM):

  • What the logs do not hold is invisible. As the AccessScope report itself says, request bodies, response content and the application's internal state are unknown. Periods where logs were rotated or deleted are invisible too
  • audit-logs reads only the latest 500 lines. On a busy server, asking for one hour does not mean all of that hour is read. The warning line in the output tells you
  • Logs are linked only by an identical address. Behind NAT or a proxy, different people share one address
  • Nothing was blocked in this practice. The addresses added to lo belong to this machine. fail2ban ignores its own addresses (ignoreself), and RoamSwitch's SSH guard recorded them as "exempt". Password guessing from outside would be a target for blocking
  • No finding is not proof of safety. PathScope's "no path" also holds only within the range it could collect
Check yourself

AccessScope reports "The same address in several logs". What is the right reading?

Show the explanation

The report says "The only link is the identical address." Ask the account's owner whose login it was. Check a blocklist before applying it, too.

Check yourself

You run sudo roamswitch audit-logs 1 twice in a row, and the second run shows the same frequency spikes. Why?

Show the explanation

A manual run never changes the baseline, however often you repeat it. The daemon's audit, which runs every log_audit_interval_secs, moves it and keeps entries in the notification history (its first run after start only learns).

#What to learn next

Features covered
What was verified

Verified on 2026-11-12. Target: Linux 1.17.39. Every lab ran on a fresh disposable Ubuntu 24.04 VM with 1.17.39 from the public apt repository (bundled AccessScope 1.0.7, PathScope 1.0.5 and PersonalSOC), one VM for Japanese and one for English. PathScope's --fail-on=new was also checked the other way round.

Command output varies with the product version and the environment. If yours differs, check the version first.