Skip to content

Commit 8b02a82

Browse files
committed
fix(update): address review feedback from #26
- Sanitize externally-sourced version strings before terminal output to prevent escape sequence injection from malicious upstream tags - Replace O(categories × results) summary loop with single-pass Counter - Simplify sort_tools_by_name to printf + sort Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
1 parent c331ff9 commit 8b02a82

2 files changed

Lines changed: 35 additions & 28 deletions

File tree

audit.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import argparse
1818
import json
1919
import os
20+
import re
2021
import sys
22+
from collections import Counter
2123
from concurrent.futures import ThreadPoolExecutor, as_completed
2224

2325
# Add current directory to path
@@ -43,6 +45,16 @@
4345
update_local_installation, merge_for_display, build_legacy_snapshot,
4446
)
4547

48+
# Strip control characters from externally-sourced strings (e.g. GitHub tags)
49+
# to prevent terminal escape sequence injection.
50+
_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
51+
52+
53+
def _sanitize(s: str) -> str:
54+
"""Remove control characters from a string for safe terminal display."""
55+
return _CONTROL_CHAR_RE.sub("", s) if s else s
56+
57+
4658
# Configuration from environment
4759
OFFLINE_MODE = os.environ.get("CLI_AUDIT_OFFLINE", "0") == "1"
4860
MAX_WORKERS = int(os.environ.get("CLI_AUDIT_MAX_WORKERS", "16"))
@@ -601,8 +613,8 @@ def cmd_update(args: argparse.Namespace) -> int:
601613
latest_color = BLUE
602614
op = "?"
603615

604-
inst_display = inst if inst else "n/a"
605-
latest_display = latest if latest else "n/a"
616+
inst_display = _sanitize(inst) if inst else "n/a"
617+
latest_display = _sanitize(latest) if latest else "n/a"
606618

607619
# Add pinned/skip markers (reuse catalog from outer scope)
608620
markers = []
@@ -643,31 +655,29 @@ def cmd_update(args: argparse.Namespace) -> int:
643655
executor.shutdown(wait=False, cancel_futures=True)
644656
raise
645657

646-
# Print grouped summary
658+
# Print grouped summary (single-pass counting)
659+
status_counts: dict[str, Counter] = {cat: Counter() for cat in sorted_cats}
660+
for r in results:
661+
cat = r.get("category", "general")
662+
if cat in status_counts:
663+
status_counts[cat][r.get("status")] += 1
664+
647665
print(f"\n# Summary by category:", file=sys.stderr)
648666
for category in sorted_cats:
649-
cat_tools = categorized[category]
650667
icon = CATEGORY_ICON.get(category, "📦")
651668
desc = CATEGORY_DESC.get(category, category)
652-
# Count statuses for this category
653-
cat_names = {t.name for t in cat_tools}
654-
cat_results = [r for r in results if r.get("tool") in cat_names]
655-
up_to_date = sum(1 for r in cat_results if r.get("status") == "UP-TO-DATE")
656-
outdated = sum(1 for r in cat_results if r.get("status") == "OUTDATED")
657-
not_installed = sum(1 for r in cat_results if r.get("status") == "NOT INSTALLED")
658-
conflict = sum(1 for r in cat_results if r.get("status") == "CONFLICT")
659-
unknown = sum(1 for r in cat_results if r.get("status") == "UNKNOWN")
669+
counts = status_counts[category]
660670
parts = []
661-
if up_to_date:
662-
parts.append(f"{GREEN}{up_to_date} current{RESET}")
663-
if outdated:
664-
parts.append(f"{YELLOW}{outdated} outdated{RESET}")
665-
if not_installed:
666-
parts.append(f"{BLUE}{not_installed} missing{RESET}")
667-
if conflict:
668-
parts.append(f"{YELLOW}{conflict} conflict{RESET}")
669-
if unknown:
670-
parts.append(f"{BLUE}{unknown} unknown{RESET}")
671+
if counts["UP-TO-DATE"]:
672+
parts.append(f"{GREEN}{counts['UP-TO-DATE']} current{RESET}")
673+
if counts["OUTDATED"]:
674+
parts.append(f"{YELLOW}{counts['OUTDATED']} outdated{RESET}")
675+
if counts["NOT INSTALLED"]:
676+
parts.append(f"{BLUE}{counts['NOT INSTALLED']} missing{RESET}")
677+
if counts["CONFLICT"]:
678+
parts.append(f"{YELLOW}{counts['CONFLICT']} conflict{RESET}")
679+
if counts["UNKNOWN"]:
680+
parts.append(f"{BLUE}{counts['UNKNOWN']} unknown{RESET}")
671681
summary = ", ".join(parts) if parts else "–"
672682
print(f"# {icon} {desc}: {summary}", file=sys.stderr)
673683

@@ -700,8 +710,8 @@ def cmd_update(args: argparse.Namespace) -> int:
700710
inst_color = BLUE
701711
op = "?"
702712

703-
inst_display = inst if inst else "n/a"
704-
latest_display = latest if latest else "n/a"
713+
inst_display = _sanitize(inst) if inst else "n/a"
714+
latest_display = _sanitize(latest) if latest else "n/a"
705715
inst_fmt = f"{inst_color}{inst_display}{RESET}"
706716
latest_fmt = f"{BOLD_GREEN}{latest_display}{RESET}"
707717
print(f"# → {versioned_name}: {inst_fmt} {op} {latest_fmt}", file=sys.stderr, flush=True)

scripts/guide.sh

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -801,10 +801,7 @@ category_matches_filter() {
801801

802802
# Helper: sort tools alphabetically by name within a category
803803
sort_tools_by_name() {
804-
local tools="$1"
805-
for tool in $tools; do
806-
echo "$tool"
807-
done | sort
804+
printf '%s\n' $1 | sort
808805
}
809806

810807
# Process tools grouped by category (in category order)

0 commit comments

Comments
 (0)