#!/usr/bin/env python3 """ DSS Security Check Hook Validates file edits for common security vulnerabilities. Written from scratch for DSS - no external dependencies. """ import json import os import sys from datetime import datetime from pathlib import Path # Security patterns to detect SECURITY_PATTERNS = [ { "id": "xss-innerhtml", "patterns": [".innerHTML =", ".innerHTML=", "innerHTML:"], "severity": "high", "message": "Potential XSS: innerHTML assignment detected. Use textContent for plain text or sanitize HTML with DOMPurify.", "file_types": [".js", ".jsx", ".ts", ".tsx"] }, { "id": "xss-dangerously", "patterns": ["dangerouslySetInnerHTML"], "severity": "high", "message": "Potential XSS: dangerouslySetInnerHTML detected. Ensure content is sanitized before rendering.", "file_types": [".js", ".jsx", ".ts", ".tsx"] }, { "id": "eval-usage", "patterns": ["eval(", "new Function("], "severity": "critical", "message": "Code injection risk: eval() or new Function() detected. These can execute arbitrary code.", "file_types": [".js", ".jsx", ".ts", ".tsx"] }, { "id": "document-write", "patterns": ["document.write("], "severity": "medium", "message": "Deprecated: document.write() detected. Use DOM manipulation methods instead.", "file_types": [".js", ".jsx", ".ts", ".tsx", ".html"] }, { "id": "sql-injection", "patterns": ["execute(f\"", "execute(f'", "cursor.execute(\"", ".query(`${"], "severity": "critical", "message": "Potential SQL injection: String interpolation in SQL query. Use parameterized queries.", "file_types": [".py", ".js", ".ts"] }, { "id": "hardcoded-secret", "patterns": ["password=", "api_key=", "secret=", "token=", "apiKey:"], "severity": "high", "message": "Potential hardcoded secret detected. Use environment variables instead.", "file_types": [".py", ".js", ".ts", ".jsx", ".tsx"] }, { "id": "python-pickle", "patterns": ["pickle.load", "pickle.loads"], "severity": "high", "message": "Insecure deserialization: pickle can execute arbitrary code. Use JSON for untrusted data.", "file_types": [".py"] }, { "id": "python-shell", "patterns": ["os.system(", "subprocess.call(shell=True", "subprocess.run(shell=True"], "severity": "high", "message": "Shell injection risk: Use subprocess with shell=False and pass args as list.", "file_types": [".py"] }, { "id": "react-ref-current", "patterns": ["ref.current.innerHTML"], "severity": "high", "message": "XSS via React ref: Avoid setting innerHTML on refs. Use state/props instead.", "file_types": [".jsx", ".tsx"] }, { "id": "unsafe-regex", "patterns": ["new RegExp(", "RegExp("], "severity": "medium", "message": "Potential ReDoS: Dynamic regex from user input can cause denial of service.", "file_types": [".js", ".ts", ".jsx", ".tsx"] } ] def get_config(): """Load hook configuration.""" config_path = Path.home() / ".dss" / "hooks-config.json" default_config = { "security_check": { "enabled": True, "block_on_critical": False, "warn_only": True, "ignored_patterns": [] } } if config_path.exists(): try: with open(config_path) as f: user_config = json.load(f) return {**default_config, **user_config} except: pass return default_config def check_content(content: str, file_path: str) -> list: """Check content for security patterns.""" issues = [] file_ext = Path(file_path).suffix.lower() for pattern_def in SECURITY_PATTERNS: # Skip if file type doesn't match if file_ext not in pattern_def.get("file_types", []): continue for pattern in pattern_def["patterns"]: if pattern.lower() in content.lower(): issues.append({ "id": pattern_def["id"], "severity": pattern_def["severity"], "message": pattern_def["message"], "pattern": pattern }) break # One match per pattern definition is enough return issues def format_output(issues: list, file_path: str) -> str: """Format issues for display.""" if not issues: return "" severity_icons = { "critical": "[CRITICAL]", "high": "[HIGH]", "medium": "[MEDIUM]", "low": "[LOW]" } lines = [f"\n=== DSS Security Check: {file_path} ===\n"] for issue in issues: icon = severity_icons.get(issue["severity"], "[?]") lines.append(f"{icon} {issue['message']}") lines.append(f" Pattern: {issue['pattern']}\n") lines.append("=" * 50) return "\n".join(lines) def main(): """Main hook entry point.""" config = get_config() if not config.get("security_check", {}).get("enabled", True): sys.exit(0) # Read hook input from stdin try: input_data = json.loads(sys.stdin.read()) except json.JSONDecodeError: sys.exit(0) # Allow tool to proceed if we can't parse tool_name = input_data.get("tool_name", "") tool_input = input_data.get("tool_input", {}) # Only check Edit and Write tools if tool_name not in ["Edit", "Write"]: sys.exit(0) file_path = tool_input.get("file_path", "") # Get content to check if tool_name == "Write": content = tool_input.get("content", "") elif tool_name == "Edit": content = tool_input.get("new_string", "") else: content = "" if not content or not file_path: sys.exit(0) # Check for security issues issues = check_content(content, file_path) if issues: output = format_output(issues, file_path) print(output, file=sys.stderr) # Check if we should block on critical issues has_critical = any(i["severity"] == "critical" for i in issues) if has_critical and config.get("security_check", {}).get("block_on_critical", False): sys.exit(2) # Block the tool sys.exit(0) # Allow tool to proceed if __name__ == "__main__": main()