Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions assets/portcve-list.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions tools/render_list_svg.py
Original file line number Diff line number Diff line change
@@ -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'<text x="{PAD}" y="{y:.0f}" fill="{color}">{html.escape(line)}</text>')
y += LINE_H

return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width:.0f}" height="{height:.0f}" viewBox="0 0 {width:.0f} {height:.0f}" role="img" aria-labelledby="title desc">
<title id="title">PortCVE list command output</title>
<desc id="desc">Real terminal output of `portcve list` showing local listening ports mapped to owner process, scope, and PID.</desc>
<style>
text {{ font: {FONT_SIZE}px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }}
.title {{ font: 13px system-ui, sans-serif; fill: #8b949e; }}
</style>
<rect width="{width:.0f}" height="{height:.0f}" rx="12" fill="#0d1117"/>
<rect width="{width:.0f}" height="{TITLE_H}" rx="12" fill="#161b22"/>
<rect y="{TITLE_H - 6}" width="{width:.0f}" height="6" fill="#161b22"/>
<circle cx="28" cy="{TITLE_H / 2:.0f}" r="6" fill="#f85149"/>
<circle cx="48" cy="{TITLE_H / 2:.0f}" r="6" fill="#d29922"/>
<circle cx="68" cy="{TITLE_H / 2:.0f}" r="6" fill="#3fb950"/>
<text x="{width / 2:.0f}" y="{TITLE_H / 2 + 5:.0f}" text-anchor="middle" class="title">portcve list</text>
{''.join(texts)}
</svg>
'''


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()
Loading