vibesec-backend / scanner /update_rules.py
dhruvkachhela
Migrate backend to Gemini 3.1 Flash Lite and optimize JS/TS rulesets
c972e32
Raw
History Blame Contribute Delete
7.58 kB
"""
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()