Networks
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 assets, attack surface, and risk.
- Parse Nmap output while retaining host identity and source evidence.
- Generate service-specific review questions, leaving risk UNASSESSED.
- Investigate missing evidence and record an owner and next action.
- Produce a review worksheet for explicitly authorized lab targets.
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 evidence, host-aware parsing, dictionaries, review questions |
| Concepts needed | Assets, attack surface, evidence, uncertainty, likelihood and impact |
| 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.
- An asset is a resource of value, such as information, a device, or a service. An inventory lists assets.
- An attack surface consists of exposed interfaces and interactions; open ports are only one part.
- Risk considers likelihood and impact in context, including exposure and controls. A service name alone cannot establish a grade.
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:
- Repeat parsing consistently while retaining each host.
- Compare observations only under matching scope and method.
- Generate reproducible review questions; record human assessment separately.
The parser can retain observations for multiple hosts. Larger inventories still need explicit authorization, input validation, consistent scan conditions, and human review; scaling a worksheet does not validate a risk 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 the service purpose and owner? Missing evidence needs investigation before a risk grade.
- 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
Retain each Nmap scan report host line as context for the following port rows. Dropping host identity merges different assets incorrectly.
3-2. Building the Parser — Picking Out Only the Regular Lines
Input (riskmap.py, stage 1)
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
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': ''}
The regular expression extracts port fields; the parser separately tracks the current host. This teaching parser handles the shown normal-output format. Nmap XML is preferable for general machine processing.
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. Selecting Review Questions
Input (riskmap.py, complete version)
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
Expected pattern for the example file after correction:
localhost (127.0.0.1) | 8000/tcp | open | http | UNASSESSED | Review transport, data and access controls localhost (127.0.0.1) | 33211/tcp | open | unknown | UNASSESSED | Identify service and owner Rows requiring evidence review: 2
Every row stays UNASSESSED until exposure, controls, and impact are reviewed. Service labels select questions, not a final risk grade. Unknown services get an identity-and-owner investigation action.
Reproducible parsing helps preserve evidence. A reproducible rule is not automatically a valid risk model; attach the evidence and reasoning used for a human assessment.
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 | Observed service | Risk | Next action |
|---|---|---|---|---|
| Owned loopback lab | 8000/tcp | HTTP response | Unassessed | Confirm purpose and listener scope; stop after practice |
| Historical example host | 33211/tcp | Unrecognized response | Unassessed | Identify owner and controls before grading |
Identifying a service resolves one uncertainty; it does not automatically make risk LOW. Keep UNASSESSED until exposure, controls, likelihood, and impact have been considered and the reasoning recorded.
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.
- List the exact owned lab addresses and ports you are authorized to inspect.
- Scan those targets with the agreed method and save the source files.
- Pass the files to riskmap.py and optionally choose a new –csv filename.
- Keep host, port, state, source, review question, and UNASSESSED risk in each row.
- Investigate service identity, exposure, controls, and impact; record the responsible person and next action.
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.
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
The complete riskmap.py above already accepts multiple input files and optional –csv report.csv. Use a new output filename; existing reports are preserved.
"""Review worksheet from your own Nmap normal output, not a risk score."""
import argparse
import csv
import re
from pathlib import Path
def parse_nmap(path):
host = None
rows = []
for line in Path(path).read_text(encoding="utf-8").splitlines():
if line.startswith("Nmap scan report for "):
host = line.removeprefix("Nmap scan report for ").strip()
match = re.match(r"^(\d+)/(tcp|udp)\s+(\S+)\s+(\S+)\s*(.*)$",
line.strip())
if match:
if host is None:
raise ValueError("port row has no host identity")
rows.append({"host": host, "port": int(match[1]),
"proto": match[2], "state": match[3],
"service": match[4], "version": match[5].strip()})
return rows
def assess(service):
questions = {
"ftp": "Check transport protection, authentication and exposure",
"telnet": "Check plaintext exposure and replacement options",
"http": "Check data sensitivity, TLS termination and authorization",
"http-alt": "Confirm service identity and transport protection",
"ssh": "Check identity, keys, authentication, patching and exposure",
}
return "UNASSESSED", questions.get(service, "Identify service and owner")
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("files", nargs="+")
parser.add_argument("--csv", type=Path)
args = parser.parse_args(argv)
report = []
for path in args.files:
for row in parse_nmap(path):
grade, question = assess(row["service"])
row.update(source=path, risk=grade, action=question)
report.append(row)
print(row["host"], f'{row["port"]}/{row["proto"]}',
row["state"], row["service"], grade, question, sep=" | ")
print("Rows requiring evidence review:", len(report))
if args.csv:
fields = ["source", "host", "port", "proto", "state", "service",
"version", "risk", "action"]
with args.csv.open("x", newline="", encoding="utf-8") as fp:
writer = csv.DictWriter(fp, fieldnames=fields)
writer.writeheader()
writer.writerows(report)
if __name__ == "__main__":
main()
For the illustrated source file, retain separate rows for 8000 and 33211 with the host included, and leave both risks UNASSESSED. Verify the actual services in your environment instead of expecting those historical ports to exist.
Verify host identity, protocol, port, state, source filename, uncertainty, and an investigation action. Scan only the explicit addresses and ports authorized for your lab.
Exercise Answers
Answer 1: An asset is a resource of value; an inventory lists it. Its attack surface includes exposed interfaces and interactions. Risk considers likelihood and impact with exposure and controls. This worksheet organizes observations and review actions; it does not automatically validate a risk grade.
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: An unknown service needs investigation, not an invented LOW or HIGH label. Here every row remains UNASSESSED until there is sufficient evidence for a reasoned assessment; identifying the service alone is not enough.
Compare observations keyed by (host, protocol, port) under the same scan scope and method. A newly observed response is not proof a new physical device appeared; a missing response is not proof a service closed. Keep service labels as separate evidence.
Completion Criteria Checklist
- [ ] I can define asset, attack surface, and risk separately.
- [ ] My parser preserves host and source identity.
- [ ] I generate review questions without inventing a risk grade.
- [ ] Unknown evidence has an investigation action and owner.
- [ ] Comparisons use the same scan conditions.
- [ ] I inspected only explicitly authorized targets and ports.
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.
A service name alone cannot establish risk. Record exposure, authentication, authorization, maintained version, asset sensitivity, and plausible impact; mark missing evidence as unknown. The helper organizes review questions and leaves risk unassessed. Do not treat a count of suspicious attributes as a validated scoring model.
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.
Coordinate the exact targets, ports, timing, and acceptable load with the owner before the lab. Permission defines scope; it does not guarantee that every probe is harmless. Stop and reassess if an unexpected effect occurs.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Asset | A resource of value; an inventory lists assets |
| Attack surface | Exposed interfaces and interactions; more than open ports |
| Risk | Likelihood and impact assessed with exposure, controls, and evidence |
The meaning of ? |
Missing evidence requires an explicit investigation; risk remains UNASSESSED |
| Reproducible judgment | Repeatable parsing and review questions support, but do not replace, reasoned assessment |
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) |
Extract port/service from scan lines |
| 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
A useful review states what was observed, what remains unknown, and who will investigate next. Fewer question marks alone are not proof of lower risk; assess the quality of evidence and the completed actions.
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.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.