From b226692326bf0f710be2ed3535911229f0144a4c Mon Sep 17 00:00:00 2001 From: GraphTheory Date: Thu, 4 Jun 2026 12:53:52 -0400 Subject: [PATCH] feat: add SVG safety score badge command (Fixes #27) Co-authored-by: lb1192176991-lab --- scripts/rugguard.py | 73 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_checks.py | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/scripts/rugguard.py b/scripts/rugguard.py index 4117171..63107eb 100644 --- a/scripts/rugguard.py +++ b/scripts/rugguard.py @@ -1745,6 +1745,50 @@ def format_json(report: RugReport) -> str: return json.dumps(report.to_dict(), indent=2, default=str) +def _svg_badge(report: RugReport, style: str = "flat", label: str = "safety") -> str: + """Generate a shields.io-compatible SVG badge.""" + score = report.safety_score + level = report.risk_level + if score >= 70: + bg = "#4c1" + elif score >= 40: + bg = "#e67e22" + elif score >= 20: + bg = "#e74c3c" + else: + bg = "#c0392b" + + label_text = label + value_text = str(score) + "/100 - " + level + label_w = max(len(label_text) * 7 + 10, 40) + value_w = len(value_text) * 7 + 10 + total_w = label_w + value_w + h = 20 + rx = 3 if style == "flat" else 0 + lx = label_w // 2 + vx = label_w + value_w // 2 + + lines = [] + lines.append('') + lines.append('' + '' + '' + '') + lines.append('') + lines.append('') + lines.append('') + lines.append('') + lines.append('' + _escape_svg(label_text) + '') + lines.append('' + _escape_svg(value_text) + '') + lines.append('') + return "\n".join(lines) + + +def _escape_svg(text: str) -> str: + return (text.replace("&", "&").replace("<", "<").replace(">", ">")) + + def _report_csv_rows(report: RugReport) -> list[dict]: """Build a list of flat dicts (one per token) for CSV/JSONL export from a token report.""" d = report.to_dict() @@ -2030,6 +2074,31 @@ def cli_wallet(args: list[str]) -> None: sys.exit(2) +def cli_badge(args: list[str]) -> None: + """Generate an SVG safety score badge for a token.""" + if not args: + print('Usage: python rugguard.py badge [--style flat|flat-square|plastic]', + '[--label TEXT]', file=sys.stderr) + sys.exit(1) + + mint = args[0] + style = "flat" + label = "safety" + + for idx, a in enumerate(args): + if a.startswith("--style="): + style = a.split("=", 1)[1] + elif a == "--style" and idx + 1 < len(args): + style = args[idx + 1] + if a.startswith("--label="): + label = a.split("=", 1)[1] + elif a == "--label" and idx + 1 < len(args): + label = args[idx + 1] + + report = rug_check_token(mint.strip()) + print(_svg_badge(report, style=style, label=label)) + + def cli_compare(args: list[str]) -> None: """Compare multiple tokens side-by-side.""" if not args: @@ -2095,6 +2164,7 @@ def cli_help() -> None: USAGE: python rugguard.py token [--json|--markdown] python rugguard.py wallet + python rugguard.py badge [--style flat|flat-square|plastic] [--label TEXT] python rugguard.py compare [ ...] [--json] python rugguard.py watch [--interval 60] [--iterations 0] [--history PATH] [--webhook URL] [--threshold SCORE] @@ -2120,6 +2190,7 @@ def cli_help() -> None: python rugguard.py token DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 --markdown python rugguard.py wallet 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM python rugguard.py wallet 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM --export jsonl + python rugguard.py badge DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 python rugguard.py compare DezXAZ8z... EPjFWdd5... [--json] python rugguard.py watch --iterations 1 --threshold 70 @@ -2141,6 +2212,8 @@ def main() -> None: cli_token(args) elif cmd == "wallet": cli_wallet(args) + elif cmd == "badge": + cli_badge(args) elif cmd == "compare": cli_compare(args) elif cmd == "watch": diff --git a/tests/test_checks.py b/tests/test_checks.py index 8ab29a3..d6c66ed 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -694,3 +694,72 @@ def test_sort_by_name(self): def test_empty_no_crash(self): from rugguard import _format_comparison_table assert _format_comparison_table([]) == "" + + +# ── Badge Tests ─────────────────────────────────────────────────────────── + +class TestBadge: + def test_badge_green_low(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=95, risk_level='LOW', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r) + assert '#4c1' in svg # green + assert '95/100' in svg + assert 'LOW' in svg + + def test_badge_yellow_medium(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=55, risk_level='MEDIUM', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r) + assert '#e67e22' in svg # yellow + assert '55/100' in svg + + def test_badge_red_high(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=25, risk_level='HIGH', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r) + assert '#e74c3c' in svg # red + + def test_badge_darkred_critical(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=10, risk_level='CRITICAL', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r) + assert '#c0392b' in svg # dark red + + def test_badge_custom_label(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=80, risk_level='LOW', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r, label='rugcheck') + assert 'rugcheck' in svg + + def test_badge_is_valid_svg(self): + from rugguard import RugFlags, RugScore, TokenMeta, _svg_badge + flags = RugFlags() + r = RugReport( + token=TokenMeta(address='A'), safety_score=80, risk_level='LOW', + score=RugScore(), flags=flags, warnings=[], recommendation='', + ) + svg = _svg_badge(r) + assert svg.startswith('') + assert 'xmlns=' in svg