Automating a Simple Reconnaissance Task
Reconnaissance generates lists: lists of hosts, names, ports. Before any tool sends a packet, you must prepare and clean those lists, and above all make sure everything on them is inside the authorized scope. In this lesson you will write a script that reads a target list, removes noise, refuses anything out of scope and produces a tidy report. No traffic is sent anywhere: this is pure data preparation, which is exactly where mistakes are cheapest to catch.
The problem
You are preparing a test in your lab. The network is the host-only network 192.168.56.0/24, a private range used by many virtual machine setups. A colleague gave you a messy text file called targets.txt:
# lab targets, first draft
192.168.56.10 # domain controller
192.168.56.20
192.168.56.20
192.168.56.30 # file server
10.99.0.5 # ??? someone added this
not-an-address
192.168.56.250
It has comments, blank lines, duplicates, an address outside the lab, and a line that is not an address at all. Sending this raw list to a scanner would be careless. A short script can clean it and, more importantly, enforce the scope.
The principle: scope is code too
In the previous course, you learned to write a scope into a contract. Here we write the same rule into the tool. A scope check in code means that even a tired tester at midnight cannot send traffic outside the agreed range by mistake. Mature teams build this guard-rail into every internal script.
Step 1: read and clean the file
from pathlib import Path
def read_targets(path):
targets = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
line = line.split("#", 1)[0].strip() # drop comments and spaces
if line: # ignore empty lines
targets.append(line)
return targets
Notice split("#", 1)[0]: it cuts the line at the first # and keeps the left part. That is how we remove comments.
Step 2: validate and check scope
Python's standard ipaddress module knows how to parse addresses and networks, so we do not need clumsy text matching.
import ipaddress
SCOPE = ipaddress.ip_network("192.168.56.0/24")
def check_target(text):
try:
address = ipaddress.ip_address(text)
except ValueError:
return None, "not a valid IP address"
if address not in SCOPE:
return None, "outside authorized scope"
return address, "ok"
The function returns a pair: the address (or None) and a message explaining the decision. Returning the reason is important, because a script that silently drops lines makes you wonder what happened.
Step 3: put it together
def prepare(path):
accepted = set()
rejected = []
for text in read_targets(path):
address, reason = check_target(text)
if address is None:
rejected.append((text, reason))
else:
accepted.add(address)
return sorted(accepted), rejected
def main():
accepted, rejected = prepare("targets.txt")
print(f"Accepted ({len(accepted)}):")
for address in accepted:
print(f" {address}")
print(f"Rejected ({len(rejected)}):")
for text, reason in rejected:
print(f" {text}: {reason}")
if __name__ == "__main__":
main()
A set automatically removes duplicates, and sorted puts the addresses in numeric order. The line if __name__ == "__main__": means "run main() only when this file is executed directly", which lets you reuse the functions from another script later.
Expected result
With the messy file above, the accepted list contains four addresses, .10, .20, .30 and .250, because the duplicate of .20 is merged by the set. Two lines are rejected: 10.99.0.5 as outside the authorized scope and not-an-address as not a valid IP address.
Step 4: export a clean, shareable result
Professionals share results as CSV files because spreadsheets open them. The standard csv module handles quoting for you:
import csv
def write_csv(path, accepted, rejected):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["address", "status", "reason"])
for address in accepted:
writer.writerow([address, "accepted", "in scope"])
for text, reason in rejected:
writer.writerow([text, "rejected", reason])
Call it at the end of main() with write_csv("targets-checked.csv", accepted, rejected). You now have an auditable record of what was accepted and why, which you can attach to your test notes.
Good habits to adopt now
- Fail closed: when in doubt, reject. Accepting by default is how scope violations happen.
- Keep the scope in one place: a single variable or file that you can read and compare with the contract.
- Log decisions: reasons make debugging and reporting easy.
- Separate preparation from action: this script prepares a list; any later script that sends traffic should call the same scope check again before sending anything.
- Do not hard-code secrets or real client data in scripts you plan to publish.
Your turn: extend the scope check
Improve the script so that the scope can contain several networks (for instance 192.168.56.0/24 and 192.168.57.0/24), and so that one specific address, 192.168.56.250, is explicitly excluded because the client asked you not to touch it (a fragile device). Then add the rejection reason "explicitly excluded" for that address.
See a solution
import ipaddress
SCOPE = [
ipaddress.ip_network("192.168.56.0/24"),
ipaddress.ip_network("192.168.57.0/24"),
]
EXCLUDED = {ipaddress.ip_address("192.168.56.250")}
def check_target(text):
try:
address = ipaddress.ip_address(text)
except ValueError:
return None, "not a valid IP address"
if address in EXCLUDED:
return None, "explicitly excluded"
if not any(address in network for network in SCOPE):
return None, "outside authorized scope"
return address, "ok"
The order matters: the exclusion is tested before the scope, so an excluded address can never slip through because it also belongs to an authorized network. Exclusions always win over inclusions.
Portfolio step
Save the final script as 02-python-security/scope_check.py with a sample targets.txt that uses only private lab addresses and a README describing the guard-rail idea. In an interview, being able to say "I built the scope check into my tooling" tells a hiring manager a lot about your professionalism.
Key takeaways
- Preparing and cleaning target lists is where reconnaissance automation starts, and where mistakes are cheapest to catch.
- The
ipaddressmodule validates addresses and checks membership in networks reliably. - Enforce scope in code, fail closed, and make exclusions override inclusions.
- Always keep the reason for each decision and export results in a shareable format such as CSV.
- This script sends no traffic: separating preparation from action is a good safety pattern.
Next, you will read data the other way around: not what you want to test, but what tools and systems already recorded.