Spaces:
Sleeping
Sleeping
| """ | |
| VibeSec — Semgrep Rules Dynamic Sync Engine | |
| Checks, validates, and atomically updates local community rulesets from the official Semgrep registry. | |
| Supports both JSON and YAML registry formats. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| import json | |
| import hashlib | |
| import tempfile | |
| import logging | |
| from pathlib import Path | |
| import requests | |
| # Set up logging matching vibesec pattern | |
| logger = logging.getLogger("vibesec.rules_sync") | |
| if not logger.handlers: | |
| import sys | |
| sh = logging.StreamHandler(sys.stdout) | |
| sh.setFormatter(logging.Formatter('[Rules Sync] %(asctime)s - %(levelname)s - %(message)s')) | |
| logger.addHandler(sh) | |
| logger.setLevel(logging.INFO) | |
| RULES_DIR = Path(__file__).parent.parent / "rules" | |
| # Maps Semgrep registry rule pack names to our local target files | |
| RULESET_MAPPINGS = { | |
| "p/owasp-top-ten": "community-owasp-top-ten.json", | |
| "p/javascript": "community-javascript.json", | |
| "p/nodejs": "community-nodejs.json", | |
| "p/python": "community-python.json", | |
| "p/react": "community-react.json", | |
| } | |
| def get_semgrep_executable() -> str: | |
| """Finds the semgrep executable in the active python virtual environment or PATH.""" | |
| semgrep_bin = "semgrep" | |
| venv_bin_dir = os.path.dirname(sys.executable) | |
| for ext in ["", ".exe"]: | |
| candidate = os.path.join(venv_bin_dir, f"semgrep{ext}") | |
| if os.path.exists(candidate): | |
| return candidate | |
| return semgrep_bin | |
| def compute_sha256(file_path: Path) -> str: | |
| """Computes SHA-256 hash of a file.""" | |
| sha256 = hashlib.sha256() | |
| with open(file_path, "rb") as f: | |
| while chunk := f.read(8192): | |
| sha256.update(chunk) | |
| return sha256.hexdigest() | |
| def validate_ruleset_with_semgrep(rules_file_path: Path) -> bool: | |
| """Uses basic syntax parsing (JSON/YAML) to ensure the ruleset is syntactically sound without timeouts.""" | |
| if not rules_file_path.exists(): | |
| return False | |
| ext = rules_file_path.suffix.lower() | |
| try: | |
| content = rules_file_path.read_text(encoding="utf-8") | |
| if ext == ".json": | |
| import json | |
| json.loads(content) | |
| elif ext in {".yaml", ".yml"}: | |
| import yaml | |
| yaml.safe_load(content) | |
| return True | |
| except Exception as e: | |
| logger.error(f"Syntax validation failed for {rules_file_path.name}: {e}") | |
| return False | |
| def sync_single_ruleset(registry_name: str, local_filename: str) -> bool: | |
| """ | |
| Downloads, validates, and atomically updates a single ruleset file. | |
| Returns True if an update was written, False if skipped or failed. | |
| """ | |
| local_path = RULES_DIR / local_filename | |
| temp_file_path = None | |
| url = f"https://semgrep.dev/c/{registry_name}" | |
| logger.info(f"Checking {registry_name} -> {local_filename}...") | |
| try: | |
| # 1. Download | |
| headers = {"User-Agent": "VibeSec-Rules-Sync/2.0 (Security Scanner Sync)"} | |
| response = requests.get(url, headers=headers, timeout=30) | |
| if response.status_code != 200: | |
| logger.error(f" [ERROR] Failed to download {registry_name}: HTTP {response.status_code}") | |
| return False | |
| # 2. Parse and Validate JSON or YAML structure | |
| data = None | |
| try: | |
| data = response.json() | |
| except ValueError: | |
| # Fallback to YAML parsing | |
| try: | |
| import yaml | |
| data = yaml.safe_load(response.text) | |
| except Exception as yaml_err: | |
| logger.error(f" [ERROR] Registry returned invalid JSON and YAML formats for {registry_name}: {yaml_err}") | |
| return False | |
| if not isinstance(data, dict) or "rules" not in data or not isinstance(data["rules"], list): | |
| logger.error(f" [ERROR] Invalid ruleset structure in registry response for {registry_name}.") | |
| return False | |
| rule_count = len(data["rules"]) | |
| if rule_count == 0: | |
| logger.error(f" [ERROR] Downloaded ruleset for {registry_name} is empty (0 rules). Aborting.") | |
| return False | |
| # 3. Create a temporary file to run Semgrep validation | |
| os.makedirs(RULES_DIR, exist_ok=True) | |
| # Use tempfile inside the target rules directory to guarantee they're on the same drive (needed for atomic os.replace) | |
| with tempfile.NamedTemporaryFile(dir=str(RULES_DIR), suffix=".json", delete=False, mode="w", encoding="utf-8") as tf: | |
| json.dump(data, tf, indent=2) | |
| temp_file_path = Path(tf.name) | |
| # 4. Perform dynamic validation using Semgrep CLI compiler validation | |
| if not validate_ruleset_with_semgrep(temp_file_path): | |
| logger.error(f" [ERROR] Semgrep compilation validation failed for downloaded {registry_name}. Rejecting update.") | |
| try: | |
| os.unlink(temp_file_path) | |
| except Exception: | |
| pass | |
| return False | |
| # 5. Compare with current local ruleset via SHA-256 hash | |
| new_hash = compute_sha256(temp_file_path) | |
| if local_path.exists(): | |
| old_hash = compute_sha256(local_path) | |
| if new_hash == old_hash: | |
| logger.info(f" [OK] Up-to-date. (Hash match: {new_hash[:8]})") | |
| try: | |
| os.unlink(temp_file_path) | |
| except Exception: | |
| pass | |
| return False | |
| logger.info(f" [UPDATE] Update detected! Hash changed from {old_hash[:8]} to {new_hash[:8]} ({rule_count} rules).") | |
| else: | |
| logger.info(f" [UPDATE] Fresh installation of ruleset ({rule_count} rules).") | |
| # 6. Atomic swap | |
| os.replace(temp_file_path, local_path) | |
| logger.info(f" [SUCCESS] Atomically updated {local_filename} ({(local_path.stat().st_size/1024):.1f} KB)") | |
| return True | |
| except Exception as e: | |
| logger.error(f" [ERROR] Error syncing {registry_name}: {e}") | |
| if temp_file_path and temp_file_path.exists(): | |
| try: | |
| os.unlink(temp_file_path) | |
| except Exception: | |
| pass | |
| return False | |
| def sync_all_community_rulesets() -> dict[str, str]: | |
| """Syncs all community rulesets and returns a summary dict.""" | |
| logger.info("=========================================") | |
| logger.info(" VibeSec Community Rulesets Sync Engine ") | |
| logger.info("=========================================") | |
| summary = {} | |
| updated_count = 0 | |
| failed_count = 0 | |
| for registry_name, local_filename in RULESET_MAPPINGS.items(): | |
| success = sync_single_ruleset(registry_name, local_filename) | |
| if success: | |
| summary[registry_name] = "Updated" | |
| updated_count += 1 | |
| else: | |
| # We check if local_path exists to classify if it's up to date or failed | |
| local_path = RULES_DIR / local_filename | |
| if local_path.exists(): | |
| summary[registry_name] = "Up-to-date" | |
| else: | |
| summary[registry_name] = "Failed" | |
| failed_count += 1 | |
| logger.info("\n=========================================") | |
| logger.info(f" Sync Complete: {updated_count} updated, {failed_count} failed, {len(RULESET_MAPPINGS)-updated_count-failed_count} already current.") | |
| logger.info("=========================================\n") | |
| return summary | |
| if __name__ == "__main__": | |
| sync_all_community_rulesets() | |