Step 82. Project — Network Map and Risk Assessment
Level 1 — Programming and the Inside of a Computer | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Step 81 complete. You can run an
-sVscan with nmap and save-oNfiles. You know basic Python syntax (file reading, regular expressions, lists).
- What you need: a Linux terminal, nmap, Python 3. And the scan report file you saved in Step 81.
- Caution: ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Every hands-on measurement in this chapter was performed against
127.0.0.1scan files only.
Do you know the very first thing an attacker does after entering a network? "Draw a map of what’s there." A defender’s first job is exactly the same — because you can’t protect what you don’t know. Today, using Step 81’s scan results as raw material, we run through the entire skeleton of a security assessment once: "discover → organize → prioritize → act." If scanning is nmap’s job, turning the results into a risk table is the job of your Python skills.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Define the three terms — asset, attack surface, and risk — and apply them in practice
- Parse an nmap
-oNoutput file with Python into a structured list - Assign a risk level to each service with rule-based logic and attach grounds
- Build the habit of separating "unidentified" items into an investigation list
- Complete a one-page "asset inventory + risk assessment table," your first security assessment document
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Linux terminal + Python 3 (script file) |
| Today’s commands/features | nmap -oN (gathering material), Python re.match() (line parsing), lists of dictionaries, rule-based classification |
| Concepts needed | Asset, attack surface, risk (impact × likelihood), the scan output format from Step 81 |
| Today’s artifacts | riskmap.py — a converter that turns a scan file into a risk table + one finished assessment table |
2-1. Asset, Attack Surface, Risk
Let’s organize the three terms we’ll use today.
- Asset: the complete inventory of what must be protected. Every device connected to the network, and every service running on it, is an asset.
- Attack surface: the sum total of contact points the asset has opened to the outside. Each open port is one square of the attack surface. The fewer openings, the narrower the surface; the narrower, the easier to defend.
- Risk: a sense that weighs together "how much it hurts if breached (impact)" and "how likely it is to be breached (likelihood)." Today, three grades — high/medium/low — are enough.
2-2. Why Parse — A File Is Data
Step 81’s scan output is a human-friendly table, but it’s actually regular text. Regular text can be read by a program. Parsing the scan file with Python makes things like this possible:
- Automatically repeat the same processing even across a hundred devices
- Automatically detect "newly opened things" compared with last month’s file
- Write risk rules as code to secure reproducibility of judgment
Today’s material is a single 127.0.0.1 scan file, but this code expands as-is into a hundred-machine assessment.
2-3. How to Think About Risk
Risk isn’t a hunch — it’s a comparison. Try asking these questions:
- Does this service travel encrypted, or in plaintext? (telnet/ftp/http are plaintext)
- Is authentication in place? Is it not a default password?
- Do I know why this port is open? Not knowing is the most dangerous of all.
- If this service is breached, what leaks out?
The worse the answers, the higher the risk. Today’s goal isn’t a precise score — it’s deciding the order of "where do we fix first."
3. Follow Along
3-1. Preparing the Material — Reviewing the Scan File
Open the full-scan file you saved in Step 81. If you don’t have it, make it again.
Input
nmap -sV --version-intensity 2 -p- -oN scan_full.txt 127.0.0.1
cat scan_full.txt
Output (measured 2026-09-09, key parts):
# Nmap 7.94SVN scan initiated Wed Sep 9 13:37:10 2026 as: nmap -sV ... -p- -oN scan_full.txt 127.0.0.1
Nmap scan report for localhost (127.0.0.1)
Host is up (0.0000010s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
8000/tcp open http SimpleHTTPServer 0.6 (Python 3.12.3)
33211/tcp open unknown
How to read it: from this file, the only lines we’ll have Python read are those of the form number/protocol state service version. The rest (headers, informational messages) must be filtered out — that’s the work of parsing.
3-2. Building the Parser — Picking Out Only the Regular Lines
Input (riskmap.py, stage 1)
import re
def parse_nmap(path):
"""Extract 'port/protocol state service version' lines from an -oN scan file."""
rows = []
with open(path, encoding="utf-8") as f:
for line in f:
m = re.match(r"^(d+)/(tcp|udp)s+(S+)s+(S+)s*(.*)$", line.strip())
if m:
rows.append({
"port": int(m.group(1)),
"proto": m.group(2),
"state": m.group(3),
"service": m.group(4),
"version": m.group(5).strip(),
})
return rows
rows = parse_nmap("scan_full.txt")
for r in rows:
print(r)
Output (measured 2026-09-09):
{'port': 8000, 'proto': 'tcp', 'state': 'open', 'service': 'http', 'version': 'SimpleHTTPServer 0.6 (Python 3.12.3)'}
{'port': 33211, 'proto': 'tcp', 'state': 'open', 'service': 'unknown', 'version': ''}
How to read it: the regular expression ^(d+)/(tcp|udp)s+(S+)s+(S+)s*(.*)$ matches only lines that "start with number/protocol followed by at least three words." Header lines (starting with #) and lines like Not shown: don’t fit the pattern and are filtered out automatically. Each line becomes one dictionary — now you can cook this data however you like.
Why: "turning a document into data" is the first step of all automation. The regular expressions from Step 48 make their field debut here.
3-3. Planting Risk Rules — Judgment as Code
Input (riskmap.py, complete version)
import re
def parse_nmap(path):
rows = []
with open(path, encoding="utf-8") as f:
for line in f:
m = re.match(r"^(d+)/(tcp|udp)s+(S+)s+(S+)s*(.*)$", line.strip())
if m:
rows.append({
"port": int(m.group(1)), "proto": m.group(2),
"state": m.group(3), "service": m.group(4),
"version": m.group(5).strip(),
})
return rows
RISK_RULES = [
(["telnet", "ftp"], "HIGH", "legacy protocol with no encryption"),
(["http", "http-alt"], "MED", "plaintext web service — contents may be exposed"),
(["ssh"], "LOW", "encrypted — just keep the version up to date"),
]
def assess(service):
for names, grade, reason in RISK_RULES:
if service in names:
return grade, reason
return "?", "unidentified — needs investigation"
rows = parse_nmap("scan_full.txt")
print(f"{'Port':<11}{'State':<8}{'Service':<12}{'Risk':<6}Basis")
for r in rows:
grade, reason = assess(r["service"])
print(f"{r['port']}/{r['proto']:<7}{r['state']:<8}{r['service']:<12}{grade:<6}{reason}")
unknown = [r for r in rows if assess(r["service"])[0] == "?"]
print(f"nUnidentified services: {len(unknown)} — added to investigation list")
Output (measured 2026-09-09):
Port State Service Risk Basis
8000/tcp open http MED plaintext web service — contents may be exposed
33211/tcp open unknown ? unidentified — needs investigation
Unidentified services: 1 — added to investigation list
How to read it: RISK_RULES is "judgment put into writing." Services that match a rule receive a grade and grounds, while unknown, which matches nothing, is separated into the investigation list with a ?. The line under the table — "Unidentified services: 1" — is this tool’s true protagonist: because the most dangerous square on the map is the square you don’t know.
Why: when risk is assigned by code, feeding the same file next month produces the same judgment. Reproducible judgment is a report’s credibility.
3-4. Make a Prediction — How Should We Handle unknown?
Here’s a prediction. If you assigned a risk level to an unknown service, which would be right: ① LOW (unknown, so low) ② MED ③ HIGH (unknown, so high) ④ don’t assign one and separate it into an investigation list?
Check for yourself: our code chose ④. And in Step 81, section 3-7, we traced this port’s identity — confirming an HTTP/1.0 404 Not Found response in the -sV fingerprint, and finally confirming with ss -tlnp that the port was opened by a container management program (measured 2026-09-09).
How to read it: ④’s power is that it doesn’t paper over "don’t know" with a grade, but connects it to an investigative action. The ? in a risk table isn’t a blank — it’s a to-do.
Why: real breaches usually begin with "a service nobody knows who opened." The habit of recording what you don’t know as unknown and chasing it to the end is what makes a map accurate.
3-5. Completing the Map — Turning the Table into a Document
Finally, transfer Python’s output into an assessment table for humans to read. In a spreadsheet or Markdown, in a format like this:
| Asset | Port | Service/Version | Risk | Action |
|---|---|---|---|---|
| My PC (127.0.0.1) | 8000 | SimpleHTTPServer 0.6 (Python 3.12.3) | MED | For practice — shut down when done |
| My PC (127.0.0.1) | 33211 | unknown → investigated: container management service | LOW (after verification) | Identity confirmed; keep local-only |
How to read it: if the action column is empty, that map is a wall ornament. However refined the risk levels, it’s only an assessment when there’s "so what will we do." The very process by which a port that was unknown gets corrected to "LOW (after verification)" through investigation is a good report row.
Why: the flow discover (scan) → organize (table) → prioritize (risk) → act (action) is exactly the same skeleton as a corporate security review. Today’s single page is its miniature.
4. Missions & Exercises
Mission — Your Home Network Map (after getting consent)
Only those who are ready: get your family’s understanding before proceeding. If you only have a lab, a lab network works too.
- Find your address and subnet with
ip a(Linux) oripconfig(Windows) (e.g.,192.168.0.10/24). - Draft an asset inventory with a ping scan:
sudo nmap -sn 192.168.0.0/24 -oN step82_ping.txt - Investigate each discovered device with
sudo nmap -sV -oN step82_devicename.txt address. - Extend riskmap.py to read multiple files at once (take file paths as a list) and print a per-device, per-port risk table.
- Fill in the "action" column of the finished table, and investigate until no unidentified (
?) items remain. Don’t leave anything unknown.
Exercises
Exercise 1. Explain the relationship among asset, attack surface, and risk in one sentence each.
Exercise 2. From the pattern’s perspective, explain why riskmap.py’s regular expression skips lines like Not shown: 65533 closed tcp ports (reset).
Exercise 3. Why is the design that gives unknown a ? and separates it into an investigation list better than a design that gives it LOW?
Exercise 4. You scan the same network again a month later. What feature could you add to riskmap.py to automatically find "ports newly opened compared to last month"? Explain the idea without code.
5. Model Answers & Completion Criteria
Mission Model Answer
Skeleton of the extension that reads multiple files:
import re, sys
def parse_nmap(path):
rows = []
with open(path, encoding="utf-8") as f:
for line in f:
m = re.match(r"^(d+)/(tcp|udp)s+(S+)s+(S+)s*(.*)$", line.strip())
if m:
rows.append({"port": int(m.group(1)), "proto": m.group(2),
"state": m.group(3), "service": m.group(4),
"version": m.group(5).strip()})
return rows
for path in sys.argv[1:]: # python3 riskmap.py file1 file2 ...
print(f"=== {path} ===")
for r in parse_nmap(path):
... # the assessment code from 3-3, as-is
Verification example based on measurement: feed it the 127.0.0.1 full-scan file and you should get two rows — 8000 (http, MED) and 33211 (unknown, ?) — and the last line should read "Unidentified services: 1" (measured 2026-09-09). In the home-network mission, success is the same output repeating for each device file.
How to verify: ① does python3 riskmap.py scan_full.txt print the table in one shot? ② are unknown items marked ? and caught in the investigation-list count? ③ is an action filled in for every row of the final table? ④ is family consent recorded on the first line of your notes?
Exercise Answers
Answer 1. An asset is the complete inventory of what must be protected; the attack surface is the sum of contact points (open ports) that asset has opened to the outside; risk is a prioritization sense that weighs, per asset, "impact if breached" together with "likelihood of being breached." The map is a document that attaches attack surface and risk to an asset inventory.
Answer 2. Because the pattern starts with ^(d+)/(tcp|udp). Not shown: doesn’t start with a digit, and it lacks the "number/protocol" head of the 8000/tcp form, so the match fails at the very first character. The same goes for the header’s # lines.
Answer 3. Giving it LOW makes it look like "a verified, safe item," and the investigation stops. Something unknown is in a state where risk assessment is impossible — not a state of low risk. A ? is assessment deferred + an action (investigation) scheduled, so the gap remains explicitly on the map and necessarily leads to follow-up action. Given that real incidents start with "services nobody knows who opened," explicitly marking unknown squares is itself defense.
Answer 4. Parse each of the two files, build a set of (port, service) pairs, then print the difference — this month’s set minus last month’s set. You can implement it with Python’s set and the - operator, and "disappeared ports" (last month − this month) works the same way. That is the algorithm of change monitoring.
Completion Criteria Checklist
- [ ] I can define asset / attack surface / risk in my own words
- [ ] I can parse an nmap
-oNfile with Python regular expressions - [ ] I can express risk rules as code and attach grounds
- [ ] I separated unknown items as
?and connected them to investigation - [ ] I completed one risk assessment table with the action column filled in
- [ ] (If doing the mission) I got family consent and completed a map of the whole subnet
6. Common Pitfalls & Fixes
Wall 1. My parse results are completely empty
Symptom: rows comes out as an empty list.
Cause: the file path is wrong, or if you saved screen output copied as-is instead of -oN, the line shapes may differ subtly.
Fix: check the file contents with your eyes using cat scan_full.txt, and shrink the regular expression one step at a time (starting from ^(d+)) to find where it stops matching. Debugging is reconnaissance too.
Wall 2. Output containing Korean gets garbled
Symptom: the table’s Korean column alignment is off, or characters are broken.
Cause: Korean characters occupy two screen cells, but f-string width alignment only counts characters. Opening a cp949-encoded file as UTF-8 also garbles it (review Step 50).
Fix: alignment doesn’t have to be pretty as long as the data is right. When opening files, specify encoding="utf-8" explicitly.
Wall 3. My criteria for assigning risk are fuzzy
Symptom: you hesitate every time over where to put high/medium/low.
Cause: that’s normal. Risk is a sense refined by experience.
Fix: ask only three things. ① Does it travel in plaintext? ② Is authentication absent or a default password? ③ Do I not know this port? If two or more of the three are bad, it’s HIGH. And if you write the rules in code (RISK_RULES), the same criteria hold next month.
Wall 4. Scanning at home, fewer devices show up than expected
Symptom: the TV is clearly on, but it’s not in the list (Screen example situation).
Cause: it’s asleep, or the device doesn’t answer pings, or it’s on a different subnet like a guest network.
Fix: wake the device and rescan. Cross-checking with the router admin page’s client list helps you find missing devices. "Not detected" isn’t "absent" — it’s "didn’t answer."
Wall 5. A security warning popped up on a family member’s device while scanning
Symptom: a security alert went off on a device you scanned.
Cause: modern devices detect port scans and report them. Your scan got "seen."
Fix: if you told them in advance, there’s nothing to worry about — which is why condition number 0 of the mission was consent. That detection is exactly the defender’s eye. It’s a great opportunity to learn how your scans look, and in Step 84 we’ll see these footprints again at the packet level.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Asset | The complete inventory of what must be protected — devices and the services on them |
| Attack surface | The sum of contact points an asset has opened — one square per open port |
| Risk | A comparative sense of impact × likelihood — high/medium/low is enough |
The meaning of ? |
Assessment deferred + investigation scheduled. The most dangerous square on the map |
| Reproducible judgment | Writing rules as code so the same conclusion comes out next month too |
Today’s Commands and Code
| Command/Code | What it does |
|---|---|
nmap -sV -p- -oN file target |
Gather material (scan report) |
| `re.match(r"^(d+)/(tcp | udp)s+(S+)s+(S+)s*(.*)$", line)` |
| List of dictionaries + list of rules | Convert discoveries into a risk table |
sys.argv[1:] |
Process multiple scan files at once |
| Set difference (this month − last month) | Change monitoring for "newly opened ports" (mission extension) |
An Instinct More Important Than Commands
An assessment doesn’t end as a perfect document — it ends as "a meeting that decides the next action." In today’s table, the most important column is the action column, and the most dangerous column is the ? column. And a map isn’t drawn once and done — it’s a document you redraw regularly to monitor change. If the map you redraw a month later has one fewer ?, that is your first security achievement report.
Lastly, this map is double-sided. To an intruder, the same map becomes a list of weaknesses, so the map file itself is also something to protect. And never draw a map of a network you don’t have permission to map in the first place — that is the usage license for this skill.
Once every box is checked, Step 82 is complete.