scripts-stack-detector
#!/usr/bin/env python3 """
title: "Stack Detector" component_type: script version: "1.0.0" audience: contributor status: stable summary: "CODITECT Dynamic Security Profiling - Technology Stack Detector" keywords: ['database', 'detector', 'security', 'stack', 'validation'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "stack-detector.py" language: python executable: true usage: "python3 scripts/stack-detector.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false
CODITECT Dynamic Security Profiling - Technology Stack Detector
Detects technology stack (languages, frameworks, databases, cloud providers) and generates security profiles with allowed command allowlists.
Usage:
python3 scripts/stack-detector.py # Generate profile
python3 scripts/stack-detector.py --refresh # Force refresh
python3 scripts/stack-detector.py --show # Display profile
python3 scripts/stack-detector.py --check
Author: AZ1.AI INC Version: 1.0.0 """
import argparse import hashlib import json import sys from datetime import datetime, timezone, timedelta from pathlib import Path from typing import Dict, List, Set, Optional
class TechnologyStack: """Technology stack detection results."""
def __init__(
self,
languages: Set[str],
frameworks: Set[str],
databases: Set[str],
cloud_providers: Set[str],
build_tools: Set[str],
version_control: Set[str],
custom_scripts: Dict[str, List[str]]
):
self.languages = languages
self.frameworks = frameworks
self.databases = databases
self.cloud_providers = cloud_providers
self.build_tools = build_tools
self.version_control = version_control
self.custom_scripts = custom_scripts
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
"languages": sorted(self.languages),
"frameworks": sorted(self.frameworks),
"databases": sorted(self.databases),
"cloud_providers": sorted(self.cloud_providers),
"build_tools": sorted(self.build_tools),
"version_control": sorted(self.version_control),
"custom_scripts": self.custom_scripts
}
class CoditectStackDetector: """Detect technology stack and generate security profiles."""
def __init__(self, project_root: Path, config_path: Path):
self.project_root = project_root
self.config_path = config_path
self.profile_path = project_root / ".coditect" / "security-profile.json"
# Load stack command mappings
with open(config_path, 'r') as f:
self.stack_config = json.load(f)
def detect_all(self) -> TechnologyStack:
"""Detect all technologies in the project."""
return TechnologyStack(
languages=self._detect_languages(),
frameworks=self._detect_frameworks(),
databases=self._detect_databases(),
cloud_providers=self._detect_cloud_providers(),
build_tools=self._detect_build_tools(),
version_control=self._detect_version_control(),
custom_scripts=self._detect_custom_scripts()
)
def _detect_languages(self) -> Set[str]:
"""Detect programming languages."""
detected = set()
lang_config = self.stack_config["mappings"]["languages"]
for lang, info in lang_config.items():
for pattern in info["patterns"]:
if self._find_files(pattern):
detected.add(lang)
break
return detected
def _detect_frameworks(self) -> Set[str]:
"""Detect frameworks."""
detected = set()
framework_config = self.stack_config["mappings"]["frameworks"]
for framework, info in framework_config.items():
for pattern in info["patterns"]:
if self._find_files(pattern):
detected.add(framework)
break
return detected
def _detect_databases(self) -> Set[str]:
"""Detect databases."""
detected = set()
db_config = self.stack_config["mappings"]["databases"]
for db, info in db_config.items():
for pattern in info.get("patterns", []):
if pattern and self._find_files(pattern):
detected.add(db)
break
return detected
def _detect_cloud_providers(self) -> Set[str]:
"""Detect cloud providers."""
detected = set()
cloud_config = self.stack_config["mappings"]["cloud_providers"]
for provider, info in cloud_config.items():
for pattern in info["patterns"]:
if self._find_files(pattern):
detected.add(provider)
break
return detected
def _detect_build_tools(self) -> Set[str]:
"""Detect build tools."""
detected = set()
build_config = self.stack_config["mappings"]["build_tools"]
for tool, info in build_config.items():
for pattern in info["patterns"]:
if self._find_files(pattern):
detected.add(tool)
break
return detected
def _detect_version_control(self) -> Set[str]:
"""Detect version control systems."""
detected = set()
vcs_config = self.stack_config["mappings"]["version_control"]
for vcs, info in vcs_config.items():
for pattern in info["patterns"]:
if self._find_files(pattern):
detected.add(vcs)
break
return detected
def _detect_custom_scripts(self) -> Dict[str, List[str]]:
"""Detect custom scripts from package.json, Makefile, pyproject.toml."""
scripts = {}
# package.json scripts
package_json = self.project_root / "package.json"
if package_json.exists():
try:
with open(package_json, 'r') as f:
data = json.load(f)
if "scripts" in data:
scripts["package.json"] = list(data["scripts"].keys())
except (json.JSONDecodeError, IOError):
pass
# Makefile targets
makefile = self.project_root / "Makefile"
if not makefile.exists():
makefile = self.project_root / "makefile"
if makefile.exists():
try:
with open(makefile, 'r') as f:
targets = []
for line in f:
line = line.strip()
if line and not line.startswith('#') and ':' in line:
target = line.split(':')[0].strip()
if target and not target.startswith('.'):
targets.append(target)
if targets:
scripts["Makefile"] = targets
except IOError:
pass
# pyproject.toml scripts
pyproject = self.project_root / "pyproject.toml"
if pyproject.exists():
try:
# Simple TOML parsing for [tool.poetry.scripts] or [project.scripts]
with open(pyproject, 'r') as f:
content = f.read()
if "[tool.poetry.scripts]" in content or "[project.scripts]" in content:
# Extract script names (basic parsing)
script_names = []
in_scripts = False
for line in content.split('\n'):
line = line.strip()
if line.startswith('[') and 'scripts]' in line:
in_scripts = True
continue
if in_scripts:
if line.startswith('['):
break
if '=' in line:
name = line.split('=')[0].strip()
if name:
script_names.append(name)
if script_names:
scripts["pyproject.toml"] = script_names
except IOError:
pass
return scripts
def _find_files(self, pattern: str) -> bool:
"""Check if files matching pattern exist in project."""
# Handle exact file names
if not any(c in pattern for c in ['*', '?', '[']):
return (self.project_root / pattern).exists()
# Handle glob patterns
if pattern.endswith('/'):
# Directory check
return (self.project_root / pattern.rstrip('/')).is_dir()
# File glob
try:
matches = list(self.project_root.rglob(pattern))
return len(matches) > 0
except (ValueError, OSError):
return False
def generate_allowed_commands(self, stack: TechnologyStack) -> Set[str]:
"""Generate allowed commands based on detected stack."""
allowed = set(self.stack_config["always_allowed"])
# Add commands for detected languages
for lang in stack.languages:
if lang in self.stack_config["mappings"]["languages"]:
allowed.update(self.stack_config["mappings"]["languages"][lang]["commands"])
# Add commands for detected frameworks
for framework in stack.frameworks:
if framework in self.stack_config["mappings"]["frameworks"]:
allowed.update(self.stack_config["mappings"]["frameworks"][framework]["commands"])
# Add commands for detected databases
for db in stack.databases:
if db in self.stack_config["mappings"]["databases"]:
allowed.update(self.stack_config["mappings"]["databases"][db]["commands"])
# Add commands for detected cloud providers
for provider in stack.cloud_providers:
if provider in self.stack_config["mappings"]["cloud_providers"]:
allowed.update(self.stack_config["mappings"]["cloud_providers"][provider]["commands"])
# Add commands for detected build tools
for tool in stack.build_tools:
if tool in self.stack_config["mappings"]["build_tools"]:
allowed.update(self.stack_config["mappings"]["build_tools"][tool]["commands"])
# Add commands for detected version control
for vcs in stack.version_control:
if vcs in self.stack_config["mappings"]["version_control"]:
allowed.update(self.stack_config["mappings"]["version_control"][vcs]["commands"])
return allowed
def generate_command_patterns(self, stack: TechnologyStack) -> Set[str]:
"""Generate allowed command patterns (e.g., 'npm run *')."""
patterns = set()
# Add patterns for detected frameworks
for framework in stack.frameworks:
if framework in self.stack_config["mappings"]["frameworks"]:
patterns.update(self.stack_config["mappings"]["frameworks"][framework].get("command_patterns", []))
# Add patterns for build tools
for tool in stack.build_tools:
if tool in self.stack_config["mappings"]["build_tools"]:
patterns.update(self.stack_config["mappings"]["build_tools"][tool].get("command_patterns", []))
# Add patterns for version control
for vcs in stack.version_control:
if vcs in self.stack_config["mappings"]["version_control"]:
patterns.update(self.stack_config["mappings"]["version_control"][vcs].get("command_patterns", []))
return patterns
def compute_project_hash(self) -> str:
"""Compute hash of key project files for cache invalidation."""
hasher = hashlib.sha256()
# Key files to monitor for changes
key_files = [
"package.json",
"requirements.txt",
"pyproject.toml",
"Cargo.toml",
"go.mod",
"Gemfile",
"composer.json",
"Makefile",
"makefile"
]
for filename in sorted(key_files):
filepath = self.project_root / filename
if filepath.exists():
try:
with open(filepath, 'rb') as f:
hasher.update(f.read())
except IOError:
pass
return hasher.hexdigest()
def generate_profile(self, force_refresh: bool = False) -> dict:
"""Generate security profile."""
# Check if cached profile is valid
if not force_refresh and self.profile_path.exists():
try:
with open(self.profile_path, 'r') as f:
profile = json.load(f)
# Check if expired
expires_at = datetime.fromisoformat(profile["expires_at"].replace('Z', '+00:00'))
if datetime.now(timezone.utc) < expires_at:
# Check if project hash matches
current_hash = self.compute_project_hash()
if current_hash == profile["project_hash"]:
return profile
except (json.JSONDecodeError, IOError, KeyError):
pass
# Detect stack
stack = self.detect_all()
# Generate allowed commands
allowed_commands = self.generate_allowed_commands(stack)
command_patterns = self.generate_command_patterns(stack)
# Create profile
profile = {
"version": "1.0.0",
"created_at": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
"expires_at": (datetime.now(timezone.utc) + timedelta(days=7)).isoformat().replace('+00:00', 'Z'),
"project_hash": self.compute_project_hash(),
"detected_stack": stack.to_dict(),
"allowed_commands": sorted(allowed_commands),
"command_patterns": sorted(command_patterns),
"blocked_commands": self.stack_config["always_blocked"],
"custom_scripts": stack.custom_scripts
}
# Save profile
self.profile_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.profile_path, 'w') as f:
json.dump(profile, f, indent=2)
f.write('\n')
return profile
def is_command_allowed(self, command: str, profile: Optional[dict] = None) -> bool:
"""Check if a command is allowed by the security profile."""
if profile is None:
if not self.profile_path.exists():
profile = self.generate_profile()
else:
with open(self.profile_path, 'r') as f:
profile = json.load(f)
# Extract base command
base_command = command.split()[0] if ' ' in command else command
# Check blocked list first
if base_command in profile["blocked_commands"]:
return False
# Check allowed list
if base_command in profile["allowed_commands"]:
return True
# Check command patterns
for pattern in profile.get("command_patterns", []):
# Simple wildcard matching
if pattern.endswith('*'):
prefix = pattern[:-1]
if command.startswith(prefix):
return True
return False
def main(): # Find repo root script_path = Path(file).resolve() repo_root = script_path.parent.parent config_path = repo_root / "config" / "stack-commands.json"
# Parse arguments
parser = argparse.ArgumentParser(
description="CODITECT Dynamic Security Profiling - Technology Stack Detector",
epilog="Detects technology stack and generates security profiles"
)
parser.add_argument(
"--refresh",
action="store_true",
help="Force refresh profile (ignore cache)"
)
parser.add_argument(
"--show",
action="store_true",
help="Display current security profile"
)
parser.add_argument(
"--check",
metavar="COMMAND",
help="Check if command is allowed"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
args = parser.parse_args()
# Create detector
detector = CoditectStackDetector(repo_root, config_path)
# Handle --check
if args.check:
allowed = detector.is_command_allowed(args.check)
if args.json:
print(json.dumps({"command": args.check, "allowed": allowed}, indent=2))
else:
status = "ALLOWED" if allowed else "BLOCKED"
print(f"{status}: {args.check}")
sys.exit(0 if allowed else 1)
# Generate profile
profile = detector.generate_profile(force_refresh=args.refresh)
# Handle --show
if args.show:
if args.json:
print(json.dumps(profile, indent=2))
else:
print("\nSecurity Profile:")
print("=" * 70)
print(f"\nDetected Stack:")
for category, items in profile["detected_stack"].items():
if items:
print(f" {category.replace('_', ' ').title()}: {', '.join(items)}")
print(f"\nAllowed Commands ({len(profile['allowed_commands'])}):")
for cmd in profile["allowed_commands"][:20]:
print(f" - {cmd}")
if len(profile["allowed_commands"]) > 20:
print(f" ... and {len(profile['allowed_commands']) - 20} more")
if profile.get("command_patterns"):
print(f"\nAllowed Patterns:")
for pattern in profile["command_patterns"]:
print(f" - {pattern}")
if profile.get("custom_scripts"):
print(f"\nCustom Scripts:")
for source, scripts in profile["custom_scripts"].items():
print(f" {source}: {', '.join(scripts[:5])}")
if len(scripts) > 5:
print(f" ... and {len(scripts) - 5} more")
print(f"\nProfile valid until: {profile['expires_at']}")
print(f"Profile hash: {profile['project_hash'][:16]}...")
print(f"\nSaved to: {detector.profile_path}")
else:
# Default output
stack = profile["detected_stack"]
print("\nTechnology Stack Detected:")
print(f" Languages: {', '.join(stack['languages']) or 'none'}")
print(f" Frameworks: {', '.join(stack['frameworks']) or 'none'}")
print(f" Databases: {', '.join(stack['databases']) or 'none'}")
print(f" Cloud: {', '.join(stack['cloud_providers']) or 'none'}")
print(f" Build Tools: {', '.join(stack['build_tools']) or 'none'}")
print(f" Version Control: {', '.join(stack['version_control']) or 'none'}")
print(f"\nAllowed {len(profile['allowed_commands'])} commands")
print(f"Profile saved to: {detector.profile_path}")
print(f"Valid for 7 days (until {profile['expires_at']})")
if name == "main": main()