Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
201
dss-claude-plugin/hooks/scripts/security-check.py
Executable file
201
dss-claude-plugin/hooks/scripts/security-check.py
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user