What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

Networks

Step 82. Project — Network Map and Risk Assessment

Step 82Estimated practice · 4 hours

Level 1 — Programming and the Inside of a Computer | Difficulty ★★★★☆ | Estimated time: 4 hours

Prerequisites: Step 81 complete. You can run an -sV scan with nmap and save -oN files. 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.1 scan 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.

  1. List the exact owned lab addresses and ports you are authorized to inspect.
  2. Scan those targets with the agreed method and save the source files.
  3. Pass the files to riskmap.py and optionally choose a new –csv filename.
  4. Keep host, port, state, source, review question, and UNASSESSED risk in each row.
  5. 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.

Something wrong with this page or a link? Let us know.
Next