mirror of
https://github.com/aljazceru/Auditor.git
synced 2025-12-17 03:24:18 +01:00
226 lines
8.2 KiB
Python
226 lines
8.2 KiB
Python
"""Rules command - inspect and summarize detection capabilities."""
|
|
|
|
import os
|
|
import yaml
|
|
import importlib
|
|
import inspect
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any
|
|
|
|
import click
|
|
|
|
from theauditor.utils import handle_exceptions
|
|
from theauditor.utils.exit_codes import ExitCodes
|
|
|
|
|
|
@click.command(name="rules")
|
|
@click.option(
|
|
"--summary",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Generate a summary of all detection capabilities",
|
|
)
|
|
@handle_exceptions
|
|
def rules_command(summary: bool) -> None:
|
|
"""Inspect and summarize TheAuditor's detection rules and patterns.
|
|
|
|
Args:
|
|
summary: If True, generate a comprehensive capability report
|
|
"""
|
|
if not summary:
|
|
click.echo(click.style("[ERROR] Please specify --summary to generate a capability report", fg="red"), err=True)
|
|
raise SystemExit(ExitCodes.TASK_INCOMPLETE)
|
|
|
|
# Get the base path for patterns and rules
|
|
base_path = Path(__file__).parent.parent
|
|
patterns_path = base_path / "patterns"
|
|
rules_path = base_path / "rules"
|
|
|
|
# Create output directory
|
|
output_dir = Path(".pf")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
output_file = output_dir / "auditor_capabilities.md"
|
|
|
|
# Collect output in a list
|
|
output_lines = []
|
|
output_lines.append("# TheAuditor Detection Capabilities\n")
|
|
|
|
# Also print to console
|
|
print("# TheAuditor Detection Capabilities\n")
|
|
|
|
# Scan YAML patterns
|
|
print("## YAML Patterns\n")
|
|
output_lines.append("## YAML Patterns\n")
|
|
yaml_patterns = scan_yaml_patterns(patterns_path)
|
|
total_patterns = 0
|
|
|
|
for category, files in yaml_patterns.items():
|
|
if files:
|
|
category_display = "patterns/" if category == "." else f"patterns/{category}/"
|
|
print(f"### {category_display}\n")
|
|
output_lines.append(f"### {category_display}\n")
|
|
for file_name, patterns in files.items():
|
|
if patterns:
|
|
print(f"**{file_name}** ({len(patterns)} patterns)")
|
|
output_lines.append(f"**{file_name}** ({len(patterns)} patterns)")
|
|
for pattern in patterns:
|
|
print(f"- `{pattern}`")
|
|
output_lines.append(f"- `{pattern}`")
|
|
print()
|
|
output_lines.append("")
|
|
total_patterns += len(patterns)
|
|
|
|
# Scan Python rules
|
|
print("## Python AST Rules\n")
|
|
output_lines.append("## Python AST Rules\n")
|
|
python_rules = scan_python_rules(rules_path)
|
|
total_rules = 0
|
|
|
|
for module_path, functions in python_rules.items():
|
|
if functions:
|
|
# Make path relative to rules/ for readability
|
|
display_path = module_path.replace(str(rules_path) + os.sep, "")
|
|
print(f"### {display_path}")
|
|
output_lines.append(f"### {display_path}")
|
|
for func in functions:
|
|
print(f"- `{func}()`")
|
|
output_lines.append(f"- `{func}()`")
|
|
print()
|
|
output_lines.append("")
|
|
total_rules += len(functions)
|
|
|
|
# Print summary statistics
|
|
print("## Summary Statistics\n")
|
|
output_lines.append("## Summary Statistics\n")
|
|
print(f"- **Total YAML Patterns**: {total_patterns}")
|
|
output_lines.append(f"- **Total YAML Patterns**: {total_patterns}")
|
|
print(f"- **Total Python Rules**: {total_rules}")
|
|
output_lines.append(f"- **Total Python Rules**: {total_rules}")
|
|
print(f"- **Combined Detection Capabilities**: {total_patterns + total_rules}")
|
|
output_lines.append(f"- **Combined Detection Capabilities**: {total_patterns + total_rules}")
|
|
|
|
# Write to file
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write('\n'.join(output_lines))
|
|
|
|
click.echo(click.style(f"\n[SUCCESS] Capability report generated successfully", fg="green"))
|
|
click.echo(f"[INFO] Report saved to: {output_file}")
|
|
raise SystemExit(ExitCodes.SUCCESS)
|
|
|
|
|
|
def scan_yaml_patterns(patterns_path: Path) -> Dict[str, Dict[str, List[str]]]:
|
|
"""Scan YAML pattern files and extract pattern names.
|
|
|
|
Args:
|
|
patterns_path: Path to the patterns directory
|
|
|
|
Returns:
|
|
Dictionary mapping category -> file -> list of pattern names
|
|
"""
|
|
results = {}
|
|
|
|
if not patterns_path.exists():
|
|
return results
|
|
|
|
# Walk through all subdirectories
|
|
for root, dirs, files in os.walk(patterns_path):
|
|
# Skip __pycache__ directories
|
|
dirs[:] = [d for d in dirs if d != "__pycache__"]
|
|
|
|
for file in files:
|
|
if file.endswith(".yml") or file.endswith(".yaml"):
|
|
file_path = Path(root) / file
|
|
|
|
# Determine category from directory structure
|
|
rel_path = file_path.relative_to(patterns_path)
|
|
# If file is in root of patterns/, use "." as category
|
|
# If in subdirectory like frameworks/, use that as category
|
|
if rel_path.parent == Path("."):
|
|
category = "."
|
|
else:
|
|
category = str(rel_path.parent)
|
|
|
|
if category not in results:
|
|
results[category] = {}
|
|
|
|
# Parse YAML and extract pattern names
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = yaml.safe_load(f)
|
|
|
|
if data and isinstance(data, list):
|
|
pattern_names = []
|
|
for pattern in data:
|
|
if isinstance(pattern, dict) and 'name' in pattern:
|
|
pattern_names.append(pattern['name'])
|
|
|
|
if pattern_names:
|
|
results[category][file] = pattern_names
|
|
|
|
except (yaml.YAMLError, OSError) as e:
|
|
# Skip files that can't be parsed
|
|
continue
|
|
|
|
return results
|
|
|
|
|
|
def scan_python_rules(rules_path: Path) -> Dict[str, List[str]]:
|
|
"""Scan Python rule files and find all find_* functions.
|
|
|
|
Args:
|
|
rules_path: Path to the rules directory
|
|
|
|
Returns:
|
|
Dictionary mapping module path -> list of find_* function names
|
|
"""
|
|
results = {}
|
|
|
|
if not rules_path.exists():
|
|
return results
|
|
|
|
# First, check what's exposed in the main __init__.py
|
|
init_file = rules_path / "__init__.py"
|
|
if init_file.exists():
|
|
try:
|
|
module = importlib.import_module("theauditor.rules")
|
|
exposed_functions = []
|
|
for name, obj in inspect.getmembers(module, inspect.isfunction):
|
|
if name.startswith("find_"):
|
|
exposed_functions.append(name)
|
|
if exposed_functions:
|
|
results["rules/__init__.py (exposed)"] = exposed_functions
|
|
except ImportError:
|
|
pass
|
|
|
|
# Walk through all Python files
|
|
for root, dirs, files in os.walk(rules_path):
|
|
# Skip __pycache__ directories
|
|
dirs[:] = [d for d in dirs if d != "__pycache__"]
|
|
|
|
for file in files:
|
|
if file.endswith(".py"):
|
|
file_path = Path(root) / file
|
|
|
|
# Skip __init__.py files for now (we handle them separately)
|
|
if file == "__init__.py":
|
|
continue
|
|
|
|
# Try basic text scanning (more reliable than import)
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Simple regex to find function definitions
|
|
import re
|
|
pattern = r'^def\s+(find_\w+)\s*\('
|
|
matches = re.findall(pattern, content, re.MULTILINE)
|
|
|
|
if matches:
|
|
# Make path relative for display
|
|
display_path = str(file_path.relative_to(rules_path.parent))
|
|
results[display_path] = matches
|
|
|
|
except OSError:
|
|
continue
|
|
|
|
return results |