🤖 AIGC disclosure: created with “AI-assisted generation + human testing”, published after community-terms review.
#!/usr/bin/env python3
# mtr-report.py (FIXED) — MTR report parsing and evidence generation (standard library only)
# Fixes (R7 audit P1):
# 1) The original NUM regex did not match real mtr output: after the hop number comes "N.|--"
# (no space after the dot, with a pipe), while the old pattern required "\d+\.\s+--?" → always a miss.
# The Loss column also carries a "%", and the old ([\d.]+) would swallow the digits before the %
# and then break on it → the whole line failed to match.
# 2) Removed the never-used LINE regex (dead code, and its parentheses did not balance).
# 3) Tolerates "???" (timed-out hop) hostnames and reverse-resolved hostnames (-z still recommended).
# Usage: mtr -rwzbc 100 TARGET_IP > raw.txt && python3 mtr-report.py raw.txt TARGET_IP
import sys, re, json, datetime
NUM = re.compile(
r"^\s*(\d+)\.\s*\|?--\s*" # hop number + optional pipe + double dash ("1.|--" / "1. --")
r"(\S+.*?)\s+" # host (including "???")
r"([\d.]+)%\s+" # Loss% (the percent sign is required)
r"(\d+)\s+" # Snt
r"([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*$" # Last Avg Best Wrst StDev
)
def parse(path):
hops = []
for ln in open(path, encoding="utf-8", errors="replace"):
m = NUM.match(ln.rstrip())
if not m:
continue
hops.append(dict(
hop=int(m.group(1)), host=m.group(2).strip(),
loss=float(m.group(3)), snt=int(m.group(4)),
last=float(m.group(5)), avg=float(m.group(6)),
best=float(m.group(7)), wrst=float(m.group(8)),
stdev=float(m.group(9))))
return hops
def judge(hops):
flags = []
last = hops[-1] if hops else None
for h in hops:
if h["loss"] > 0 and h is not last:
after = [x for x in hops if x["hop"] > h["hop"]]
if after and all(x["loss"] == 0 for x in after):
flags.append((h["hop"], "ICMP_RATE_LIMITED",
"intermediate hop loss with clean hops after it: suspected ICMP rate limiting by the router, not a fault"))
if h["avg"] > 0 and h["stdev"] > h["avg"] * 0.5:
flags.append((h["hop"], "HIGH_JITTER",
f"StDev {h['stdev']} > Avg {h['avg']}×0.5: significant jitter"))
verdict = "PATH_OK"
if last and last["loss"] > 0:
verdict = "REAL_LOSS"
flags.append((last["hop"], "REAL_LOSS",
f"sustained loss at the final hop {last['loss']}%: real loss, safe to open a ticket"))
return verdict, flags
def main():
if len(sys.argv) < 3:
print(__doc__); sys.exit(1)
src, target = sys.argv[1], sys.argv[2]
out = sys.argv[3] if len(sys.argv) > 3 else "mtr-evidence"
hops = parse(src)
if not hops:
print("no hops parsed (confirm the input is mtr --report / -rwzbc format)"); sys.exit(2)
verdict, flags = judge(hops)
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
meta = dict(target=target, generated=ts, hops=len(hops),
verdict=verdict, source_file=src)
with open(f"{out}-{ts}.json", "w", encoding="utf-8") as f:
json.dump(dict(meta=meta, hops=hops,
flags=[dict(hop=a, tag=b, note=c) for a, b, c in flags]),
f, ensure_ascii=False, indent=1)
md = ["# MTR evidence file", "",
f"- Target: `{target}` · Generated: {ts} · Verdict: **{verdict}**", "",
"| Hop | Host | Loss% | Snt | Avg | Wrst | StDev |",
"|---|---|---|---|---|---|---|"]
for h in hops:
md.append(f"| {h['hop']} | {h['host']} | {h['loss']} | {h['snt']} "
f"| {h['avg']} | {h['wrst']} | {h['stdev']} |")
if flags:
md += ["", "## Automatic verdict"]
md += [f"- hop {a} [{b}] {c}" for a, b, c in flags]
with open(f"{out}-{ts}.md", "w", encoding="utf-8") as f:
f.write("\n".join(md) + "\n")
print(f"verdict={verdict} · wrote {out}-{ts}.json / {out}-{ts}.md")
if __name__ == "__main__":
main()
──────────────────────
[Test log · fixed version] Parses a real `mtr -rwzbc 100` sample correctly (6 hops, including a "???" timed-out hop and a 100.0% loss line); the REAL_LOSS / ICMP_RATE_LIMITED / HIGH_JITTER verdicts were validated against constructed samples.
[What was fixed] The R7 audit found the original regex did not match real mtr output (the hop column is "N.|--" and the Loss column carries a %). It is now \d+\.\s*\|?-- with a ([\d.]+)% capture, and the unused LINE dead code is gone.
[Ticket convention] Paste the .md file straight into the ticket: verdict line + per-hop table + automatic reading.
[Known limits] It is most stable on -z (no reverse DNS) output; if you want reverse-resolved names, rerun with `mtr --report-wide -z`.