Unit 1: Python for defenders · Lesson 3 of 6 · 22 min
Loops, conditions and errors
Walk the whole inventory, keep the risky hosts and read the errors Python gives you.
mission
- You are
- the engineer running this week's exposure review
- Situation
- Telnet sends passwords in clear text and remote desktop is a favourite target. Your manager wants every host that still exposes either one.
- Goal
- Write a script that loops over the inventory and flags telnet and remote desktop.
- Hand in
- A list of hosts with telnet (23) or remote desktop (3389) open.
A loop, one line at a time
web02 web ubuntu-24.04 22,80,443,3389 db02 db ubuntu-24.04 22,5432,8443 print01 print ubuntu-24.04 23,80,9100
3 lines. data/inventory.txt, before any filter
Write the script
Indentation is the syntax: everything indented under for runs once per line.
cat > scripts/exposure.py <<'EOF'
RISKY = {23: "telnet", 3389: "remote desktop"}
for line in open("data/inventory.txt"):
name, role, system, ports = line.split()
ports = [int(p) for p in ports.split(",")]
for port, service in RISKY.items():
if port in ports:
print(f"{name:<10} {service}")
EOF
Run it
python3 scripts/exposure.py
web02 remote desktop
db01 telnet
vpn01 telnet
dc01 remote desktop
dc02 telnet
dc02 remote desktop
dc03 remote desktop
file02 telnet
file03 remote desktop
print01 telnet
print02 telnet
print03 telnet
legacy01 telnet
you should see One line per host and risky service. A host with both appears twice.
Count instead of listing
Managers want numbers first. Pipe the output into standard tools rather than rewriting the script.
python3 scripts/exposure.py | grep -c telnet 8 python3 scripts/exposure.py | grep -c 'remote desktop' 5
Meet your first traceback
Real data is messy. Add a broken line to a copy of the inventory and see how Python reports it.
cp data/inventory.txt /tmp/inv.txt && echo 'printer-old 9100' >> /tmp/inv.txt sed 's#data/inventory.txt#/tmp/inv.txt#' scripts/exposure.py > /tmp/exposure.py && python3 /tmp/exposure.py | tail -n 7 Traceback (most recent call last): File "/tmp/exposure.py", line 4, in <module> name, role, system, ports = line.split() ^^^^^^^^^^^^^^^^^^^^^^^^^ ValueError: not enough values to unpack (expected 4, got 2) dc03 remote desktop file02 telnet file03 remote desktop print01 telnet print02 telnet print03 telnet legacy01 telnet
you should see Read tracebacks from the bottom: the last line names the error, the line above shows where.
defender tip A crash halfway through a run still printed earlier findings. In automation that partial output is easy to mistake for a full result.
Handle bad lines on purpose
Catch the specific error, report the bad line and keep going.
cat > /tmp/exposure2.py <<'EOF' RISKY = {23: "telnet", 3389: "remote desktop"} for n, line in enumerate(open("/tmp/inv.txt"), 1): try: name, role, system, ports = line.split() except ValueError: print(f"skipped line {n}: {line.strip()!r}") continue ports = [int(p) for p in ports.split(",")] for port, service in RISKY.items(): if port in ports: print(f"{name:<10} {service}") EOF python3 /tmp/exposure2.py | tail -n 3 print03 telnet legacy01 telnet skipped line 18: 'printer-old 9100'
you should see The script finishes and tells you which line it skipped.
try:
name, role, system, ports = line.split()
except Exception:
passSwallows every error. A changed log format now looks like a quiet day.
try:
name, role, system, ports = line.split()
except ValueError:
print(f"skipped line {n}")
continueCatches only the expected error and says what it skipped.
What a defender takes away
- 1A loop plus a condition is the core of most security scripts.
- 2Read tracebacks bottom up: error type, then the line that raised it.
- 3Never hide errors. Report skipped input so a quiet result means something.
Questions
0 of 3 solved
Answer each one from what you find on your lab machine.