ui-ux-pro-max-skill/scripts/validate-csv.py
Andras Polgar 3da52ff1ca
security: harden skill against agent-audit risk patterns (#417)
Addresses the behavioral patterns that plausibly triggered the Gen
(Agent Trust Hub) High Risk rating on skills.sh, without changing
runtime functionality:

- Prerequisites no longer instruct agents to run sudo/brew/apt/winget
  install commands; agents must ask the user to install Python instead
  (SKILL.md, skill-content.md template, README.md, README.zh.md)
- Soften coercive invocation language ("Must Use" -> "Primary Use
  Cases") and replace keyword-stuffed frontmatter descriptions with
  factual ones (SKILL.md, quick-reference.md, claude/droid.json,
  plugin.json)
- Remove design.csv, draft.csv and _sync_all.py: unused by the runtime
  (not registered in core.py CSV_CONFIG) and containing prompt-shaped
  "System Prompt: ... You are ..." blocks that read as injection risk
- Fix path traversal in --persist: new safe_slug() restricts project
  and page names to [a-z0-9_-], so ../ in -p/--page can no longer
  escape the design-system/ output folder

Verified: validate-csv.py (35 files), smoke-domains (12/12),
smoke-stacks (22/22), check:assets in sync, persist traversal attempt
stays confined.

Co-authored-by: Andras Polgar <5525341+ruredi@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 21:30:06 +07:00

90 lines
2.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate UI/UX Pro Max CSV data files.
Checks every CSV under src/ui-ux-pro-max/data for structural issues that
csv.DictReader otherwise accepts silently:
- duplicate or blank header names
- rows with too many fields (unquoted commas)
- rows with too few fields (missing trailing columns)
- unexpected empty rows inside the dataset
"""
from __future__ import annotations
import csv
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DATA_DIR = REPO_ROOT / "src" / "ui-ux-pro-max" / "data"
# Every CSV under data/ is a runtime dataset loaded by core.py.
# (Former reference-only notes design.csv/draft.csv were removed: they were
# unused by the runtime and contained prompt-shaped free-form text.)
REFERENCE_ONLY: set[Path] = set()
def validate_file(path: Path) -> list[str]:
errors: list[str] = []
rel = path.relative_to(REPO_ROOT)
with path.open("r", encoding="utf-8", newline="") as fh:
reader = csv.reader(fh)
try:
header = next(reader)
except StopIteration:
errors.append(f"{rel}: empty file")
return errors
if not header or all(not col.strip() for col in header):
errors.append(f"{rel}: missing header")
return errors
blank_headers = [idx + 1 for idx, col in enumerate(header) if not col.strip()]
if blank_headers:
errors.append(f"{rel}: blank header columns {blank_headers}")
duplicates = sorted({col for col in header if col and header.count(col) > 1})
if duplicates:
errors.append(f"{rel}: duplicate headers {duplicates}")
expected = len(header)
for line_no, row in enumerate(reader, start=2):
if not row or all(not cell.strip() for cell in row):
errors.append(f"{rel}:{line_no}: blank row")
continue
actual = len(row)
if actual != expected:
errors.append(
f"{rel}:{line_no}: expected {expected} fields, got {actual}"
)
return errors
def main() -> int:
if not DATA_DIR.exists():
print(f"CSV data directory not found: {DATA_DIR}", file=sys.stderr)
return 2
errors: list[str] = []
checked = 0
for path in sorted(DATA_DIR.rglob("*.csv")):
if path in REFERENCE_ONLY:
continue
checked += 1
errors.extend(validate_file(path))
if errors:
print("CSV validation failed:", file=sys.stderr)
for error in errors:
print(f" - {error}", file=sys.stderr)
print(f"\nChecked {checked} runtime CSV files.", file=sys.stderr)
return 1
print(f"CSV validation passed: {checked} runtime CSV files checked.")
return 0
if __name__ == "__main__":
raise SystemExit(main())