diff --git a/README.md b/README.md index ec75cef..e899162 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ The current version is `0.2.0-alpha.1`. Windows x64 is the only supported target PortCVE is not an exploit framework. It does not brute-force credentials, send exploit payloads, close ports, edit firewall rules, or claim that a CVE match is exploitable. +## What it looks like + +`portcve list` maps every local listener to its owner process, scope, and PID: + +![Terminal output of portcve list showing listening ports mapped to owner process, scope, and PID](assets/portcve-list.svg) + +The screenshot is real output from a Windows x64 build (`HOST POLICY` reports `NOT CHECKED` until a CVE fingerprint rule is supplied). + ## Build from source Install the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0), then run: diff --git a/assets/portcve-list.svg b/assets/portcve-list.svg new file mode 100644 index 0000000..dd2affa --- /dev/null +++ b/assets/portcve-list.svg @@ -0,0 +1,16 @@ + + PortCVE list command output + Real terminal output of `portcve list` showing local listening ports mapped to owner process, scope, and PID. + + + + + + + + portcve list + PROTO LOCAL STATE SCOPE OWNER CONTAINER PID HOST POLICY----- ------------------------------------ ------ --------- ----------------------- --------- ----- -----------TCP4 0.0.0.0:135 LISTEN wildcard svchost.exe - 2356 NOT CHECKEDTCP6 [::]:135 LISTEN wildcard svchost.exe - 2356 NOT CHECKEDTCP4 169.254.38.255:139 LISTEN interface System - 4 NOT CHECKEDTCP4 172.22.32.1:139 LISTEN interface System - 4 NOT CHECKEDTCP4 192.168.68.70:139 LISTEN interface System - 4 NOT CHECKEDTCP4 0.0.0.0:445 LISTEN wildcard System - 4 NOT CHECKEDTCP6 [::]:445 LISTEN wildcard System - 4 NOT CHECKEDTCP4 0.0.0.0:5040 LISTEN wildcard svchost.exe - 2768 NOT CHECKEDTCP4 0.0.0.0:5357 LISTEN wildcard System - 4 NOT CHECKEDTCP6 [::]:5357 LISTEN wildcard System - 4 NOT CHECKED + diff --git a/tools/render_list_svg.py b/tools/render_list_svg.py new file mode 100644 index 0000000..033cc15 --- /dev/null +++ b/tools/render_list_svg.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Render the README terminal screenshot from a real `portcve list` run. + +Requires a published Windows x64 build first: + + dotnet publish src\\PortCVE\\PortCVE.csproj -c Release -r win-x64 --self-contained true -o artifacts\\win-x64 + +Then: + + python tools/render_list_svg.py + python tools/render_list_svg.py --check +""" + +from __future__ import annotations + +import argparse +import html +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "assets" / "portcve-list.svg" + +FONT_SIZE = 13 +CHAR_W = 7.8 +LINE_H = 20 +PAD = 22 +TITLE_H = 38 + + +def render(exe: Path, rows: int = 12) -> str: + out = subprocess.run( + [str(exe), "list"], capture_output=True, text=True, + encoding="utf-8", errors="replace", + ).stdout + lines = out.splitlines()[:rows] + + maxlen = max(len(line) for line in lines) + width = PAD * 2 + maxlen * CHAR_W + height = TITLE_H + PAD + len(lines) * LINE_H + PAD + + texts = [] + y = TITLE_H + PAD + FONT_SIZE + for i, line in enumerate(lines): + color = "#58a6ff" if i == 0 else ("#484f58" if i == 1 else "#e6edf3") + texts.append(f'{html.escape(line)}') + y += LINE_H + + return f''' + PortCVE list command output + Real terminal output of `portcve list` showing local listening ports mapped to owner process, scope, and PID. + + + + + + + + portcve list + {''.join(texts)} + +''' + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--exe", default=str(ROOT / "artifacts" / "win-x64" / "portcve.exe"), + help="path to a published portcve.exe") + parser.add_argument("--check", action="store_true", help="fail when the committed SVG is stale") + args = parser.parse_args() + + exe = Path(args.exe) + if not exe.exists(): + raise SystemExit(f"portcve.exe not found at {exe} — run dotnet publish first (see docstring)") + + expected = render(exe) + + if args.check: + if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != expected: + raise SystemExit(f"stale generated asset: {OUTPUT.relative_to(ROOT)}") + print(f"up to date: {OUTPUT.relative_to(ROOT)}") + return + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(expected, encoding="utf-8") + print(f"wrote {OUTPUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main()